vagrant/lib/vagrant/plugin/v2/command.rb

297 lines
11 KiB
Ruby

require 'log4r'
require "vagrant/util/safe_puts"
module Vagrant
module Plugin
module V2
# This is the base class for a CLI command.
class Command
include Util::SafePuts
# This should return a brief (60 characters or less) synopsis of what
# this command does. It will be used in the output of the help.
#
# @return [String]
def self.synopsis
""
end
def initialize(argv, env)
@argv = argv
@env = env
@logger = Log4r::Logger.new("vagrant::command::#{self.class.to_s.downcase}")
end
# This is what is called on the class to actually execute it. Any
# subclasses should implement this method and do any option parsing
# and validation here.
def execute
end
protected
# Parses the options given an OptionParser instance.
#
# This is a convenience method that properly handles duping the
# originally argv array so that it is not destroyed.
#
# This method will also automatically detect "-h" and "--help"
# and print help. And if any invalid options are detected, the help
# will be printed, as well.
#
# If this method returns `nil`, then you should assume that help
# was printed and parsing failed.
def parse_options(opts=nil)
# Creating a shallow copy of the arguments so the OptionParser
# doesn't destroy the originals.
argv = @argv.dup
# Default opts to a blank optionparser if none is given
opts ||= OptionParser.new
# Add the help option, which must be on every command.
opts.on_tail("-h", "--help", "Print this help") do
safe_puts(opts.help)
return nil
end
opts.parse!(argv)
return argv
rescue OptionParser::InvalidOption, OptionParser::MissingArgument
raise Errors::CLIInvalidOptions, help: opts.help.chomp
end
# Yields a VM for each target VM for the command.
#
# This is a convenience method for easily implementing methods that
# take a target VM (in the case of multi-VM) or every VM if no
# specific VM name is specified.
#
# @param [String] name The name of the VM. Nil if every VM.
# @param [Hash] options Additional tweakable settings.
# @option options [Symbol] :provider The provider to back the
# machines with. All machines will be backed with this
# provider. If none is given, a sensible default is chosen.
# @option options [Boolean] :reverse If true, the resulting order
# of machines is reversed.
# @option options [Boolean] :single_target If true, then an
# exception will be raised if more than one target is found.
def with_target_vms(names=nil, options=nil)
@logger.debug("Getting target VMs for command. Arguments:")
@logger.debug(" -- names: #{names.inspect}")
@logger.debug(" -- options: #{options.inspect}")
# Setup the options hash
options ||= {}
# Require that names be an array
names ||= []
names = [names] if !names.is_a?(Array)
# Determine if we require a local Vagrant environment. There are
# two cases that we require a local environment:
#
# * We're asking for ANY/EVERY VM (no names given).
#
# * We're asking for specific VMs, at least once of which
# is NOT in the local machine index.
#
requires_local_env = false
requires_local_env = true if names.empty?
requires_local_env ||= names.any? { |n|
!@env.machine_index.include?(n)
}
raise Errors::NoEnvironmentError if requires_local_env && !@env.root_path
# Cache the active machines outside the loop
active_machines = @env.active_machines
# This is a helper that gets a single machine with the proper
# provider. The "proper provider" in this case depends on what was
# given:
#
# * If a provider was explicitly specified, then use that provider.
# But if an active machine exists with a DIFFERENT provider,
# then throw an error (for now), since we don't yet support
# bringing up machines with different providers.
#
# * If no provider was specified, then use the active machine's
# provider if it exists, otherwise use the default provider.
#
get_machine = lambda do |name|
# Check for an active machine with the same name
provider_to_use = options[:provider]
provider_to_use = provider_to_use.to_sym if provider_to_use
# If we have this machine in our index, load that.
entry = @env.machine_index.get(name.to_s)
if entry
@env.machine_index.release(entry)
# Create an environment for this location and yield the
# machine in that environment. We silence warnings here because
# Vagrantfiles often have constants, so people would otherwise
# constantly (heh) get "already initialized constant" warnings.
begin
env = entry.vagrant_env(
@env.home_path, ui_class: @env.ui_class)
rescue Vagrant::Errors::EnvironmentNonExistentCWD
# This means that this environment working directory
# no longer exists, so delete this entry.
entry = @env.machine_index.get(name.to_s)
@env.machine_index.delete(entry) if entry
raise
end
next env.machine(entry.name.to_sym, entry.provider.to_sym)
end
active_machines.each do |active_name, active_provider|
if name == active_name
# We found an active machine with the same name
if provider_to_use && provider_to_use != active_provider
# We found an active machine with a provider that doesn't
# match the requested provider. Show an error.
raise Errors::ActiveMachineWithDifferentProvider,
name: active_name.to_s,
active_provider: active_provider.to_s,
requested_provider: provider_to_use.to_s
else
# Use this provider and exit out of the loop. One of the
# invariants [for now] is that there shouldn't be machines
# with multiple providers.
@logger.info("Active machine found with name #{active_name}. " +
"Using provider: #{active_provider}")
provider_to_use = active_provider
break
end
end
end
# Use the default provider if nothing else
provider_to_use ||= @env.default_provider(machine: name)
# Get the right machine with the right provider
@env.machine(name, provider_to_use)
end
# First determine the proper array of VMs.
machines = []
if names.length > 0
names.each do |name|
if pattern = name[/^\/(.+?)\/$/, 1]
@logger.debug("Finding machines that match regex: #{pattern}")
# This is a regular expression name, so we convert to a regular
# expression and allow that sort of matching.
regex = Regexp.new(pattern)
@env.machine_names.each do |machine_name|
if machine_name =~ regex
machines << get_machine.call(machine_name)
end
end
raise Errors::VMNoMatchError if machines.empty?
else
# String name, just look for a specific VM
@logger.debug("Finding machine that match name: #{name}")
machines << get_machine.call(name.to_sym)
raise Errors::VMNotFoundError, name: name if !machines[0]
end
end
else
# No name was given, so we return every VM in the order
# configured.
@logger.debug("Loading all machines...")
machines = @env.machine_names.map do |machine_name|
get_machine.call(machine_name)
end
end
# Make sure we're only working with one VM if single target
if options[:single_target] && machines.length != 1
@logger.debug("Using primary machine since single target")
primary_name = @env.primary_machine_name
raise Errors::MultiVMTargetRequired if !primary_name
machines = [get_machine.call(primary_name)]
end
# If we asked for reversed ordering, then reverse it
machines.reverse! if options[:reverse]
# Go through each VM and yield it!
color_order = [:default]
color_index = 0
machines.each do |machine|
# Set the machine color
machine.ui.opts[:color] = color_order[color_index % color_order.length]
color_index += 1
@logger.info("With machine: #{machine.name} (#{machine.provider.inspect})")
yield machine
# Call the state method so that we update our index state. Don't
# worry about exceptions here, since we just care about updating
# the cache.
begin
# Called for side effects
machine.state
rescue Errors::VagrantError
end
end
end
# This method will split the argv given into three parts: the
# flags to this command, the subcommand, and the flags to the
# subcommand. For example:
#
# -v status -h -v
#
# The above would yield 3 parts:
#
# ["-v"]
# "status"
# ["-h", "-v"]
#
# These parts are useful because the first is a list of arguments
# given to the current command, the second is a subcommand, and the
# third are the commands given to the subcommand.
#
# @return [Array] The three parts.
def split_main_and_subcommand(argv)
# Initialize return variables
main_args = nil
sub_command = nil
sub_args = []
# We split the arguments into two: One set containing any
# flags before a word, and then the rest. The rest are what
# get actually sent on to the subcommand.
argv.each_index do |i|
if !argv[i].start_with?("-")
# We found the beginning of the sub command. Split the
# args up.
main_args = argv[0, i]
sub_command = argv[i]
sub_args = argv[i + 1, argv.length - i + 1]
# Break so we don't find the next non flag and shift our
# main args.
break
end
end
# Handle the case that argv was empty or didn't contain any subcommand
main_args = argv.dup if main_args.nil?
return [main_args, sub_command, sub_args]
end
end
end
end
end