diff --git a/plugins/commands/port/command.rb b/plugins/commands/port/command.rb new file mode 100644 index 000000000..af35def4b --- /dev/null +++ b/plugins/commands/port/command.rb @@ -0,0 +1,90 @@ +require "vagrant/util/presence" + +require "optparse" + +module VagrantPlugins + module CommandPort + class Command < Vagrant.plugin("2", :command) + include Vagrant::Util::Presence + + def self.synopsis + "displays information about guest port mappings" + end + + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant port [options] [name]" + o.separator "" + o.separator "Options:" + o.separator "" + + o.on("--guest PORT", "Output the host port that maps to the given guest port") do |port| + options[:guest] = port + end + + o.on("--machine-readable", "Display machine-readable output") + end + + # Parse the options + argv = parse_options(opts) + return if !argv + + with_target_vms(argv, single_target: true) do |vm| + vm.action_raw(:config_validate, + Vagrant::Action::Builtin::ConfigValidate) + + if !vm.provider.capability?(:forwarded_ports) + @env.ui.error(I18n.t("port_command.missing_capability", + provider: vm.provider_name, + )) + return 1 + end + + ports = vm.provider.capability(:forwarded_ports) + + if !present?(ports) + @env.ui.info(I18n.t("port_command.empty_ports")) + return 0 + end + + if present?(options[:guest]) + return print_single(vm, ports, options[:guest]) + else + return print_all(vm, ports) + end + end + end + + private + + # Print all the guest <=> host port mappings. + # @return [0] the exit code + def print_all(vm, ports) + @env.ui.info(I18n.t("port_command.details")) + @env.ui.info("") + ports.each do |host, guest| + @env.ui.info("#{guest.to_s.rjust(6)} (guest) => #{host} (host)") + @env.ui.machine("forwarded_port", guest, host, target: vm.name.to_s) + end + return 0 + end + + # Print the host mapping that matches the given guest target. + # @return [0,1] the exit code + def print_single(vm, ports, target) + map = ports.find { |_, guest| "#{guest}" == "#{target}" } + if !present?(map) + @env.ui.error(I18n.t("port_command.no_matching_port", + port: target, + )) + return 1 + end + + @env.ui.info("#{map[0]}") + return 0 + end + end + end +end diff --git a/plugins/commands/port/locales/en.yml b/plugins/commands/port/locales/en.yml new file mode 100644 index 000000000..ba1389db7 --- /dev/null +++ b/plugins/commands/port/locales/en.yml @@ -0,0 +1,20 @@ +en: + port_command: + details: |- + The forwarded ports for the machine are listed below. Please note that + these values may differ from values configured in the Vagrantfile if the + provider supports automatic port collision detection and resolution. + empty_ports: |- + The provider reported there are no forwarded ports for this virtual + machine. This can be caused if there are no ports specified in the + Vagrantfile or if the virtual machine is not currently running. Please + check that the virtual machine is running and try again. + missing_capability: |- + The %{provider} provider does not support listing forwarded ports. This is + most likely a limitation of the provider and not a bug in Vagrant. If you + believe this is a bug in Vagrant, please search existing issues before + opening a new one. + no_matching_port: |- + The guest is not currently mapping port %{port} to the host machine. Is + the port configured in the Vagrantfile? You may need to run `vagrant reload` + if changes were made to the port configuration in the Vagrantfile. diff --git a/plugins/commands/port/plugin.rb b/plugins/commands/port/plugin.rb new file mode 100644 index 000000000..7437f0436 --- /dev/null +++ b/plugins/commands/port/plugin.rb @@ -0,0 +1,27 @@ +require "vagrant" + +module VagrantPlugins + module CommandPort + class Plugin < Vagrant.plugin("2") + name "port command" + description <<-DESC + The `port` command displays guest port mappings. + DESC + + command("port") do + require_relative "command" + self.init! + Command + end + + protected + + def self.init! + return if defined?(@_init) + I18n.load_path << File.expand_path("../locales/en.yml", __FILE__) + I18n.reload! + @_init = true + end + end + end +end diff --git a/plugins/providers/virtualbox/cap.rb b/plugins/providers/virtualbox/cap.rb index 77f8ee1ad..1c0512407 100644 --- a/plugins/providers/virtualbox/cap.rb +++ b/plugins/providers/virtualbox/cap.rb @@ -9,6 +9,8 @@ module VagrantPlugins # # @return [Hash] Host => Guest port mappings. def self.forwarded_ports(machine) + return nil if machine.state.id != :running + {}.tap do |result| machine.provider.driver.read_forwarded_ports.each do |_, _, h, g| result[h] = g diff --git a/test/unit/plugins/commands/port/command_test.rb b/test/unit/plugins/commands/port/command_test.rb new file mode 100644 index 000000000..23abf5d7b --- /dev/null +++ b/test/unit/plugins/commands/port/command_test.rb @@ -0,0 +1,137 @@ +require File.expand_path("../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/port/command") + +describe VagrantPlugins::CommandPort::Command do + include_context "unit" + include_context "command plugin helpers" + + let(:iso_env) { isolated_environment } + let(:env) do + iso_env.vagrantfile(<<-VF) + Vagrant.configure("2") do |config| + config.vm.box = "hashicorp/precise64" + end + VF + iso_env.create_vagrant_env + end + + let(:state) { double(:state, id: :running) } + + let(:machine) { env.machine(env.machine_names[0], :dummy) } + + before(:all) do + I18n.load_path << Vagrant.source_root.join("plugins/commands/port/locales/en.yml") + I18n.reload! + end + + subject { described_class.new([], env) } + + before do + allow(machine).to receive(:state).and_return(state) + allow(subject).to receive(:with_target_vms) { |&block| block.call(machine) } + end + + describe "#execute" do + it "validates the configuration" do + iso_env.vagrantfile <<-EOH + Vagrant.configure("2") do |config| + config.vm.box = "hashicorp/precise64" + + config.push.define "noop" do |push| + push.bad = "ham" + end + end + EOH + + subject = described_class.new([], iso_env.create_vagrant_env) + + expect { subject.execute }.to raise_error(Vagrant::Errors::ConfigInvalid) { |err| + expect(err.message).to include("The following settings shouldn't exist: bad") + } + end + + it "ensures the vm is running" do + allow(state).to receive(:id).and_return(:stopped) + expect(env.ui).to receive(:error).with { |message, _| + expect(message).to include("does not support listing forwarded ports") + } + + expect(subject.execute).to eq(1) + end + + it "shows a friendly error when the capability is not supported" do + allow(machine.provider).to receive(:capability?).and_return(false) + expect(env.ui).to receive(:error).with { |message, _| + expect(message).to include("does not support listing forwarded ports") + } + + expect(subject.execute).to eq(1) + end + + it "returns a friendly message when there are no forwarded ports" do + allow(machine.provider).to receive(:capability?).and_return(true) + allow(machine.provider).to receive(:capability).with(:forwarded_ports) + .and_return([]) + + expect(env.ui).to receive(:info).with { |message, _| + expect(message).to include("there are no forwarded ports") + } + + expect(subject.execute).to eq(0) + end + + it "returns the list of ports" do + allow(machine.provider).to receive(:capability?).and_return(true) + allow(machine.provider).to receive(:capability).with(:forwarded_ports) + .and_return([[2222,22], [1111,11]]) + + output = "" + allow(env.ui).to receive(:info) do |data| + output << data + end + + expect(subject.execute).to eq(0) + + expect(output).to include("forwarded ports for the machine") + expect(output).to include("22 (guest) => 2222 (host)") + expect(output).to include("11 (guest) => 1111 (host)") + end + + it "prints the matching host port when --guest is given" do + argv = ["--guest", "22"] + subject = described_class.new(argv, env) + + allow(machine.provider).to receive(:capability?).and_return(true) + allow(machine.provider).to receive(:capability).with(:forwarded_ports) + .and_return([[2222,22]]) + + output = "" + allow(env.ui).to receive(:info) do |data| + output << data + end + + expect(subject.execute).to eq(0) + + expect(output).to eq("2222") + end + + it "returns an error with no port is mapped to the --guest option" do + argv = ["--guest", "80"] + subject = described_class.new(argv, env) + + allow(machine.provider).to receive(:capability?).and_return(true) + allow(machine.provider).to receive(:capability).with(:forwarded_ports) + .and_return([[2222,22]]) + + output = "" + allow(env.ui).to receive(:error) do |data| + output << data + end + + expect(subject.execute).to_not eq(0) + + expect(output).to include("not currently mapping port 80") + end + end +end diff --git a/test/unit/plugins/providers/virtualbox/cap_test.rb b/test/unit/plugins/providers/virtualbox/cap_test.rb index baa7f610f..52eb9bc43 100644 --- a/test/unit/plugins/providers/virtualbox/cap_test.rb +++ b/test/unit/plugins/providers/virtualbox/cap_test.rb @@ -15,14 +15,16 @@ describe VagrantPlugins::ProviderVirtualBox::Cap do let(:machine) do iso_env.machine(iso_env.machine_names[0], :dummy).tap do |m| m.provider.stub(driver: driver) + m.stub(state: state) end end let(:driver) { double("driver") } + let(:state) { double("state", id: :running) } describe "#forwarded_ports" do it "returns all the forwarded ports" do - expect(driver).to receive(:read_forwarded_ports).and_return([ + allow(driver).to receive(:read_forwarded_ports).and_return([ [nil, nil, 123, 456], [nil, nil, 245, 245], ]) @@ -32,5 +34,10 @@ describe VagrantPlugins::ProviderVirtualBox::Cap do 245 => 245, }) end + + it "returns nil when the machine is not running" do + allow(machine).to receive(:state).and_return(double(:state, id: :stopped)) + expect(described_class.forwarded_ports(machine)).to be(nil) + end end end diff --git a/website/docs/source/layouts/layout.erb b/website/docs/source/layouts/layout.erb index e9831b329..db40d692b 100644 --- a/website/docs/source/layouts/layout.erb +++ b/website/docs/source/layouts/layout.erb @@ -104,6 +104,7 @@ >login >package >plugin + >port >powershell >provision >rdp diff --git a/website/docs/source/v2/cli/port.html.md b/website/docs/source/v2/cli/port.html.md new file mode 100644 index 000000000..97f815de8 --- /dev/null +++ b/website/docs/source/v2/cli/port.html.md @@ -0,0 +1,35 @@ +--- +page_title: "vagrant port - Command-Line Interface" +sidebar_current: "cli-port" +--- + +# Port + +**Command: `vagrant port`** + +The port command displays the full list of guest ports mapped to the host +machine ports: + +``` +$ vagrant port + 22 (guest) => 2222 (host) + 80 (guest) => 8080 (host) +``` + +In a multi-machine Vagrantfile, the name of the machine must be specified: + +``` +$ vagrant port my-machine +``` + +## Options + +* `--guest PORT` - This displays just the host port that corresponds to the + given guest port. If the guest is not forwarding that port, an error is + returned. This is useful for quick scripting, for example: + + $ ssh -p $(vagrant port --guest 22) + +* `--machine-readable` - This tells Vagrant to display machine-readable output + instead of the human-friendly output. More information is available in the + [machine-readable output](/v2/cli/machine-readable.html) documentation.