diff --git a/plugins/providers/docker/action.rb b/plugins/providers/docker/action.rb index f423f6e65..b512da387 100644 --- a/plugins/providers/docker/action.rb +++ b/plugins/providers/docker/action.rb @@ -161,6 +161,7 @@ module VagrantPlugins b4.use action_halt b4.use HostMachineSyncFoldersDisable b4.use Destroy + b4.use DestroyNetwork b4.use DestroyBuildImage else b4.use Message, @@ -243,6 +244,7 @@ module VagrantPlugins b2.use PrepareNFSValidIds b2.use SyncedFolderCleanup b2.use PrepareNFSSettings + b2.use PrepareNetworks b2.use Login b2.use Build @@ -265,6 +267,7 @@ module VagrantPlugins end end + b2.use ConnectNetworks b2.use Start b2.use WaitForRunning @@ -292,9 +295,11 @@ module VagrantPlugins action_root = Pathname.new(File.expand_path("../action", __FILE__)) autoload :Build, action_root.join("build") autoload :CompareSyncedFolders, action_root.join("compare_synced_folders") + autoload :ConnectNetworks, action_root.join("connect_networks") autoload :Create, action_root.join("create") autoload :Destroy, action_root.join("destroy") autoload :DestroyBuildImage, action_root.join("destroy_build_image") + autoload :DestroyNetwork, action_root.join("destroy_network") autoload :ForwardedPorts, action_root.join("forwarded_ports") autoload :HasSSH, action_root.join("has_ssh") autoload :HostMachine, action_root.join("host_machine") @@ -308,12 +313,13 @@ module VagrantPlugins autoload :IsBuild, action_root.join("is_build") autoload :IsHostMachineCreated, action_root.join("is_host_machine_created") autoload :Login, action_root.join("login") - autoload :Pull, action_root.join("pull") - autoload :PrepareSSH, action_root.join("prepare_ssh") - autoload :Stop, action_root.join("stop") + autoload :PrepareNetworks, action_root.join("prepare_networks") autoload :PrepareNFSValidIds, action_root.join("prepare_nfs_valid_ids") autoload :PrepareNFSSettings, action_root.join("prepare_nfs_settings") + autoload :PrepareSSH, action_root.join("prepare_ssh") + autoload :Pull, action_root.join("pull") autoload :Start, action_root.join("start") + autoload :Stop, action_root.join("stop") autoload :WaitForRunning, action_root.join("wait_for_running") end end diff --git a/plugins/providers/docker/action/connect_networks.rb b/plugins/providers/docker/action/connect_networks.rb new file mode 100644 index 000000000..b1d1670a5 --- /dev/null +++ b/plugins/providers/docker/action/connect_networks.rb @@ -0,0 +1,77 @@ +require 'ipaddr' +require 'log4r' + +require 'vagrant/util/scoped_hash_override' + +module VagrantPlugins + module DockerProvider + module Action + class ConnectNetworks + + include Vagrant::Util::ScopedHashOverride + + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new('vagrant::plugins::docker::connectnetworks') + end + + # Generate CLI arguments for creating the docker network. + # + # @param [Hash] options Options from the network config + # @returns[Array Network create arguments + def generate_connect_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 + + # 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 + + env[:ui].info(I18n.t("docker_provider.network_connect")) + + connections = env[:docker_connects] || {} + + machine.config.vm.networks.each_with_index do |args, idx| + type, options = args + next if type != :private_network && type != :public_network + + network_options = scoped_hash_override(options, :docker_connect) + network_options.delete_if{|k,_| options.key?(k)} + network_name = connections[idx] + + if !network_name + raise Errors::NetworkNameMissing, + index: idx, + container: machine.name + end + + @logger.debug("Connecting network #{network_name} to container guest #{machine.name}") + if options[:ip] && options[:type] != "dhcp" + if IPAddr.new(options[:ip]).ipv4? + network_options[:ip] = options[:ip] + else + network_options[:ip6] = options[:ip] + end + end + network_options[:alias] = options[:alias] if options[:alias] + connect_opts = generate_connect_cli_arguments(network_options) + machine.provider.driver.connect_network(network_name, machine.id, connect_opts) + end + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/docker/action/destroy_network.rb b/plugins/providers/docker/action/destroy_network.rb new file mode 100644 index 000000000..d2aa613ec --- /dev/null +++ b/plugins/providers/docker/action/destroy_network.rb @@ -0,0 +1,50 @@ +require 'log4r' + +module VagrantPlugins + module DockerProvider + module Action + class DestroyNetwork + + @@lock = Mutex.new + + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new('vagrant::plugins::docker::network') + end + + 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 + + @@lock.synchronize do + machine.env.lock("docker-network-destroy", retry: true) do + machine.config.vm.networks.each do |type, options| + next if type != :private_network && type != :public_network + + vagrant_networks = machine.provider.driver.list_network_names.find_all do |n| + n.start_with?("vagrant_network") + end + + vagrant_networks.each do |network_name| + if machine.provider.driver.existing_named_network?(network_name) && + !machine.provider.driver.network_used?(network_name) + env[:ui].info(I18n.t("docker_provider.network_destroy", network_name: network_name)) + machine.provider.driver.rm_network(network_name) + else + @logger.debug("Network #{network_name} not found or in use") + end + end + end + end + end + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/docker/action/prepare_networks.rb b/plugins/providers/docker/action/prepare_networks.rb new file mode 100644 index 000000000..4d1904e85 --- /dev/null +++ b/plugins/providers/docker/action/prepare_networks.rb @@ -0,0 +1,355 @@ +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.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 + # + # @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) + base_opts[:subnet] = "#{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.name, 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 + # + # @param [Hash] network_options Docker scoped networking options + # @param [String] 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, + 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) + puts "we in here" + env[:ui].warn(I18n.t( + "docker_provider.network_bridge_iprange_outofbounds", + subnet: network_options[:subnet], + range: "#{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 + "#{range}/#{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 diff --git a/plugins/providers/docker/driver.rb b/plugins/providers/docker/driver.rb index 88903039c..acad830cf 100644 --- a/plugins/providers/docker/driver.rb +++ b/plugins/providers/docker/driver.rb @@ -152,21 +152,25 @@ module VagrantPlugins def rmi(id) execute('docker', 'rmi', id) return true - rescue Exception => e + rescue => e return false if e.to_s.include?("is using it") raise if !e.to_s.include?("No such image") end + # Inspect the provided container + # + # @param [String] cid ID or name of container + # @return [Hash] def inspect_container(cid) - # DISCUSS: Is there a chance that this json will change after the container - # has been brought up? - @data ||= JSON.parse(execute('docker', 'inspect', cid)).first + JSON.parse(execute('docker', 'inspect', cid)).first end + # @return [Array] list of all container IDs def all_containers execute('docker', 'ps', '-a', '-q', '--no-trunc').to_s.split end + # @return [String] IP address of the docker bridge def docker_bridge_ip output = execute('/sbin/ip', '-4', 'addr', 'show', 'scope', 'global', 'docker0') if output =~ /^\s+inet ([0-9.]+)\/[0-9]+\s+/ @@ -177,9 +181,149 @@ module VagrantPlugins end end + # @param [String] network - name of network to connect conatiner to + # @param [String] cid - container id + # @param [Array] opts - An array of flags used for listing networks + def connect_network(network, cid, opts=nil) + command = ['docker', 'network', 'connect', network, cid].push(*opts) + output = execute(*command) + output + end + + # @param [String] network - name of network to create + # @param [Array] opts - An array of flags used for listing networks + def create_network(network, opts=nil) + command = ['docker', 'network', 'create', network].push(*opts) + output = execute(*command) + output + end + + # @param [String] network - name of network to disconnect container from + # @param [String] cid - container id + def disconnect_network(network, cid) + command = ['docker', 'network', 'disconnect', network, cid, "--force"] + output = execute(*command) + output + end + + # @param [Array] networks - list of networks to inspect + # @param [Array] opts - An array of flags used for listing networks + def inspect_network(network, opts=nil) + command = ['docker', 'network', 'inspect'] + Array(network) + command = command.push(*opts) + output = execute(*command) + begin + JSON.load(output) + rescue JSON::ParserError + @logger.warn("Failed to parse network inspection of network: #{network}") + @logger.debug("Failed network output content: `#{output.inspect}`") + nil + end + end + + # @param [String] opts - Flags used for listing networks + def list_network(*opts) + command = ['docker', 'network', 'ls', *opts] + output = execute(*command) + output + end + + # Will delete _all_ defined but unused networks in the docker engine. Even + # networks not created by Vagrant. + # + # @param [Array] opts - An array of flags used for listing networks + def prune_network(opts=nil) + command = ['docker', 'network', 'prune', '--force'].push(*opts) + output = execute(*command) + output + end + + # Delete network(s) + # + # @param [String] network - name of network to remove + def rm_network(*network) + command = ['docker', 'network', 'rm', *network] + output = execute(*command) + output + end + + # @param [Array] opts - An array of flags used for listing networks def execute(*cmd, **opts, &block) @executor.execute(*cmd, **opts, &block) end + + # ###################### + # Docker network helpers + # ###################### + + # Determines if a given network has been defined through vagrant with a given + # subnet string + # + # @param [String] subnet_string - Subnet to look for + # @return [String] network name - Name of network with requested subnet.`nil` if not found + def network_defined?(subnet_string) + all_networks = list_network_names + + network_info = inspect_network(all_networks) + network_info.each do |network| + config = network["IPAM"]["Config"] + if (config.size > 0 && + config.first["Subnet"] == subnet_string) + @logger.debug("Found existing network #{network["Name"]} already configured with #{subnet_string}") + return network["Name"] + end + end + return nil + end + + # Locate network which contains given address + # + # @param [String] address IP address + # @return [String] network name + def network_containing_address(address) + names = list_network_names + networks = inspect_network(names) + return if !networks + networks.each do |net| + next if !net["IPAM"] + config = net["IPAM"]["Config"] + next if !config || config.size < 1 + config.each do |opts| + subnet = IPAddr.new(opts["Subnet"]) + if subnet.include?(address) + return net["Name"] + end + end + end + nil + end + + # Looks to see if a docker network has already been defined + # with the given name + # + # @param [String] network_name - name of network to look for + # @return [Bool] + def existing_named_network?(network_name) + result = list_network_names + result.any?{|net_name| net_name == network_name} + end + + # @return [Array] list of all docker networks + def list_network_names + list_network("--format={{.Name}}").split("\n").map(&:strip) + end + + # Returns true or false if network is in use or not. + # Nil if Vagrant fails to receive proper JSON from `docker network inspect` + # + # @param [String] network - name of network to look for + # @return [Bool,nil] + def network_used?(network) + result = inspect_network(network) + return nil if !result + return result.first["Containers"].size > 0 + end + end end end diff --git a/plugins/providers/docker/errors.rb b/plugins/providers/docker/errors.rb index 36e1b9859..11ae6fd68 100644 --- a/plugins/providers/docker/errors.rb +++ b/plugins/providers/docker/errors.rb @@ -45,6 +45,34 @@ module VagrantPlugins error_key(:docker_provider_nfs_without_privileged) end + class NetworkAddressInvalid < DockerError + error_key(:network_address_invalid) + end + + class NetworkIPAddressRequired < DockerError + error_key(:network_address_required) + end + + class NetworkSubnetInvalid < DockerError + error_key(:network_subnet_invalid) + end + + class NetworkInvalidOption < DockerError + error_key(:network_invalid_option) + end + + class NetworkNameMissing < DockerError + error_key(:network_name_missing) + end + + class NetworkNameUndefined < DockerError + error_key(:network_name_undefined) + end + + class NetworkNoInterfaces < DockerError + error_key(:network_no_interfaces) + end + class PackageNotSupported < DockerError error_key(:package_not_supported) end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 1e4188c5b..5927d6b21 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -2148,6 +2148,8 @@ en: choice_help: |- When choosing an interface, it is usually the one that is being used to connect to the internet. + select_interface: |- + Which interface should the network bridge to? specific_not_found: |- Specific bridge '%{bridge}' not found. You may be asked to specify which network to bridge to. diff --git a/templates/locales/providers_docker.yml b/templates/locales/providers_docker.yml index ba90473c6..5c6b00018 100644 --- a/templates/locales/providers_docker.yml +++ b/templates/locales/providers_docker.yml @@ -45,6 +45,35 @@ en: This container requires a host VM, and the state of that VM is unknown. Run `vagrant up` to verify that the container and its host VM is running, then try again. + network_bridge_gateway_invalid: |- + The provided gateway IP address is invalid (%{gateway}). Please + provide a valid IP address. + network_bridge_gateway_outofbounds: |- + The provided gateway IP (%{gateway}) is not within the defined + subnet (%{subnet}). Please provide an IP address within the + defined subnet. + network_bridge_gateway_request: |- + Gateway IP address for %{interface} interface [%{default_gateway}]: + network_bridge_iprange_info: |- + When an explicit address is not provided to a container attached + to this bridged network, docker will supply an address to the + container. This is independent of the local DHCP service that + may be available on the network. + network_bridge_iprange_invalid: |- + The provided IP address range is invalid (%{range}). Please + provide a valid range. + network_bridge_iprange_outofbounds: |- + The provided IP address range (%{range}) is not within the + defined subnet (%{subnet}). Please provide an address range + within the defined subnet. + network_bridge_iprange_request: |- + Available address range for assignment on %{interface} interface [%{default_range}]: + network_create: |- + Creating and configuring docker networks... + network_connect: |- + Enabling network interfaces... + network_destroy: |- + Removing network %{network_name} ... not_created_skip: |- Container not created. Skipping. not_docker_provider: |- @@ -66,6 +95,9 @@ en: ssh_through_host_vm: |- SSH will be proxied through the Docker virtual machine since we're not running Docker natively. This is just a notice, and not an error. + subnet_exists: |- + A network called '%{network_name}' using subnet '%{subnet}' is already in use. + Using '%{network_name}' instead of creating a new network... synced_folders_changed: |- Vagrant has noticed that the synced folder definitions have changed. With Docker, these synced folder changes won't take effect until you @@ -197,6 +229,38 @@ en: is functional and properly configured. Host VM ID: %{id} + network_address_invalid: |- + The configured network address is not valid within the configured + subnet of the defined network. Please update the network settings + and try again. + + Configured address: %{address} + Network name: %{network_name} + network_address_required: |- + An IP address is required if not using `type: "dhcp"` or not specifying a `subnet`. + network_invalid_option: |- + Invalid option given for docker network for guest "%{container}". Must specify either + a `subnet` or use `type: "dhcp"`. + network_name_missing: |- + The Docker provider is unable to connect the container to the + defined network due to a missing network name. Please validate + your configuration and try again. + + Container: %{container} + Network Number: %{index} + network_name_undefined: |- + The Docker provider was unable to configure networking using the + provided network name `%{network_name}`. Please ensure the network + name is correct and exists, then try again. + network_no_interfaces: |- + The Docker provider was unable to list any available interfaces to bridge + the public network with. + network_subnet_invalid: |- + The configured network subnet is not valid for the defined network. + Please update the network settings and try again. + + Configured subnet: %{subnet} + Network name: %{network_name} package_not_supported: |- The "package" command is not supported with the Docker provider. If you'd like to commit or push your Docker container, please SSH diff --git a/test/unit/plugins/providers/docker/action/connect_networks_test.rb b/test/unit/plugins/providers/docker/action/connect_networks_test.rb new file mode 100644 index 000000000..8d57f1d4b --- /dev/null +++ b/test/unit/plugins/providers/docker/action/connect_networks_test.rb @@ -0,0 +1,128 @@ +require_relative "../../../../base" +require_relative "../../../../../../plugins/providers/docker/action/connect_networks" + + +describe VagrantPlugins::DockerProvider::Action::ConnectNetworks do + include_context "unit" + include_context "virtualbox" + + let(:sandbox) { isolated_environment } + + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + sandbox.vagrantfile("") + sandbox.create_vagrant_env + end + + let(:machine) do + iso_env.machine(iso_env.machine_names[0], :docker).tap do |m| + allow(m).to receive(:id).and_return("12345") + allow(m.provider).to receive(:driver).and_return(driver) + allow(m.provider).to receive(:host_vm?).and_return(false) + allow(m.config.vm).to receive(:networks).and_return(networks) + end + end + + let(:docker_connects) { {0=>"vagrant_network_172.20.0.0/16", 1=>"vagrant_network_public_wlp4s0", 2=>"vagrant_network_2a02:6b8:b010:9020:1::/80"} } + + let(:env) {{ machine: machine, ui: machine.ui, root_path: Pathname.new("."), + docker_connects: docker_connects }} + let(:app) { lambda { |*args| }} + let(:driver) { double("driver", create: "abcd1234") } + + let(:networks) { [[:private_network, + {:ip=>"172.20.128.2", + :subnet=>"172.20.0.0/16", + :driver=>"bridge", + :internal=>"true", + :alias=>"mynetwork", + :protocol=>"tcp", + :id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58"}], + [:public_network, + {:ip=>"172.30.130.2", + :subnet=>"172.30.0.0/16", + :driver=>"bridge", + :id=>"30e017d5-488f-5a2f-a3ke-k8dce8246b60"}], + [:private_network, + {:type=>"dhcp", + :ipv6=>"true", + :subnet=>"2a02:6b8:b010:9020:1::/80", + :protocol=>"tcp", + :id=>"b8f23054-38d5-45c3-99ea-d33fc5d1b9f2"}], + [:forwarded_port, + {:guest=>22, :host=>2200, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}]] + } + + subject { described_class.new(app, env) } + + after do + sandbox.close + end + + describe "#call" do + it "calls the next action in the chain" do + allow(driver).to receive(:host_vm?).and_return(false) + allow(driver).to receive(:connect_network).and_return(true) + + called = false + app = ->(*args) { called = true } + + action = described_class.new(app, env) + + action.call(env) + + expect(called).to eq(true) + end + + it "connects all of the avaiable networks to a container" do + expect(driver).to receive(:connect_network).with("vagrant_network_172.20.0.0/16", "12345", ["--ip", "172.20.128.2", "--alias", "mynetwork"]) + expect(driver).to receive(:connect_network).with("vagrant_network_public_wlp4s0", "12345", ["--ip", "172.30.130.2"]) + expect(driver).to receive(:connect_network).with("vagrant_network_2a02:6b8:b010:9020:1::/80", "12345", []) + + subject.call(env) + end + + context "with missing env values" do + it "raises an error if the network name is missing" do + env[:docker_connects] = {} + + expect{subject.call(env)}.to raise_error(VagrantPlugins::DockerProvider::Errors::NetworkNameMissing) + end + end + end + + describe "#generate_connect_cli_arguments" do + let(:network_options) { + {:ip=>"172.20.128.2", + :subnet=>"172.20.0.0/16", + :driver=>"bridge", + :internal=>"true", + :alias=>"mynetwork", + :protocol=>"tcp", + :id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58"} } + + let(:false_network_options) { + {:ip=>"172.20.128.2", + :subnet=>"172.20.0.0/16", + :driver=>"bridge", + :internal=>"false", + :alias=>"mynetwork", + :protocol=>"tcp", + :id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58"} } + + it "removes false values" do + cli_args = subject.generate_connect_cli_arguments(false_network_options) + expect(cli_args).to eq(["--ip", "172.20.128.2", "--subnet", "172.20.0.0/16", "--driver", "bridge", "--alias", "mynetwork", "--protocol", "tcp", "--id", "80e017d5-388f-4a2f-a3de-f8dce8156a58"]) + end + + it "removes true and leaves flag value in arguments" do + cli_args = subject.generate_connect_cli_arguments(network_options) + expect(cli_args).to eq(["--ip", "172.20.128.2", "--subnet", "172.20.0.0/16", "--driver", "bridge", "--internal", "--alias", "mynetwork", "--protocol", "tcp", "--id", "80e017d5-388f-4a2f-a3de-f8dce8156a58"]) + end + + it "takes options and generates cli flags" do + cli_args = subject.generate_connect_cli_arguments(network_options) + expect(cli_args).to eq(["--ip", "172.20.128.2", "--subnet", "172.20.0.0/16", "--driver", "bridge", "--internal", "--alias", "mynetwork", "--protocol", "tcp", "--id", "80e017d5-388f-4a2f-a3de-f8dce8156a58"]) + end + end +end diff --git a/test/unit/plugins/providers/docker/action/destroy_network_test.rb b/test/unit/plugins/providers/docker/action/destroy_network_test.rb new file mode 100644 index 000000000..e6856f022 --- /dev/null +++ b/test/unit/plugins/providers/docker/action/destroy_network_test.rb @@ -0,0 +1,104 @@ +require_relative "../../../../base" +require_relative "../../../../../../plugins/providers/docker/action/destroy_network" + +describe VagrantPlugins::DockerProvider::Action::DestroyNetwork do + include_context "unit" + + let(:sandbox) { isolated_environment } + + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + sandbox.vagrantfile("") + sandbox.create_vagrant_env + end + + let(:machine) do + iso_env.machine(iso_env.machine_names[0], :docker).tap do |m| + allow(m.provider).to receive(:driver).and_return(driver) + allow(m.config.vm).to receive(:networks).and_return(networks) + end + end + + let(:env) {{ machine: machine, ui: machine.ui, root_path: Pathname.new(".") }} + let(:app) { lambda { |*args| }} + let(:driver) { double("driver", create: "abcd1234") } + + let(:networks) { [[:private_network, + {:ip=>"172.20.128.2", + :subnet=>"172.20.0.0/16", + :driver=>"bridge", + :internal=>"true", + :alias=>"mynetwork", + :protocol=>"tcp", + :id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58"}], + [:private_network, + {:type=>"dhcp", + :ipv6=>"true", + :subnet=>"2a02:6b8:b010:9020:1::/80", + :protocol=>"tcp", + :id=>"b8f23054-38d5-45c3-99ea-d33fc5d1b9f2"}], + [:forwarded_port, + {:guest=>22, :host=>2200, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}]] + } + + subject { described_class.new(app, env) } + + after do + sandbox.close + end + + describe "#call" do + let(:network_names) { ["vagrant_network_172.20.0.0/16", "vagrant_network_2a02:6b8:b010:9020:1::/80"] } + + it "calls the next action in the chain" do + allow(driver).to receive(:host_vm?).and_return(false) + allow(driver).to receive(:existing_network?).and_return(true) + allow(driver).to receive(:network_used?).and_return(true) + allow(driver).to receive(:list_network_names).and_return([]) + + called = false + app = ->(*args) { called = true } + + action = described_class.new(app, env) + action.call(env) + + expect(called).to eq(true) + end + + it "calls the proper driver method to destroy the network" do + allow(driver).to receive(:list_network_names).and_return(network_names) + allow(driver).to receive(:host_vm?).and_return(false) + allow(driver).to receive(:existing_named_network?).with("vagrant_network_172.20.0.0/16"). + and_return(true) + allow(driver).to receive(:network_used?).with("vagrant_network_172.20.0.0/16"). + and_return(false) + allow(driver).to receive(:existing_named_network?).with("vagrant_network_2a02:6b8:b010:9020:1::/80"). + and_return(true) + allow(driver).to receive(:network_used?).with("vagrant_network_2a02:6b8:b010:9020:1::/80"). + and_return(false) + + expect(driver).to receive(:rm_network).with("vagrant_network_172.20.0.0/16").twice + expect(driver).to receive(:rm_network).with("vagrant_network_2a02:6b8:b010:9020:1::/80").twice + + subject.call(env) + end + + it "doesn't destroy the network if another container is still using it" do + allow(driver).to receive(:host_vm?).and_return(false) + allow(driver).to receive(:list_network_names).and_return(network_names) + allow(driver).to receive(:existing_named_network?).with("vagrant_network_172.20.0.0/16"). + and_return(true) + allow(driver).to receive(:network_used?).with("vagrant_network_172.20.0.0/16"). + and_return(true) + allow(driver).to receive(:existing_named_network?).with("vagrant_network_2a02:6b8:b010:9020:1::/80"). + and_return(true) + allow(driver).to receive(:network_used?).with("vagrant_network_2a02:6b8:b010:9020:1::/80"). + and_return(true) + + expect(driver).not_to receive(:rm_network).with("vagrant_network_172.20.0.0/16") + expect(driver).not_to receive(:rm_network).with("vagrant_network_2a02:6b8:b010:9020:1::/80") + + subject.call(env) + end + end +end diff --git a/test/unit/plugins/providers/docker/action/prepare_networks_test.rb b/test/unit/plugins/providers/docker/action/prepare_networks_test.rb new file mode 100644 index 000000000..1a2ccb7eb --- /dev/null +++ b/test/unit/plugins/providers/docker/action/prepare_networks_test.rb @@ -0,0 +1,340 @@ +require_relative "../../../../base" +require_relative "../../../../../../plugins/providers/docker/action/prepare_networks" + +describe VagrantPlugins::DockerProvider::Action::PrepareNetworks do + include_context "unit" + include_context "virtualbox" + + let(:sandbox) { isolated_environment } + + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + sandbox.vagrantfile("") + sandbox.create_vagrant_env + end + + let(:machine) do + iso_env.machine(iso_env.machine_names[0], :docker).tap do |m| + allow(m.provider).to receive(:driver).and_return(driver) + allow(m.config.vm).to receive(:networks).and_return(networks) + end + end + + let(:env) {{ machine: machine, ui: machine.ui, root_path: Pathname.new(".") }} + let(:app) { lambda { |*args| }} + let(:driver) { double("driver", create: "abcd1234") } + + let(:networks) { [[:private_network, + {:ip=>"172.20.128.2", + :subnet=>"172.20.0.0/16", + :driver=>"bridge", + :internal=>"true", + :alias=>"mynetwork", + :protocol=>"tcp", + :id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58"}], + [:public_network, + {:ip=>"172.30.130.2", + :subnet=>"172.30.0.0/16", + :driver=>"bridge", + :id=>"30e017d5-488f-5a2f-a3ke-k8dce8246b60"}], + [:private_network, + {:type=>"dhcp", + :ipv6=>"true", + :subnet=>"2a02:6b8:b010:9020:1::/80", + :protocol=>"tcp", + :id=>"b8f23054-38d5-45c3-99ea-d33fc5d1b9f2"}], + [:forwarded_port, + {:guest=>22, :host=>2200, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}]] + } + + let(:invalid_network) { + [[:private_network, + {:ipv6=>"true", + :protocol=>"tcp", + :id=>"b8f23054-38d5-45c3-99ea-d33fc5d1b9f2"}]] + } + + subject { described_class.new(app, env) } + + after do + sandbox.close + end + + describe "#call" do + it "calls the next action in the chain" do + allow(driver).to receive(:host_vm?).and_return(false) + allow(driver).to receive(:existing_named_network?).and_return(false) + allow(driver).to receive(:create_network).and_return(true) + + called = false + app = ->(*args) { called = true } + + action = described_class.new(app, env) + + allow(action).to receive(:process_public_network).and_return(["name", {}]) + allow(action).to receive(:process_private_network).and_return(["name", {}]) + + action.call(env) + + expect(called).to eq(true) + end + + it "calls the proper driver methods to setup a network" do + allow(driver).to receive(:host_vm?).and_return(false) + allow(driver).to receive(:existing_named_network?).and_return(false) + allow(driver).to receive(:network_containing_address). + with("172.20.128.2").and_return(nil) + allow(driver).to receive(:network_containing_address). + with("192.168.1.1").and_return(nil) + allow(driver).to receive(:network_defined?).with("172.20.128.0/24"). + and_return(false) + allow(driver).to receive(:network_defined?).with("172.30.128.0/24"). + and_return(false) + allow(driver).to receive(:network_defined?).with("2a02:6b8:b010:9020:1::/80"). + and_return(false) + + allow(subject).to receive(:request_public_gateway).and_return("1234") + allow(subject).to receive(:request_public_iprange).and_return("1234") + + expect(subject).to receive(:process_private_network).with(networks[0][1], {}, env). + and_return(["vagrant_network_172.20.128.0/24", {:ipv6=>false, :subnet=>"172.20.128.0/24"}]) + + expect(subject).to receive(:process_public_network).with(networks[1][1], {}, env). + and_return(["vagrant_network_public_wlp4s0", {"opt"=>"parent=wlp4s0", "subnet"=>"192.168.1.0/24", "driver"=>"macvlan", "gateway"=>"1234", "ipv6"=>false, "ip_range"=>"1234"}]) + + expect(subject).to receive(:process_private_network).with(networks[2][1], {}, env). + and_return(["vagrant_network_2a02:6b8:b010:9020:1::/80", {:ipv6=>true, :subnet=>"2a02:6b8:b010:9020:1::/80"}]) + + allow(machine.ui).to receive(:ask).and_return("1") + + expect(driver).to receive(:create_network). + with("vagrant_network_172.20.128.0/24", ["--subnet", "172.20.128.0/24"]) + expect(driver).to receive(:create_network). + with("vagrant_network_public_wlp4s0", ["--opt", "parent=wlp4s0", "--subnet", "192.168.1.0/24", "--driver", "macvlan", "--gateway", "1234", "--ip-range", "1234"]) + expect(driver).to receive(:create_network). + with("vagrant_network_2a02:6b8:b010:9020:1::/80", ["--ipv6", "--subnet", "2a02:6b8:b010:9020:1::/80"]) + + subject.call(env) + + expect(env[:docker_connects]).to eq({0=>"vagrant_network_172.20.128.0/24", 1=>"vagrant_network_public_wlp4s0", 2=>"vagrant_network_2a02:6b8:b010:9020:1::/80"}) + end + + it "uses an existing network if a matching subnet is found" do + allow(driver).to receive(:host_vm?).and_return(false) + allow(driver).to receive(:network_containing_address). + with("172.20.128.2").and_return(nil) + allow(driver).to receive(:network_containing_address). + with("192.168.1.1").and_return(nil) + allow(driver).to receive(:network_defined?).with("172.20.128.0/24"). + and_return("vagrant_network_172.20.128.0/24") + allow(driver).to receive(:network_defined?).with("172.30.128.0/24"). + and_return("vagrant_network_public_wlp4s0") + allow(driver).to receive(:network_defined?).with("2a02:6b8:b010:9020:1::/80"). + and_return("vagrant_network_2a02:6b8:b010:9020:1::/80") + allow(machine.ui).to receive(:ask).and_return("1") + + expect(driver).to receive(:existing_named_network?). + with("vagrant_network_172.20.128.0/24").and_return(true) + expect(driver).to receive(:existing_named_network?). + with("vagrant_network_public_wlp4s0").and_return(true) + expect(driver).to receive(:existing_named_network?). + with("vagrant_network_2a02:6b8:b010:9020:1::/80").and_return(true) + + expect(subject).to receive(:process_private_network).with(networks[0][1], {}, env). + and_return(["vagrant_network_172.20.128.0/24", {:ipv6=>false, :subnet=>"172.20.128.0/24"}]) + + expect(subject).to receive(:process_public_network).with(networks[1][1], {}, env). + and_return(["vagrant_network_public_wlp4s0", {"opt"=>"parent=wlp4s0", "subnet"=>"192.168.1.0/24", "driver"=>"macvlan", "gateway"=>"1234", "ipv6"=>false, "ip_range"=>"1234"}]) + + expect(subject).to receive(:process_private_network).with(networks[2][1], {}, env). + and_return(["vagrant_network_2a02:6b8:b010:9020:1::/80", {:ipv6=>true, :subnet=>"2a02:6b8:b010:9020:1::/80"}]) + expect(driver).not_to receive(:create_network) + + expect(subject).to receive(:validate_network_configuration!). + with("vagrant_network_172.20.128.0/24", networks[0][1], + {:ipv6=>false, :subnet=>"172.20.128.0/24"}, driver) + + expect(subject).to receive(:validate_network_configuration!). + with("vagrant_network_public_wlp4s0", networks[1][1], + {"opt"=>"parent=wlp4s0", "subnet"=>"192.168.1.0/24", "driver"=>"macvlan", "gateway"=>"1234", "ipv6"=>false, "ip_range"=>"1234"}, driver) + + expect(subject).to receive(:validate_network_configuration!). + with("vagrant_network_2a02:6b8:b010:9020:1::/80", networks[2][1], + {:ipv6=>true, :subnet=>"2a02:6b8:b010:9020:1::/80"}, driver) + + subject.call(env) + end + + it "raises an error if an inproper network configuration is given" do + allow(machine.config.vm).to receive(:networks).and_return(invalid_network) + allow(driver).to receive(:host_vm?).and_return(false) + allow(driver).to receive(:existing_network?).and_return(false) + + expect{ subject.call(env) }.to raise_error(VagrantPlugins::DockerProvider::Errors::NetworkIPAddressRequired) + end + end + + describe "#generate_create_cli_arguments" do + let(:network_options) { + {:ip=>"172.20.128.2", + :subnet=>"172.20.0.0/16", + :driver=>"bridge", + :internal=>"true", + :alias=>"mynetwork", + :protocol=>"tcp", + :id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58"} } + + let(:false_network_options) { + {:ip=>"172.20.128.2", + :subnet=>"172.20.0.0/16", + :driver=>"bridge", + :internal=>"false", + :alias=>"mynetwork", + :protocol=>"tcp", + :id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58"} } + + it "returns an array of cli arguments" do + cli_args = subject.generate_create_cli_arguments(network_options) + expect(cli_args).to eq( ["--ip", "172.20.128.2", "--subnet", "172.20.0.0/16", "--driver", "bridge", "--internal", "--alias", "mynetwork", "--protocol", "tcp", "--id", "80e017d5-388f-4a2f-a3de-f8dce8156a58"]) + end + + it "removes option if set to false" do + cli_args = subject.generate_create_cli_arguments(false_network_options) + expect(cli_args).to eq( ["--ip", "172.20.128.2", "--subnet", "172.20.0.0/16", "--driver", "bridge", "--alias", "mynetwork", "--protocol", "tcp", "--id", "80e017d5-388f-4a2f-a3de-f8dce8156a58"]) + end + end + + describe "#validate_network_name!" do + let(:netname) { "vagrant_network" } + + it "returns true if name exists" do + allow(driver).to receive(:existing_named_network?).with(netname). + and_return(true) + + expect(subject.validate_network_name!(netname, env)).to be_truthy + end + + it "raises an error if name does not exist" do + allow(driver).to receive(:existing_named_network?).with(netname). + and_return(false) + + expect{subject.validate_network_name!(netname, env)}.to raise_error(VagrantPlugins::DockerProvider::Errors::NetworkNameUndefined) + end + end + + describe "#validate_network_configuration!" do + let(:netname) { "vagrant_network_172.20.128.0/24" } + let(:options) { {:ip=>"172.20.128.2", :subnet=>"172.20.0.0/16", :driver=>"bridge", :internal=>"true", :alias=>"mynetwork", :protocol=>"tcp", :id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58", :netmask=>24} } + let(:network_options) { {:ipv6=>false, :subnet=>"172.20.128.0/24"} } + + it "returns true if all options are valid" do + allow(driver).to receive(:network_containing_address).with(options[:ip]). + and_return(netname) + allow(driver).to receive(:network_containing_address).with(network_options[:subnet]). + and_return(netname) + + expect(subject.validate_network_configuration!(netname, options, network_options, driver)). + to be_truthy + end + + it "raises an error of the address is invalid" do + allow(driver).to receive(:network_containing_address).with(options[:ip]). + and_return("fakename") + expect{subject.validate_network_configuration!(netname, options, network_options, driver)}. + to raise_error(VagrantPlugins::DockerProvider::Errors::NetworkAddressInvalid) + end + + it "raises an error of the subnet is invalid" do + allow(driver).to receive(:network_containing_address).with(options[:ip]). + and_return(netname) + allow(driver).to receive(:network_containing_address).with(network_options[:subnet]). + and_return("fakename") + + expect{subject.validate_network_configuration!(netname, options, network_options, driver)}. + to raise_error(VagrantPlugins::DockerProvider::Errors::NetworkSubnetInvalid) + end + end + + describe "#process_private_network" do + let(:options) { {:ip=>"172.20.128.2", :subnet=>"172.20.0.0/16", :driver=>"bridge", :internal=>"true", :alias=>"mynetwork", :protocol=>"tcp", :id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58", :netmask=>24} } + let(:dhcp_options) { {type: "dhcp"} } + let(:bad_options) { {driver: "bridge"} } + + it "generates a network name and config for a dhcp private network" do + network_name, network_options = subject.process_private_network(dhcp_options, {}, env) + + expect(network_name).to eq("vagrant_network") + expect(network_options).to eq({}) + end + + it "generates a network name and options for a static ip" do + allow(driver).to receive(:network_defined?).and_return(nil) + network_name, network_options = subject.process_private_network(options, {}, env) + expect(network_name).to eq("vagrant_network_172.20.0.0/16") + expect(network_options).to eq({:ipv6=>false, :subnet=>"172.20.0.0/16"}) + end + + it "raises an error if no ip address or type `dhcp` was given" do + expect{subject.process_private_network(bad_options, {}, env)}. + to raise_error(VagrantPlugins::DockerProvider::Errors::NetworkIPAddressRequired) + end + end + + describe "#process_public_network" do + let(:options) { {:ip=>"172.30.130.2", :subnet=>"172.30.0.0/16", :driver=>"bridge", :id=>"30e017d5-488f-5a2f-a3ke-k8dce8246b60"} } + let(:ipaddr) { double("ipaddr", prefix: 22, succ: "10.1.10.2", ipv6?: false) } + + it "raises an error if there are no network interfaces" do + expect(subject).to receive(:list_interfaces).and_return([]) + + expect{subject.process_public_network(options, {}, env)}. + to raise_error(VagrantPlugins::DockerProvider::Errors::NetworkNoInterfaces) + end + + it "generates a network name and configuration" do + allow(machine.ui).to receive(:ask).and_return("1") + allow(subject).to receive(:request_public_gateway).and_return("1234") + allow(subject).to receive(:request_public_iprange).and_return("1234") + allow(IPAddr).to receive(:new).and_return(ipaddr) + allow(driver).to receive(:existing_named_network?).and_return(false) + allow(driver).to receive(:network_containing_address). + with("10.1.10.2").and_return("vagrant_network_public") + + network_name, network_options = subject.process_public_network(options, {}, env) + expect(network_name).to eq("vagrant_network_public") + end + end + + describe "#request_public_gateway" do + let(:options) { {:ip=>"172.30.130.2", :subnet=>"172.30.0.0/16", :driver=>"bridge", :id=>"30e017d5-488f-5a2f-a3ke-k8dce8246b60"} } + let(:ipaddr) { double("ipaddr", to_s: "172.30.130.2", prefix: 22, succ: "172.30.130.3", + ipv6?: false) } + + it "requests a gateway" do + allow(IPAddr).to receive(:new).and_return(ipaddr) + allow(ipaddr).to receive(:include?).and_return(false) + allow(machine.ui).to receive(:ask).and_return("1") + + addr = subject.request_public_gateway(options, "bridge", env) + + expect(addr).to eq("172.30.130.2") + end + end + + describe "#request_public_iprange" do + let(:options) { {:ip=>"172.30.130.2", :subnet=>"172.30.0.0/16", :driver=>"bridge", :id=>"30e017d5-488f-5a2f-a3ke-k8dce8246b60"} } + let(:ipaddr) { double("ipaddr", to_s: "172.30.100.2", prefix: 22, succ: "172.30.100.3", + ipv6?: false) } + let(:subnet) { double("ipaddr", to_s: "172.30.130.2", prefix: 22, succ: "172.30.130.3", + ipv6?: false) } + + it "requests a public ip range" do + allow(IPAddr).to receive(:new).with(options[:subnet]).and_return(subnet) + allow(IPAddr).to receive(:new).with("172.30.130.2").and_return(ipaddr) + allow(subnet).to receive(:include?).and_return(true) + allow(machine.ui).to receive(:ask).and_return(options[:ip]) + + addr = subject.request_public_iprange(options, "bridge", env) + end + end +end diff --git a/test/unit/plugins/providers/docker/driver_test.rb b/test/unit/plugins/providers/docker/driver_test.rb index fb271af54..6504876cb 100644 --- a/test/unit/plugins/providers/docker/driver_test.rb +++ b/test/unit/plugins/providers/docker/driver_test.rb @@ -10,6 +10,149 @@ describe VagrantPlugins::DockerProvider::Driver do allow(subject).to receive(:execute) { |*args| @cmd = args.join(' ') } end + let(:docker_network_struct) { +[ + { + "Name": "bridge", + "Id": "ae74f6cc18bbcde86326937797070b814cc71bfc4a6d8e3e8cf3b2cc5c7f4a7d", + "Created": "2019-03-20T14:10:06.313314662-07:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": nil, + "Config": [ + { + "Subnet": "172.17.0.0/16", + "Gateway": "172.17.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Containers": { + "a1ee9b12bcea8268495b1f43e8d1285df1925b7174a695075f6140adb9415d87": { + "Name": "vagrant-sandbox_docker-1_1553116237", + "EndpointID": "fc1b0ed6e4f700cf88bb26a98a0722655191542e90df3e3492461f4d1f3c0cae", + "MacAddress": "02:42:ac:11:00:02", + "IPv4Address": "172.17.0.2/16", + "IPv6Address": "" + } + }, + "Options": { + "com.docker.network.bridge.default_bridge": "true", + "com.docker.network.bridge.enable_icc": "true", + "com.docker.network.bridge.enable_ip_masquerade": "true", + "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", + "com.docker.network.bridge.name": "docker0", + "com.docker.network.driver.mtu": "1500" + }, + "Labels": {} + }, + { + "Name": "host", + "Id": "2a2845e77550e33bf3e97bda8b71477ac7d3ccf78bc9102585fdb6056fb84cbf", + "Created": "2018-09-28T10:54:08.633543196-07:00", + "Scope": "local", + "Driver": "host", + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": nil, + "Config": [] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Containers": {}, + "Options": {}, + "Labels": {} + }, + { + "Name": "vagrant_network", + "Id": "93385d4fd3cf7083a36e62fa72a0ad0a21203d0ddf48409c32b550cd8462b3ba", + "Created": "2019-03-20T14:10:36.828235585-07:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": {}, + "Config": [ + { + "Subnet": "172.18.0.0/16", + "Gateway": "172.18.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Containers": { + "a1ee9b12bcea8268495b1f43e8d1285df1925b7174a695075f6140adb9415d87": { + "Name": "vagrant-sandbox_docker-1_1553116237", + "EndpointID": "9502cd9d37ae6815e3ffeb0bc2de9b84f79e7223e8a1f8f4ccc79459e96c7914", + "MacAddress": "02:42:ac:12:00:02", + "IPv4Address": "172.18.0.2/16", + "IPv6Address": "" + } + }, + "Options": {}, + "Labels": {} + }, + { + "Name": "vagrant_network_172.20.0.0/16", + "Id": "649f0ab3ef0eef6f2a025c0d0398bd7b9b4d05ec88b0d7bd573b44153d903cfb", + "Created": "2019-03-20T14:10:37.088885647-07:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": {}, + "Config": [ + { + "Subnet": "172.20.0.0/16" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Containers": { + "a1ee9b12bcea8268495b1f43e8d1285df1925b7174a695075f6140adb9415d87": { + "Name": "vagrant-sandbox_docker-1_1553116237", + "EndpointID": "e19156f8018f283468227fa97c145f4ea0eaba652fb7e977a0c759b1c3ec168a", + "MacAddress": "02:42:ac:14:80:02", + "IPv4Address": "172.20.0.2/16", + "IPv6Address": "" + } + }, + "Options": {}, + "Labels": {} + } +].to_json } + + + describe '#create' do let(:params) { { image: 'jimi/hendrix:electric-ladyland', @@ -251,4 +394,139 @@ describe VagrantPlugins::DockerProvider::Driver do expect(subject.docker_bridge_ip).to eq('123.456.789.012') end end + + describe '#docker_connect_network' do + let(:opts) { ["--ip", "172.20.128.2"] } + it 'connects a network to a container' do + expect(subject).to receive(:execute).with("docker", "network", "connect", "vagrant_network", cid, "--ip", "172.20.128.2") + subject.connect_network("vagrant_network", cid, opts) + end + end + + describe '#docker_create_network' do + let(:opts) { ["--subnet", "172.20.0.0/16"] } + it 'creates a network' do + expect(subject).to receive(:execute).with("docker", "network", "create", "vagrant_network", "--subnet", "172.20.0.0/16") + subject.create_network("vagrant_network", opts) + end + end + + describe '#docker_disconnet_network' do + it 'disconnects a network from a container' do + expect(subject).to receive(:execute).with("docker", "network", "disconnect", "vagrant_network", cid, "--force") + subject.disconnect_network("vagrant_network", cid) + end + end + + describe '#docker_inspect_network' do + it 'gets info about a network' do + expect(subject).to receive(:execute).with("docker", "network", "inspect", "vagrant_network") + subject.inspect_network("vagrant_network") + end + end + + describe '#docker_list_network' do + it 'lists docker networks' do + expect(subject).to receive(:execute).with("docker", "network", "ls") + subject.list_network() + end + end + + describe '#docker_rm_network' do + it 'deletes a docker network' do + expect(subject).to receive(:execute).with("docker", "network", "rm", "vagrant_network") + subject.rm_network("vagrant_network") + end + end + + describe '#network_defined?' do + let(:subnet_string) { "172.20.0.0/16" } + let(:network_names) { ["vagrant_network_172.20.0.0/16", "bridge", "null" ] } + + it "returns network name if defined" do + allow(subject).to receive(:list_network_names).and_return(network_names) + allow(subject).to receive(:inspect_network).and_return(JSON.load(docker_network_struct)) + + network_name = subject.network_defined?(subnet_string) + expect(network_name).to eq("vagrant_network_172.20.0.0/16") + end + + it "returns nil name if not defined" do + allow(subject).to receive(:list_network_names).and_return(network_names) + allow(subject).to receive(:inspect_network).and_return(JSON.load(docker_network_struct)) + + network_name = subject.network_defined?("120.20.0.0/24") + expect(network_name).to eq(nil) + end + end + + describe '#network_containing_address' do + let(:address) { "172.20.128.2" } + let(:network_names) { ["vagrant_network_172.20.0.0/16", "bridge", "null" ] } + + it "returns the network name if it contains the requested address" do + allow(subject).to receive(:list_network_names).and_return(network_names) + allow(subject).to receive(:inspect_network).and_return(JSON.load(docker_network_struct)) + + network_name = subject.network_containing_address(address) + expect(network_name).to eq("vagrant_network_172.20.0.0/16") + end + + it "returns nil if no networks contain the requested address" do + allow(subject).to receive(:list_network_names).and_return(network_names) + allow(subject).to receive(:inspect_network).and_return(JSON.load(docker_network_struct)) + + network_name = subject.network_containing_address("127.0.0.1") + expect(network_name).to eq(nil) + end + end + + describe '#existing_named_network?' do + let(:network_names) { ["vagrant_network_172.20.0.0/16", "bridge", "null" ] } + + it "returns true if the network exists" do + allow(subject).to receive(:list_network_names).and_return(network_names) + + expect(subject.existing_named_network?("vagrant_network_172.20.0.0/16")).to be_truthy + end + + it "returns false if the network does not exist" do + allow(subject).to receive(:list_network_names).and_return(network_names) + + expect(subject.existing_named_network?("vagrant_network_17.0.0/16")).to be_falsey + end + end + + describe '#list_network_names' do + let(:unparsed_network_names) { "vagrant_network_172.20.0.0/16\nbridge\nnull" } + let(:network_names) { ["vagrant_network_172.20.0.0/16", "bridge", "null" ] } + + it "lists the network names" do + allow(subject).to receive(:list_network).with("--format={{.Name}}"). + and_return(unparsed_network_names) + + expect(subject.list_network_names).to eq(network_names) + end + end + + describe '#network_used?' do + let(:network_name) { "vagrant_network_172.20.0.0/16" } + it "returns nil if no networks" do + allow(subject).to receive(:inspect_network).with(network_name).and_return(nil) + + expect(subject.network_used?(network_name)).to eq(nil) + end + + it "returns true if network has containers in use" do + allow(subject).to receive(:inspect_network).with(network_name).and_return([JSON.load(docker_network_struct).last]) + + expect(subject.network_used?(network_name)).to be_truthy + end + + it "returns false if network has containers in use" do + allow(subject).to receive(:inspect_network).with("host").and_return([JSON.load(docker_network_struct)[1]]) + + expect(subject.network_used?("host")).to be_falsey + end + end end diff --git a/website/source/docs/docker/basics.html.md b/website/source/docs/docker/basics.html.md index e0930b1c2..a38234c5e 100644 --- a/website/source/docs/docker/basics.html.md +++ b/website/source/docs/docker/basics.html.md @@ -73,8 +73,6 @@ This helps keep your Vagrantfile similar to how it has always looked. The Docker provider does not support specifying options for `owner` or `group` on folders synced with a docker container. -Private and public networks are not currently supported. - ### Volume Consistency Docker's [volume consistency](https://docs.docker.com/v17.09/engine/admin/volumes/bind-mounts/) setting can be specified using the `docker_consistency` option when defining a synced folder. This can diff --git a/website/source/docs/docker/networking.html.md b/website/source/docs/docker/networking.html.md new file mode 100644 index 000000000..29961e6a6 --- /dev/null +++ b/website/source/docs/docker/networking.html.md @@ -0,0 +1,290 @@ +--- +layout: "docs" +page_title: "Networking - Docker Provider" +sidebar_current: "providers-docker-networking" +description: |- + The Vagrant Docker provider supports using the private network using the + `docker network` commands. +--- + +# Networking + +Vagrant uses the `docker network` command under the hood to create and manage +networks for containers. Vagrant will do its best to create and manage networks +for any containers configured inside the Vagrantfile. Each docker network is grouped +by the subnet used for a requested ip address. + +For each newly unique network, Vagrant will run the `docker network create` subcommand +with the provided options from the network config inside your Vagrantfile. If multiple +networks share the same subnet, Vagrant will reuse that existing network for multiple +containers. Once these networks have been created, Vagrant will attach these +networks to the requested containers using the `docker network connect` for each +network. + +Vagrant names the networks inside docker as `vagrant_network` or `vagrant_network_` +where `` is the subnet for the network if defined by the user. An +example of these networks is shown later in this page. If no subnet is requested +for the network, Vagrant will connect the `vagrant_network` to the container. + +When destroying containers through Vagrant, Vagrant will clean up the network if +there are no more containers using the network. + +## Docker Network Options + +Most of the options work similar to other Vagrant providers. Defining either an +ip or using `type: 'dhcp'` will give you a network on your container. + +```ruby +docker.vm.network :private_network, type: "dhcp" +docker.vm.network :private_network, ip: "172.20.128.2" +``` + +If you want to set something specific with a new network you can use scoped options +which align with the command line flags for the [docker network create](https://docs.docker.com/engine/reference/commandline/network_create/) +command. If there are any specific options you want to enable from the `docker network create` +command, you can specify them like this: + +```ruby +docker.vm.network :private_network, type: "dhcp", docker_network__internal: true +``` + +This will enable the `internal` option for the network when created with `docker network create`. + +Where `option` corresponds to the given flag that will be provided to the `docker network create` +command. Similarly, if there is a value you wish to enable when connecting a container +to a given network, you can use the following value in your network config: + +```ruby +docker_connect__option: "value" +``` + +When the docker provider creates a new network a netmask is required. If the netmask +is not provided, Vagrant will default to a `/24` for IPv4 and `/64` for IPv6. To provide +a different mask, set it using the `netmask` option: + +```ruby +docker.vm.network :private_network, ip: "172.20.128.2", netmask: 16 +``` + +For networks which set the type to "dhcp", it is also possible to specify a specific +subnet for the network connection. This allows containers to connect to networks other +than the default `vagrant_network` network. The docker provider supports specifying +the desired subnet in two ways. The first is by using the `ip` and `netmask` options: + +```ruby +docker.vm.network :private_network, type: "dhcp", ip: "172.20.128.0", netmask: 24 +``` + +The second is by using the `subnet` option: + +```ruby +docker.vm.network :private_network, type: "dhcp", subnet: "172.20.128.0/24" +``` + +### Public Networks + +The Vagrant docker provider also supports defining public networks. The easiest way +to define a public network is by setting the `type` option to "dhcp": + +```ruby +docker.vm.network :public_network, type: "dhcp" +``` + +A bridge interface is required when setting up a public network. When no bridge +device name is provided, Vagrant will prompt for the appropriate device to use. This +can also be set using the `bridge` option: + +```ruby +docker.vm.network :public_network, type: "dhcp", bridge: "eth0" +``` + +The `bridge` option also supports a list of interfaces which can be used for +setting up the network. Vagrant will inspect the defined interfaces and use +the first active interface when setting up the network: + +```ruby +docker.vm.network :public_network, type: "dhcp", bridge: ["eth0", "wlan0"] +``` + +The available IP range for the bridge interface must be known when setting up +the docker network. Even though a DHCP service may be available on the public +network, docker will manage IP addresses provided to containers. This means +that the subnet provided when defining the available IP range for the network +should not be included within the subnet managed by the DHCP service. Vagrant +will prompt for the available IP range information, however, it can also be +provided in the Vagrantfile using the `docker_network__ip_range` option: + +```ruby +docker.vm.network :public_network, type: "dhcp", bridge: "eth0", docker_network__ip_range: "192.168.1.252/30" +``` + +Finally, the gateway for the interface is required during setup. The docker +provider will default the gateway address to the first address available for +the subnet of the bridge device. Vagrant will prompt for confirmation to use +the default address. The address can also be manually set in the Vagrantfile +using the `docker_network__gateway` option: + +```ruby +docker.vm.network :public_network, type: "dhcp", bridge: "eth0", docker_network__gateway: "192.168.1.2" +``` + +More examples are shared below which demonstrate creating a few common network +interfaces. + +## Docker Network Example + +The following Vagrantfile will generate these networks for a container: + +1. A IPv4 IP address assigned by DHCP +2. A IPv4 IP address 172.20.128.2 on a network with subnet 172.20.0.0/16 +3. A IPv6 IP address assigned by DHCP on subnet 2a02:6b8:b010:9020:1::/80 + +```ruby +Vagrant.configure("2") do |config| + config.vm.define "docker" do |docker| + docker.vm.network :private_network, type: "dhcp", docker_network__internal: true + docker.vm.network :private_network, + ip: "172.20.128.2", netmask: "16" + docker.vm.network :private_network, type: "dhcp", subnet: "2a02:6b8:b010:9020:1::/80" + docker.vm.provider "docker" do |d| + d.build_dir = "docker_build_dir" + end + end +end +``` + +You can test that your container has the proper configured networks by looking +at the result of running `ip addr`, for example: + +``` +brian@localghost:vagrant-sandbox % docker ps ±[●][master] +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +370f4e5d2217 196a06ef12f5 "tail -f /dev/null" 5 seconds ago Up 3 seconds 80/tcp, 443/tcp vagrant-sandbox_docker-1_1551810440 +brian@localghost:vagrant-sandbox % docker exec 370f4e5d2217 ip addr ±[●][master] +1: lo: mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 + link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 + inet 127.0.0.1/8 scope host lo + valid_lft forever preferred_lft forever + inet6 ::1/128 scope host + valid_lft forever preferred_lft forever +24: eth0@if25: mtu 1500 qdisc noqueue state UP group default + link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0 + inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0 + valid_lft forever preferred_lft forever +27: eth1@if28: mtu 1500 qdisc noqueue state UP group default + link/ether 02:42:ac:13:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0 + inet 172.19.0.2/16 brd 172.19.255.255 scope global eth1 + valid_lft forever preferred_lft forever +30: eth2@if31: mtu 1500 qdisc noqueue state UP group default + link/ether 02:42:ac:14:80:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0 + inet 172.20.128.2/16 brd 172.20.255.255 scope global eth2 + valid_lft forever preferred_lft forever +33: eth3@if34: mtu 1500 qdisc noqueue state UP group default + link/ether 02:42:ac:15:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0 + inet 172.21.0.2/16 brd 172.21.255.255 scope global eth3 + valid_lft forever preferred_lft forever + inet6 2a02:6b8:b010:9020:1::2/80 scope global nodad + valid_lft forever preferred_lft forever + inet6 fe80::42:acff:fe15:2/64 scope link + valid_lft forever preferred_lft forever +``` + +You can also connect your containers to a docker network that was created outside +of Vagrant: + +``` +$ docker network create my-custom-network --subnet=172.20.0.0/16 +``` + +```ruby +Vagrant.configure("2") do |config| + config.vm.define "docker" do |docker| + docker.vm.network :private_network, type: "dhcp" name: "my-custom-network" + docker.vm.provider "docker" do |d| + d.build_dir = "docker_build_dir" + end + end +end +``` + +Vagrant will not delete or modify these outside networks when deleting the container, however. + +## Useful Debugging Tips + +The `docker network` command provides some helpful insights to what might be going +on with the networks Vagrant creates. For example, if you want to know what networks +you currently have running on your machine, you can run the `docker network ls` command: + +``` +brian@localghost:vagrant-sandbox % docker network ls ±[●][master] +NETWORK ID NAME DRIVER SCOPE +a2bfc26bd876 bridge bridge local +2a2845e77550 host host local +f36682aeba68 none null local +00d4986c7dc2 vagrant_network bridge local +d02420ff4c39 vagrant_network_2a02:6b8:b010:9020:1::/80 bridge local +799ae9dbaf98 vagrant_network_172.20.0.0/16 bridge local +``` + +You can also inspect any network for more information: + +``` +brian@localghost:vagrant-sandbox % docker network inspect vagrant_network ±[●][master] +[ + { + "Name": "vagrant_network", + "Id": "00d4986c7dc2ed7bf1961989ae1cfe98504c711f9de2f547e5dfffe2bb819fc2", + "Created": "2019-03-05T10:27:21.558824922-08:00", + "Scope": "local", + "Driver": "bridge", + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": {}, + "Config": [ + { + "Subnet": "172.19.0.0/16", + "Gateway": "172.19.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Containers": { + "370f4e5d2217e698b16376583fbf051dd34018e5fd18958b604017def92fea63": { + "Name": "vagrant-sandbox_docker-1_1551810440", + "EndpointID": "166b7ca8960a9f20a150bb75a68d07e27e674781ed9f916e9aa58c8bc2539a61", + "MacAddress": "02:42:ac:13:00:02", + "IPv4Address": "172.19.0.2/16", + "IPv6Address": "" + } + }, + "Options": {}, + "Labels": {} + } +] +``` + +## Caveats + +For now, Vagrant only looks at the subnet when figuring out if it should create +a new network for a guest container. If you bring up a container with a network, +and then change or add some new options (but leave the subnet the same), it will +not apply those changes or create a new network. + +Because the `--link` flag for the `docker network connect` command is considered +legacy, Vagrant does not support that option when creating containers and connecting +networks. + +## More Information + +For more information on how docker manages its networks, please refer to their +documentation: + +- https://docs.docker.com/network/ +- https://docs.docker.com/engine/reference/commandline/network/ diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 462b5c509..437a6609a 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -166,6 +166,7 @@ >Commands >Boxes >Configuration + >Networking >