# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1

require 'fileutils'
require 'json'
require 'pathname'
require 'set'
require 'thread'

require 'log4r'

require 'vagrant/util/file_mode'
require 'vagrant/util/platform'
require 'vagrant/util/hash_with_indifferent_access'
require "vagrant/util/silence_warnings"
require "vagrant/vagrantfile"
require "vagrant/version"

module Vagrant
  # A "Vagrant environment" represents a configuration of how Vagrant
  # should behave: data directories, working directory, UI output,
  # etc. In day-to-day usage, every `vagrant` invocation typically
  # leads to a single Vagrant environment.
  class Environment
    # This is the current version that this version of Vagrant is
    # compatible with in the home directory.
    #
    # @return [String]
    CURRENT_SETUP_VERSION = "1.5"

    DEFAULT_LOCAL_DATA = ".vagrant"

    # The `cwd` that this environment represents
    attr_reader :cwd

    # The persistent data directory where global data can be stored. It
    # is up to the creator of the data in this directory to properly
    # remove it when it is no longer needed.
    #
    # @return [Pathname]
    attr_reader :data_dir

    # The valid name for a Vagrantfile for this environment.
    attr_reader :vagrantfile_name

    # The {UI} object to communicate with the outside world.
    attr_reader :ui

    # This is the UI class to use when creating new UIs.
    attr_reader :ui_class

    # The directory to the "home" folder that Vagrant will use to store
    # global state.
    attr_reader :home_path

    # The directory to the directory where local, environment-specific
    # data is stored.
    attr_reader :local_data_path

    # The directory where temporary files for Vagrant go.
    attr_reader :tmp_path

    # File where command line aliases go.
    attr_reader :aliases_path

    # The directory where boxes are stored.
    attr_reader :boxes_path

    # The path where the plugins are stored (gems)
    attr_reader :gems_path

    # The path to the default private keys directory
    attr_reader :default_private_keys_directory

    # The paths for each of the default private keys
    attr_reader :default_private_key_paths

    # Initializes a new environment with the given options. The options
    # is a hash where the main available key is `cwd`, which defines where
    # the environment represents. There are other options available but
    # they shouldn't be used in general. If `cwd` is nil, then it defaults
    # to the `Dir.pwd` (which is the cwd of the executing process).
    def initialize(opts=nil)
      opts = {
        cwd:              nil,
        home_path:        nil,
        local_data_path:  nil,
        ui_class:         nil,
        ui_opts:          nil,
        vagrantfile_name: nil,
      }.merge(opts || {})

      # Set the default working directory to look for the vagrantfile
      opts[:cwd] ||= ENV["VAGRANT_CWD"] if ENV.key?("VAGRANT_CWD")
      opts[:cwd] ||= Dir.pwd
      opts[:cwd] = Pathname.new(opts[:cwd])
      if !opts[:cwd].directory?
        raise Errors::EnvironmentNonExistentCWD, cwd: opts[:cwd].to_s
      end
      opts[:cwd] = opts[:cwd].expand_path

      # Set the default ui class
      opts[:ui_class] ||= UI::Silent

      # Set the Vagrantfile name up. We append "Vagrantfile" and "vagrantfile" so that
      # those continue to work as well, but anything custom will take precedence.
      opts[:vagrantfile_name] ||= ENV["VAGRANT_VAGRANTFILE"] if \
        ENV.key?("VAGRANT_VAGRANTFILE")
      opts[:vagrantfile_name] = [opts[:vagrantfile_name]] if \
        opts[:vagrantfile_name] && !opts[:vagrantfile_name].is_a?(Array)

      # Set instance variables for all the configuration parameters.
      @cwd              = opts[:cwd]
      @home_path        = opts[:home_path]
      @vagrantfile_name = opts[:vagrantfile_name]
      @ui               = opts.fetch(:ui, opts[:ui_class].new)
      @ui_class         = opts[:ui_class]

      if @ui.nil?
        if opts[:ui_opts].nil?
          @ui = opts[:ui_class].new
        else
          @ui = opts[:ui_class].new(*opts[:ui_opts])
        end
      end

      # This is the batch lock, that enforces that only one {BatchAction}
      # runs at a time from {#batch}.
      @batch_lock = Mutex.new

      @locks = {}

      @logger = Log4r::Logger.new("vagrant::environment")
      @logger.info("Environment initialized (#{self})")
      @logger.info("  - cwd: #{cwd}")

      # Setup the home directory
      @home_path  ||= Vagrant.user_data_path
      @home_path  = Util::Platform.fs_real_path(@home_path)
      @boxes_path = @home_path.join("boxes")
      @data_dir   = @home_path.join("data")
      @gems_path  = Vagrant::Bundler.instance.plugin_gem_path
      @tmp_path   = @home_path.join("tmp")
      @machine_index_dir = @data_dir.join("machine-index")

      @aliases_path = Pathname.new(ENV["VAGRANT_ALIAS_FILE"]).expand_path if ENV.key?("VAGRANT_ALIAS_FILE")
      @aliases_path ||= @home_path.join("aliases")

      # Prepare the directories
      setup_home_path

      # Setup the local data directory. If a configuration path is given,
      # it is expanded relative to the root path. Otherwise, we use the
      # default (which is also expanded relative to the root path).
      if !root_path.nil?
        if !ENV["VAGRANT_DOTFILE_PATH"].to_s.empty? && !opts[:child]
          opts[:local_data_path] ||= Pathname.new(File.expand_path(ENV["VAGRANT_DOTFILE_PATH"], root_path))
        else
          opts[:local_data_path] ||= root_path.join(DEFAULT_LOCAL_DATA)
        end
      end
      if opts[:local_data_path]
        @local_data_path = Pathname.new(File.expand_path(opts[:local_data_path], @cwd))
      end

      @logger.debug("Effective local data path: #{@local_data_path}")

      # If we have a root path, load the ".vagrantplugins" file.
      if root_path
        plugins_file = root_path.join(".vagrantplugins")
        if plugins_file.file?
          @logger.info("Loading plugins file: #{plugins_file}")
          load plugins_file
        end
      end

      setup_local_data_path

      # Setup the default private key
      @default_private_key_path = @home_path.join("insecure_private_key")
      @default_private_keys_directory = @home_path.join("insecure_private_keys")
      if !@default_private_keys_directory.directory?
        @default_private_keys_directory.mkdir
      end
      @default_private_key_paths = []
      copy_insecure_private_keys

      # Initialize localized plugins
      plugins = Vagrant::Plugin::Manager.instance.localize!(self)
      # Load any environment local plugins
      Vagrant::Plugin::Manager.instance.load_plugins(plugins)

      # Initialize globalize plugins
      plugins = Vagrant::Plugin::Manager.instance.globalize!
      # Load any global plugins
      Vagrant::Plugin::Manager.instance.load_plugins(plugins)

      plugins = process_configured_plugins

      # Call the hooks that does not require configurations to be loaded
      # by using a "clean" action runner
      hook(:environment_plugins_loaded, runner: Action::PrimaryRunner.new(env: self))

      # Call the environment load hooks
      hook(:environment_load, runner: Action::PrimaryRunner.new(env: self))
    end

    # The path to the default private key
    # NOTE: deprecated, used default_private_keys_directory instead
    def default_private_key_path
      # TODO(spox): Add deprecation warning
      @default_private_key_path
    end

    # Return a human-friendly string for pretty printed or inspected
    # instances.
    #
    # @return [String]
    def inspect
      "#<#{self.class}: #{@cwd}>".encode('external')
    end

    # Action runner for executing actions in the context of this environment.
    #
    # @return [Action::Runner]
    def action_runner
      @action_runner ||= Action::PrimaryRunner.new do
        {
          action_runner:  action_runner,
          box_collection: boxes,
          hook:           method(:hook),
          host:           host,
          machine_index:  machine_index,
          gems_path:      gems_path,
          home_path:      home_path,
          root_path:      root_path,
          tmp_path:       tmp_path,
          ui:             @ui,
          env:            self
        }
      end
    end

    # Returns a list of machines that this environment is currently
    # managing that physically have been created.
    #
    # An "active" machine is a machine that Vagrant manages that has
    # been created. The machine itself may be in any state such as running,
    # suspended, etc. but if a machine is "active" then it exists.
    #
    # Note that the machines in this array may no longer be present in
    # the Vagrantfile of this environment. In this case the machine can
    # be considered an "orphan." Determining which machines are orphan
    # and which aren't is not currently a supported feature, but will
    # be in a future version.
    #
    # @return [Array<String, Symbol>]
    def active_machines
      # We have no active machines if we have no data path
      return [] if !@local_data_path

      machine_folder = @local_data_path.join("machines")

      # If the machine folder is not a directory then we just return
      # an empty array since no active machines exist.
      return [] if !machine_folder.directory?

      # Traverse the machines folder accumulate a result
      result = []

      machine_folder.children(true).each do |name_folder|
        # If this isn't a directory then it isn't a machine
        next if !name_folder.directory?

        name = name_folder.basename.to_s.to_sym
        name_folder.children(true).each do |provider_folder|
          # If this isn't a directory then it isn't a provider
          next if !provider_folder.directory?

          # If this machine doesn't have an ID, then ignore
          next if !provider_folder.join("id").file?

          provider = provider_folder.basename.to_s.to_sym
          result << [name, provider]
        end
      end

      # Return the results
      result
    end

    # This creates a new batch action, yielding it, and then running it
    # once the block is called.
    #
    # This handles the case where batch actions are disabled by the
    # VAGRANT_NO_PARALLEL environmental variable.
    def batch(parallel=true)
      parallel = false if ENV["VAGRANT_NO_PARALLEL"]

      @batch_lock.synchronize do
        BatchAction.new(parallel).tap do |b|
          # Yield it so that the caller can setup actions
          yield b

          # And run it!
          b.run
        end
      end
    end

    # Makes a call to the CLI with the given arguments as if they
    # came from the real command line (sometimes they do!). An example:
    #
    #     env.cli("package", "--vagrantfile", "Vagrantfile")
    #
    def cli(*args)
      CLI.new(args.flatten, self).execute
    end

    # This returns the provider name for the default provider for this
    # environment.
    #
    # @param check_usable [Boolean] (true) whether to filter for `.usable?` providers
    # @param exclude [Array<Symbol>] ([]) list of provider names to exclude from
    #   consideration
    # @param force_default [Boolean] (true) whether to prefer the value of
    #   VAGRANT_DEFAULT_PROVIDER over other strategies if it is set
    # @param machine [Symbol] (nil) a machine name to scope this lookup
    # @return [Symbol] Name of the default provider.
    def default_provider(**opts)
      opts[:exclude]       = Set.new(opts[:exclude]) if opts[:exclude]
      opts[:force_default] = true if !opts.key?(:force_default)
      opts[:check_usable] = true if !opts.key?(:check_usable)

      # Implement the algorithm from
      # https://www.vagrantup.com/docs/providers/basic_usage.html#default-provider
      # with additional steps 2.5 and 3.5 from
      # https://bugzilla.redhat.com/show_bug.cgi?id=1444492
      # to allow system-configured provider priorities.
      #
      # 1. The --provider flag on a vagrant up is chosen above all else, if it is
      #    present.
      #
      # (Step 1 is done by the caller; this method is only called if --provider
      # wasn't given.)
      #
      # 2. If the VAGRANT_DEFAULT_PROVIDER environmental variable is set, it
      #    takes next priority and will be the provider chosen.

      default = ENV["VAGRANT_DEFAULT_PROVIDER"].to_s
      if default.empty?
        default = nil
      else
        default = default.to_sym
        @logger.debug("Default provider: `#{default}`")
      end

      # If we're forcing the default, just short-circuit and return
      # that (the default behavior)
      if default && opts[:force_default]
        @logger.debug("Using forced default provider: `#{default}`")
        return default
      end

      # Determine the config to use to look for provider definitions. By
      # default it is the global but if we're targeting a specific machine,
      # then look there.
      root_config = vagrantfile.config
      if opts[:machine]
        machine_info = vagrantfile.machine_config(opts[:machine], nil, nil, nil)
        root_config = machine_info[:config]
      end

      # Get the list of providers within our configuration, in order.
      config = root_config.vm.__providers

      # Get the list of usable providers with their internally-declared
      # priorities.
      usable = []
      Vagrant.plugin("2").manager.providers.each do |key, data|
        impl  = data[0]
        popts = data[1]

        # Skip excluded providers
        next if opts[:exclude] && opts[:exclude].include?(key)

        # Skip providers that can't be defaulted, unless they're in our
        # config, in which case someone made our decision for us.
        if !config.include?(key)
          next if popts.key?(:defaultable) && !popts[:defaultable]
        end

        # Skip providers that aren't usable.
        next if opts[:check_usable] && !impl.usable?(false)

        # Each provider sets its own priority, defaulting to 5 so we can trust
        # it's always set.
        usable << [popts[:priority], key]
      end
      @logger.debug("Initial usable provider list: #{usable}")

      # Sort the usable providers by priority. Higher numbers are higher
      # priority, otherwise alpha sort.
      usable = usable.sort {|a, b| a[0] == b[0] ? a[1] <=> b[1] : b[0] <=> a[0]}
                      .map {|prio, key| key}
      @logger.debug("Priority sorted usable provider list: #{usable}")

      # If we're not forcing the default, but it's usable and hasn't been
      # otherwise excluded, return it now.
      if usable.include?(default)
        @logger.debug("Using default provider `#{default}` as it was found in usable list.")
        return default
      end

      # 2.5. Vagrant will go through all of the config.vm.provider calls in the
      #      Vagrantfile and try each in order. It will choose the first
      #      provider that is usable and listed in VAGRANT_PREFERRED_PROVIDERS.

      preferred = ENV.fetch('VAGRANT_PREFERRED_PROVIDERS', '')
                     .split(',')
                     .map {|s| s.strip}
                     .select {|s| !s.empty?}
                     .map {|s| s.to_sym}
      @logger.debug("Preferred provider list: #{preferred}")

      config.each do |key|
        if usable.include?(key) && preferred.include?(key)
          @logger.debug("Using preferred provider `#{key}` detected in configuration and usable.")
          return key
        end
      end

      # 3. Vagrant will go through all of the config.vm.provider calls in the
      #    Vagrantfile and try each in order. It will choose the first provider
      #    that is usable. For example, if you configure Hyper-V, it will never
      #    be chosen on Mac this way. It must be both configured and usable.

      config.each do |key|
        if usable.include?(key)
          @logger.debug("Using provider `#{key}` detected in configuration and usable.")
          return key
        end
      end

      # 3.5. Vagrant will go through VAGRANT_PREFERRED_PROVIDERS and find the
      #      first plugin that reports it is usable.

      preferred.each do |key|
        if usable.include?(key)
          @logger.debug("Using preferred provider `#{key}` found in usable list.")
          return key
        end
      end

      # 4. Vagrant will go through all installed provider plugins (including the
      #    ones that come with Vagrant), and find the first plugin that reports
      #    it is usable. There is a priority system here: systems that are known
      #    better have a higher priority than systems that are worse. For
      #    example, if you have the VMware provider installed, it will always
      #    take priority over VirtualBox.

      if !usable.empty?
        @logger.debug("Using provider `#{usable[0]}` as it is the highest priority in the usable list.")
        return usable[0]
      end

      # 5. If Vagrant still has not found any usable providers, it will error.

      # No providers available is a critical error for Vagrant.
      raise Errors::NoDefaultProvider
    end

    # Returns whether or not we know how to install the provider with
    # the given name.
    #
    # @return [Boolean]
    def can_install_provider?(name)
      host.capability?(provider_install_key(name))
    end

    # Installs the provider with the given name.
    #
    # This will raise an exception if we don't know how to install the
    # provider with the given name. You should guard this call with
    # `can_install_provider?` for added safety.
    #
    # An exception will be raised if there are any failures installing
    # the provider.
    def install_provider(name)
      host.capability(provider_install_key(name))
    end

    # Returns the collection of boxes for the environment.
    #
    # @return [BoxCollection]
    def boxes
      @_boxes ||= BoxCollection.new(
        boxes_path,
        hook: method(:hook),
        temp_dir_root: tmp_path)
    end

    # Returns the {Config::Loader} that can be used to load Vagrantfiles
    # given the settings of this environment.
    #
    # @return [Config::Loader]
    def config_loader
      return @config_loader if @config_loader

      home_vagrantfile = nil
      root_vagrantfile = nil
      home_vagrantfile = find_vagrantfile(home_path) if home_path
      if root_path
        root_vagrantfile = find_vagrantfile(root_path, @vagrantfile_name)
      end

      @config_loader = Config::Loader.new(
        Config::VERSIONS, Config::VERSIONS_ORDER)
      @config_loader.set(:home, home_vagrantfile) if home_vagrantfile
      @config_loader.set(:root, root_vagrantfile) if root_vagrantfile
      @config_loader
    end

    # Loads another environment for the given Vagrantfile, sharing as much
    # useful state from this Environment as possible (such as UI and paths).
    # Any initialization options can be overidden using the opts hash.
    #
    # @param [String] vagrantfile Path to a Vagrantfile
    # @return [Environment]
    def environment(vagrantfile, **opts)
      path = File.expand_path(vagrantfile, root_path)
      file = File.basename(path)
      path = File.dirname(path)

      Util::SilenceWarnings.silence! do
        Environment.new({
          child:     true,
          cwd:       path,
          home_path: home_path,
          ui_class:  ui_class,
          vagrantfile_name: file,
        }.merge(opts))
      end
    end

    # This defines a hook point where plugin action hooks that are registered
    # against the given name will be run in the context of this environment.
    #
    # @param [Symbol] name Name of the hook.
    # @param [Action::Runner] action_runner A custom action runner for running hooks.
    def hook(name, opts=nil)
      @logger.info("Running hook: #{name}")

      opts ||= {}
      opts[:callable] ||= Action::Builder.new
      opts[:runner] ||= action_runner
      opts[:action_name] = name
      opts[:env] = self
      opts.delete(:runner).run(opts.delete(:callable), opts)
    end

    # Returns the host object associated with this environment.
    #
    # @return [Class]
    def host
      return @host if defined?(@host)

      # Determine the host class to use. ":detect" is an old Vagrant config
      # that shouldn't be valid anymore, but we respect it here by assuming
      # its old behavior. No need to deprecate this because I thin it is
      # fairly harmless.
      host_klass = vagrantfile.config.vagrant.host
      host_klass = nil if host_klass == :detect

      begin
        @host = Host.new(
          host_klass,
          Vagrant.plugin("2").manager.hosts,
          Vagrant.plugin("2").manager.host_capabilities,
          self)
      rescue Errors::CapabilityHostNotDetected
        # If the auto-detect failed, then we create a brand new host
        # with no capabilities and use that. This should almost never happen
        # since Vagrant works on most host OS's now, so this is a "slow path"
        klass = Class.new(Vagrant.plugin("2", :host)) do
          def detect?(env); true; end
        end

        hosts     = { generic: [klass, nil] }
        host_caps = {}

        @host = Host.new(:generic, hosts, host_caps, self)
      rescue Errors::CapabilityHostExplicitNotDetected => e
        raise Errors::HostExplicitNotDetected, e.extra_data
      end
    end

    # This acquires a process-level lock with the given name.
    #
    # The lock file is held within the data directory of this environment,
    # so make sure that all environments that are locking are sharing
    # the same data directory.
    #
    # This will raise Errors::EnvironmentLockedError if the lock can't
    # be obtained.
    #
    # @param [String] name Name of the lock, since multiple locks can
    #   be held at one time.
    def lock(name="global", **opts)
      f = nil

      # If we don't have a block, then locking is useless, so ignore it
      return if !block_given?

      # This allows multiple locks in the same process to be nested
      return yield if @locks[name] || opts[:noop]

      # The path to this lock
      lock_path = data_dir.join("lock.#{name}.lock")

      @logger.debug("Attempting to acquire process-lock: #{name}")
      lock("dotlock", noop: name == "dotlock", retry: true) do
        f = File.open(lock_path, "w+")
      end

      # The file locking fails only if it returns "false." If it
      # succeeds it returns a 0, so we must explicitly check for
      # the proper error case.
      while f.flock(File::LOCK_EX | File::LOCK_NB) === false
        @logger.warn("Process-lock in use: #{name}")

        if !opts[:retry]
          raise Errors::EnvironmentLockedError,
            name: name
        end

        sleep 0.2
      end

      @logger.info("Acquired process lock: #{name}")

      result = nil
      begin
        # Mark that we have a lock
        @locks[name] = true

        result = yield
      ensure
        # We need to make sure that no matter what this is always
        # reset to false so we don't think we have a lock when we
        # actually don't.
        @locks.delete(name)
        @logger.info("Released process lock: #{name}")
      end

      # Clean up the lock file, this requires another lock
      if name != "dotlock"
        lock("dotlock", retry: true) do
          f.close
          begin
            File.delete(lock_path)
          rescue
            @logger.error(
              "Failed to delete lock file #{lock_path} - some other thread " +
              "might be trying to acquire it. ignoring this error")
          end
        end
      end

      # Return the result
      return result
    ensure
      begin
        f.close if f
      rescue IOError
      end
    end

    # This executes the push with the given name, raising any exceptions that
    # occur.
    #
    # @param name [String] Push plugin name
    # @param manager [Vagrant::Plugin::Manager] Plugin Manager to use,
    # defaults to the primary one registered but parameterized so it can be
    # overridden in server mode
    #
    # @see VagrantPlugins::CommandServe::Service::PushService Server mode behavior
    #
    # Precondition: the push is not nil and exists.
    def push(name, manager: Vagrant.plugin("2").manager)
      @logger.info("Getting push: #{name}")

      name = name.to_sym

      pushes = self.vagrantfile.config.push.__compiled_pushes
      if !pushes.key?(name)
        raise Vagrant::Errors::PushStrategyNotDefined,
          name: name,
          pushes: pushes.keys
      end

      strategy, config = pushes[name]
      push_registry = manager.pushes
      klass, _ = push_registry.get(strategy)
      if klass.nil?
        raise Vagrant::Errors::PushStrategyNotLoaded,
          name: strategy,
          pushes: push_registry.keys
      end

      klass.new(self, config).push
    end

    # The list of pushes defined in this Vagrantfile.
    #
    # @return [Array<Symbol>]
    def pushes
      self.vagrantfile.config.push.__compiled_pushes.keys
    end

    # This returns a machine with the proper provider for this environment.
    # The machine named by `name` must be in this environment.
    #
    # @param [Symbol] name Name of the machine (as configured in the
    #   Vagrantfile).
    # @param [Symbol] provider The provider that this machine should be
    #   backed by.
    # @param [Boolean] refresh If true, then if there is a cached version
    #   it is reloaded.
    # @return [Machine]
    def machine(name, provider, refresh=false)
      @logger.info("Getting machine: #{name} (#{provider})")

      # Compose the cache key of the name and provider, and return from
      # the cache if we have that.
      cache_key = [name, provider]
      @machines ||= {}
      if refresh
        @logger.info("Refreshing machine (busting cache): #{name} (#{provider})")
        @machines.delete(cache_key)
      end

      if @machines.key?(cache_key)
        @logger.info("Returning cached machine: #{name} (#{provider})")
        return @machines[cache_key]
      end

      @logger.info("Uncached load of machine.")

      # Determine the machine data directory and pass it to the machine.
      machine_data_path = @local_data_path.join(
        "machines/#{name}/#{provider}")

      # Create the machine and cache it for future calls. This will also
      # return the machine from this method.
      @machines[cache_key] = vagrantfile.machine(
        name, provider, boxes, machine_data_path, self)
    end

    # The {MachineIndex} to store information about the machines.
    #
    # @return [MachineIndex]
    def machine_index
      @machine_index ||= MachineIndex.new(@machine_index_dir)
    end

    # This returns a list of the configured machines for this environment.
    # Each of the names returned by this method is valid to be used with
    # the {#machine} method.
    #
    # @return [Array<Symbol>] Configured machine names.
    def machine_names
      vagrantfile.machine_names
    end

    # This returns the name of the machine that is the "primary." In the
    # case of  a single-machine environment, this is just the single machine
    # name. In the case of a multi-machine environment, then this can
    # potentially be nil if no primary machine is specified.
    #
    # @return [Symbol]
    def primary_machine_name
      vagrantfile.primary_machine_name
    end

    # The root path is the path where the top-most (loaded last)
    # Vagrantfile resides. It can be considered the project root for
    # this environment.
    #
    # @return [String]
    def root_path
      return @root_path if defined?(@root_path)

      root_finder = lambda do |path|
        # Note: To remain compatible with Ruby 1.8, we have to use
        # a `find` here instead of an `each`.
        vf = find_vagrantfile(path, @vagrantfile_name)
        return path if vf
        return nil if path.root? || !File.exist?(path)
        root_finder.call(path.parent)
      end

      @root_path = root_finder.call(cwd)
    end

    # Unload the environment, running completion hooks. The environment
    # should not be used after this (but CAN be, technically). It is
    # recommended to always immediately set the variable to `nil` after
    # running this so you can't accidentally run any more methods. Example:
    #
    #     env.unload
    #     env = nil
    #
    def unload
      hook(:environment_unload)
    end

    # Represents the default Vagrantfile, or the Vagrantfile that is
    # in the working directory or a parent of the working directory
    # of this environment.
    #
    # The existence of this function is primarily a convenience. There
    # is nothing stopping you from instantiating your own {Vagrantfile}
    # and loading machines in any way you see fit. Typical behavior of
    # Vagrant, however, loads this Vagrantfile.
    #
    # This Vagrantfile is comprised of two major sources: the Vagrantfile
    # in the user's home directory as well as the "root" Vagrantfile or
    # the Vagrantfile in the working directory (or parent).
    #
    # @return [Vagrantfile]
    def vagrantfile
      @vagrantfile ||= Vagrantfile.new(config_loader, [:home, :root])
    end

    #---------------------------------------------------------------
    # Load Methods
    #---------------------------------------------------------------

    # This sets the `@home_path` variable properly.
    #
    # @return [Pathname]
    def setup_home_path
      @logger.info("Home path: #{@home_path}")

      # Setup the list of child directories that need to be created if they
      # don't already exist.
      dirs    = [
        @home_path,
        @home_path.join("rgloader"),
        @boxes_path,
        @data_dir,
        @gems_path,
        @tmp_path,
        @machine_index_dir,
      ]

      # Go through each required directory, creating it if it doesn't exist
      dirs.each do |dir|
        next if File.directory?(dir)

        begin
          @logger.info("Creating: #{dir}")
          FileUtils.mkdir_p(dir)
        rescue Errno::EACCES, Errno::EROFS
          raise Errors::HomeDirectoryNotAccessible, home_path: @home_path.to_s
        end
      end

      # Attempt to write into the home directory to verify we can
      begin
        # Append a random suffix to avoid race conditions if Vagrant
        # is running in parallel with other Vagrant processes.
        suffix = (0...32).map { (65 + rand(26)).chr }.join
        path   = @home_path.join("perm_test_#{suffix}")
        path.open("w") do |f|
          f.write("hello")
        end
        path.unlink
      rescue Errno::EACCES
        raise Errors::HomeDirectoryNotAccessible, home_path: @home_path.to_s
      end

      # Create the version file that we use to track the structure of
      # the home directory. If we have an old version, we need to explicitly
      # upgrade it. Otherwise, we just mark that its the current version.
      version_file = @home_path.join("setup_version")
      if version_file.file?
        version = version_file.read.chomp
        if version > CURRENT_SETUP_VERSION
          raise Errors::HomeDirectoryLaterVersion
        end

        case version
        when CURRENT_SETUP_VERSION
          # We're already good, at the latest version.
        when "1.1"
          # We need to update our directory structure
          upgrade_home_path_v1_1

          # Delete the version file so we put our latest version in
          version_file.delete
        else
          raise Errors::HomeDirectoryUnknownVersion,
            path: @home_path.to_s,
            version: version
        end
      end

      if !version_file.file?
        @logger.debug(
          "Creating home directory version file: #{CURRENT_SETUP_VERSION}")
        version_file.open("w") do |f|
          f.write(CURRENT_SETUP_VERSION)
        end
      end

      # Create the rgloader/loader file so we can use encoded files.
      loader_file = @home_path.join("rgloader", "loader.rb")
      if !loader_file.file?
        source_loader = Vagrant.source_root.join("templates/rgloader.rb")
        FileUtils.cp(source_loader.to_s, loader_file.to_s)
      end
    end

    # This creates the local data directory and show an error if it
    # couldn't properly be created.
    def setup_local_data_path(force=false)
      if @local_data_path.nil?
        @logger.warn("No local data path is set. Local data cannot be stored.")
        return
      end

      @logger.info("Local data path: #{@local_data_path}")

      # If the local data path is a file, then we are probably seeing an
      # old (V1) "dotfile." In this case, we upgrade it. The upgrade process
      # will remove the old data file if it is successful.
      if @local_data_path.file?
        upgrade_v1_dotfile(@local_data_path)
      end

      # If we don't have a root path, we don't setup anything
      return if !force && root_path.nil?

      begin
        @logger.debug("Creating: #{@local_data_path}")
        FileUtils.mkdir_p(@local_data_path)
        # Create the rgloader/loader file so we can use encoded files.
        loader_file = @local_data_path.join("rgloader", "loader.rb")
        if !loader_file.file?
          source_loader = Vagrant.source_root.join("templates/rgloader.rb")
          FileUtils.mkdir_p(@local_data_path.join("rgloader").to_s)
          FileUtils.cp(source_loader.to_s, loader_file.to_s)
        end
      rescue Errno::EACCES
        raise Errors::LocalDataDirectoryNotAccessible,
          local_data_path: @local_data_path.to_s
      end
    end

    protected

    # Attempt to guess the configured provider in use. Will fallback
    # to the default provider if an explicit provider name is not
    # provided. This can be pretty error prone, but is used during
    # initial environment setup to allow loading plugins so it doesn't
    # need to be perfect
    #
    # @return [String]
    def guess_provider
      gp = nil
      ARGV.each_with_index do |val, idx|
        if val.start_with?("--provider=")
          gp = val.split("=", 2).last
          break
        elsif val == "--provider"
          gp = ARGV[idx+1]
          break
        end
      end
      return gp.to_sym if gp
      begin
        default_provider
      rescue Errors::NoDefaultProvider
        # if a provider cannot be determined just return nil
        nil
      end
    end

    # Load any configuration provided by guests defined within
    # the Vagrantfile to pull plugin information they may have
    # defined.
    def find_configured_plugins
      plugins = []
      provider = guess_provider
      vagrantfile.machine_names.each do |mname|
        ldp = @local_data_path.join("machines/#{mname}/#{provider}") if @local_data_path
        plugins << vagrantfile.machine_config(mname, provider, boxes, ldp, false)[:config]
      end
      result = plugins.reverse.inject(Vagrant::Util::HashWithIndifferentAccess.new) do |memo, val|
        Vagrant::Util::DeepMerge.deep_merge(memo, val.vagrant.plugins)
      end
      Vagrant::Util::DeepMerge.deep_merge(result, vagrantfile.config.vagrant.plugins)
    end

    # Check for any local plugins defined within the Vagrantfile. If
    # found, validate they are available. If they are not available,
    # request to install them, or raise an exception
    #
    # @return [Hash] plugin list for loading
    def process_configured_plugins
      return if !Vagrant.plugins_enabled?
      errors = vagrantfile.config.vagrant.validate(nil)
      if !Array(errors["vagrant"]).empty?
        raise Errors::ConfigInvalid,
          errors: Util::TemplateRenderer.render(
            "config/validation_failed",
            errors: {vagrant: errors["vagrant"]}
          )
      end
      # Check if defined plugins are installed
      installed = Plugin::Manager.instance.installed_plugins
      needs_install = []
      config_plugins = find_configured_plugins
      config_plugins.each do |name, info|
        if !installed[name]
          needs_install << name
        end
      end
      if !needs_install.empty?
        ui.warn(I18n.t("vagrant.plugins.local.uninstalled_plugins",
          plugins: needs_install.sort.join(", ")))
        if !Vagrant.auto_install_local_plugins?
          answer = nil
          until ["y", "n"].include?(answer)
            answer = ui.ask(I18n.t("vagrant.plugins.local.request_plugin_install") + " [N]: ")
            answer = answer.strip.downcase
            answer = "n" if answer.to_s.empty?
          end
          if answer == "n"
            raise Errors::PluginMissingLocalError,
              plugins: needs_install.sort.join(", ")
          end
        end
        needs_install.each do |name|
          pconfig = Util::HashWithIndifferentAccess.new(config_plugins[name])
          ui.info(I18n.t("vagrant.commands.plugin.installing", name: name))

          options = {sources: Vagrant::Bundler::DEFAULT_GEM_SOURCES.dup, env_local: true}
          options[:sources] = pconfig[:sources] if pconfig[:sources]
          options[:require] = pconfig[:entry_point] if pconfig[:entry_point]
          options[:version] = pconfig[:version] if pconfig[:version]

          spec = Plugin::Manager.instance.install_plugin(name, **options)

          ui.info(I18n.t("vagrant.commands.plugin.installed",
            name: spec.name, version: spec.version.to_s))
        end
        ui.info("\n")
        # Force halt after installation and require command to be run again. This
        # will proper load any new locally installed plugins which are now available.
        ui.warn(I18n.t("vagrant.plugins.local.install_rerun_command"))
        exit(-1)
      end
      if Vagrant::Plugin::Manager.instance.local_file
        Vagrant::Plugin::Manager.instance.local_file.installed_plugins
      else
        {}
      end
    end

    # This method copies the private keys into the home directory if they
    # do not already exist. The `default_private_key_path` references the
    # original rsa based private key and is retained for compatibility. The
    # `default_private_keys_directory` contains the list of valid private
    # keys supported by Vagrant.
    #
    # NOTE: The keys are copied because `ssh` requires that the key is chmod
    # 0600, but if Vagrant is installed as a separate user, then the
    # effective uid won't be able to read the key. So the key is copied
    # to the home directory and chmod 0600.
    def copy_insecure_private_keys
      # First setup the deprecated single key path
      if !@default_private_key_path.exist?
        @logger.info("Copying private key to home directory")

        source      = File.expand_path("keys/vagrant", Vagrant.source_root)
        destination = @default_private_key_path

        begin
          FileUtils.cp(source, destination)
        rescue Errno::EACCES
          raise Errors::CopyPrivateKeyFailed,
            source: source,
            destination: destination
        end
      end

      if !Util::Platform.windows?
        # On Windows, permissions don't matter as much, so don't worry
        # about doing chmod.
        if Util::FileMode.from_octal(@default_private_key_path.stat.mode) != "600"
          @logger.info("Changing permissions on private key to 0600")
          @default_private_key_path.chmod(0600)
        end
      end

      # Now setup the key directory
      Dir.glob(File.expand_path("keys/vagrant.key.*", Vagrant.source_root)).each do |source|
        destination = default_private_keys_directory.join(File.basename(source))
        default_private_key_paths << destination
        next if File.exist?(destination)
        begin
          FileUtils.cp(source, destination)
        rescue Errno::EACCES
          raise Errors::CopyPrivateKeyFailed,
            source: source,
            destination: destination
        end
      end

      if !Util::Platform.windows?
        default_private_key_paths.each do |key_path|
          if Util::FileMode.from_octal(key_path.stat.mode) != "600"
            @logger.info("Changing permissions on private key (#{key_path}) to 0600")
            key_path.chmod(0600)
          end
        end
      end
    end

    # Finds the Vagrantfile in the given directory.
    #
    # @param [Pathname] path Path to search in.
    # @return [Pathname]
    def find_vagrantfile(search_path, filenames=nil)
      filenames ||= ["Vagrantfile", "vagrantfile"]
      filenames.each do |vagrantfile|
        current_path = search_path.join(vagrantfile)
        return current_path if current_path.file?
      end

      nil
    end

    # Returns the key used for the host capability for provider installs
    # of the given name.
    def provider_install_key(name)
      "provider_install_#{name}".to_sym
    end

    # This upgrades a home directory that was in the v1.1 format to the
    # v1.5 format. It will raise exceptions if anything fails.
    def upgrade_home_path_v1_1
      if !ENV["VAGRANT_UPGRADE_SILENT_1_5"]
        @ui.ask(I18n.t("vagrant.upgrading_home_path_v1_5"))
      end

      collection = BoxCollection.new(
        @home_path.join("boxes"), temp_dir_root: tmp_path)
      collection.upgrade_v1_1_v1_5
    end

    # This upgrades a Vagrant 1.0.x "dotfile" to the new V2 format.
    #
    # This is a destructive process. Once the upgrade is complete, the
    # old dotfile is removed, and the environment becomes incompatible for
    # Vagrant 1.0 environments.
    #
    # @param [Pathname] path The path to the dotfile
    def upgrade_v1_dotfile(path)
      @logger.info("Upgrading V1 dotfile to V2 directory structure...")

      # First, verify the file isn't empty. If it is an empty file, we
      # just delete it and go on with life.
      contents = path.read.strip
      if contents.strip == ""
        @logger.info("V1 dotfile was empty. Removing and moving on.")
        path.delete
        return
      end

      # Otherwise, verify there is valid JSON in here since a Vagrant
      # environment would always ensure valid JSON. This is a sanity check
      # to make sure we don't nuke a dotfile that is not ours...
      @logger.debug("Attempting to parse JSON of V1 file")
      json_data = nil
      begin
        json_data = JSON.parse(contents)
        @logger.debug("JSON parsed successfully. Things are okay.")
      rescue JSON::ParserError
        # The file could've been tampered with since Vagrant 1.0.x is
        # supposed to ensure that the contents are valid JSON. Show an error.
        raise Errors::DotfileUpgradeJSONError,
          state_file: path.to_s
      end

      # Alright, let's upgrade this guy to the new structure. Start by
      # backing up the old dotfile.
      backup_file = path.dirname.join(".vagrant.v1.#{Time.now.to_i}")
      @logger.info("Renaming old dotfile to: #{backup_file}")
      path.rename(backup_file)

      # Now, we create the actual local data directory. This should succeed
      # this time since we renamed the old conflicting V1.
      setup_local_data_path(true)

      if json_data["active"]
        @logger.debug("Upgrading to V2 style for each active VM")
        json_data["active"].each do |name, id|
          @logger.info("Upgrading dotfile: #{name} (#{id})")

          # Create the machine configuration directory
          directory = @local_data_path.join("machines/#{name}/virtualbox")
          FileUtils.mkdir_p(directory)

          # Write the ID file
          directory.join("id").open("w+") do |f|
            f.write(id)
          end
        end
      end

      # Upgrade complete! Let the user know
      @ui.info(I18n.t("vagrant.general.upgraded_v1_dotfile",
                      backup_path: backup_file.to_s))
    end
  end
end
