class Vagrant::BoxCollection
Represents a collection a boxes found on disk. This provides methods for accessing/finding individual boxes, adding new boxes, or deleting boxes.
Constants
- TEMP_PREFIX
- VAGRANT_COLON
- VAGRANT_SLASH
Attributes
The directory where the boxes in this collection are stored.
A box collection matches a very specific folder structure that Vagrant
expects in order to easily manage and modify boxes. The folder structure is the following:
COLLECTION_ROOT/BOX_NAME/PROVIDER/metadata.json
Where:
* COLLECTION_ROOT - This is the root of the box collection, and is the directory given to the initializer. * BOX_NAME - The name of the box. This is a logical name given by the user of Vagrant. * PROVIDER - The provider that the box was built for (VirtualBox, VMware, etc.). * metadata.json - A simple JSON file that at the bare minimum contains a "provider" key that matches the provider for the box. This metadata JSON, however, can contain anything.
@return [Pathname]
Public Class Methods
Initializes the collection.
@param [Pathname] directory The directory that contains the collection
of boxes.
# File lib/vagrant/box_collection.rb, line 49 def initialize(directory, options=nil) options ||= {} @directory = directory @hook = options[:hook] @lock = Monitor.new @temp_root = options[:temp_dir_root] @logger = Log4r::Logger.new("vagrant::box_collection") end
Public Instance Methods
This adds a new box to the system.
There are some exceptional cases:
-
BoxAlreadyExists - The box you’re attempting to add already exists.
-
BoxProviderDoesntMatch - If the given box provider doesn’t match the actual box provider in the untarred box.
-
BoxUnpackageFailure - An invalid tar file.
Preconditions:
-
File given in ‘path` must exist.
@param [Pathname] path Path to the box file on disk. @param [String] name Logical name for the box. @param [String] version The version of this box. @param [Array<String>] providers The providers that this box can
be a part of. This will be verified with the `metadata.json` and is meant as a basic check. If this isn't given, then whatever provider the box represents will be added.
@param [Boolean] force If true, any existing box with the same name
and provider will be replaced.
# File lib/vagrant/box_collection.rb, line 79 def add(path, name, version, **opts) providers = opts[:providers] providers = Array(providers) if providers provider = nil # A helper to check if a box exists. We store this in a variable # since we call it multiple times. check_box_exists = lambda do |box_formats| box = find(name, box_formats, version) next if !box if !opts[:force] @logger.error( "Box already exists, can't add: #{name} v#{version} #{box_formats.join(", ")}") raise Errors::BoxAlreadyExists, name: name, provider: box_formats.join(", "), version: version end # We're forcing, so just delete the old box @logger.info( "Box already exists, but forcing so removing: " + "#{name} v#{version} #{box_formats.join(", ")}") box.destroy! end with_collection_lock do log_provider = providers ? providers.join(", ") : "any provider" @logger.debug("Adding box: #{name} (#{log_provider}) from #{path}") # Verify the box doesn't exist early if we're given a provider. This # can potentially speed things up considerably since we don't need # to unpack any files. check_box_exists.call(providers) if providers # Create a temporary directory since we're not sure at this point if # the box we're unpackaging already exists (if no provider was given) with_temp_dir do |temp_dir| # Extract the box into a temporary directory. @logger.debug("Unpacking box into temporary directory: #{temp_dir}") result = Util::Subprocess.execute( "bsdtar", "--no-same-owner", "--no-same-permissions", "-v", "-x", "-m", "-s", "|\\\\\|/|", "-C", temp_dir.to_s, "-f", path.to_s) if result.exit_code != 0 raise Errors::BoxUnpackageFailure, output: result.stderr.to_s end # If we get a V1 box, we want to update it in place if v1_box?(temp_dir) @logger.debug("Added box is a V1 box. Upgrading in place.") temp_dir = v1_upgrade(temp_dir) end # We re-wrap ourselves in the safety net in case we upgraded. # If we didn't upgrade, then this is still safe because the # helper will only delete the directory if it exists with_temp_dir(temp_dir) do |final_temp_dir| # Get an instance of the box we just added before it is finalized # in the system so we can inspect and use its metadata. box = Box.new(name, nil, version, final_temp_dir) # Get the provider, since we'll need that to at the least add it # to the system or check that it matches what is given to us. box_provider = box.metadata["provider"] if providers found = providers.find { |p| p.to_sym == box_provider.to_sym } if !found @logger.error("Added box provider doesnt match expected: #{log_provider}") raise Errors::BoxProviderDoesntMatch, expected: log_provider, actual: box_provider end else # Verify the box doesn't already exist check_box_exists.call([box_provider]) end # We weren't given a provider, so store this one. provider = box_provider.to_sym # Create the directory for this box, not including the provider root_box_dir = @directory.join(dir_name(name)) box_dir = root_box_dir.join(version) box_dir.mkpath @logger.debug("Box directory: #{box_dir}") # This is the final directory we'll move it to final_dir = box_dir.join(provider.to_s) if final_dir.exist? @logger.debug("Removing existing provider directory...") final_dir.rmtree end # Move to final destination final_dir.mkpath # Recursively move individual files from the temporary directory # to the final location. We do this instead of moving the entire # directory to avoid issues on Windows. [GH-1424] copy_pairs = [[final_temp_dir, final_dir]] while !copy_pairs.empty? from, to = copy_pairs.shift from.children(true).each do |f| dest = to.join(f.basename) # We don't copy entire directories, so create the # directory and then add to our list to copy. if f.directory? dest.mkpath copy_pairs << [f, dest] next end # Copy the single file @logger.debug("Moving: #{f} => #{dest}") FileUtils.mv(f, dest) end end if opts[:metadata_url] root_box_dir.join("metadata_url").open("w") do |f| f.write(opts[:metadata_url]) end end end end end # Return the box find(name, provider, version) end
This returns an array of all the boxes on the system, given by their name and their provider.
@return [Array] Array of ‘[name, version, provider]` of the boxes
installed on this system.
# File lib/vagrant/box_collection.rb, line 217 def all results = [] with_collection_lock do @logger.debug("Finding all boxes in: #{@directory}") @directory.children(true).each do |child| # Ignore non-directories, since files are not interesting to # us in our folder structure. next if !child.directory? box_name = undir_name(child.basename.to_s) # Otherwise, traverse the subdirectories and see what versions # we have. child.children(true).each do |versiondir| next if !versiondir.directory? next if versiondir.basename.to_s.start_with?(".") version = versiondir.basename.to_s versiondir.children(true).each do |provider| # Ensure version of box is correct before continuing if !Gem::Version.correct?(version) ui = Vagrant::UI::Prefixed.new(Vagrant::UI::Colored.new, "vagrant") ui.warn(I18n.t("vagrant.box_version_malformed", version: version, box_name: box_name)) @logger.debug("Invalid version #{version} for box #{box_name}") next end # Verify this is a potentially valid box. If it looks # correct enough then include it. if provider.directory? && provider.join("metadata.json").file? provider_name = provider.basename.to_s.to_sym @logger.debug("Box: #{box_name} (#{provider_name}, #{version})") results << [box_name, version, provider_name] else @logger.debug("Invalid box #{box_name}, ignoring: #{provider}") end end end end end # Sort the list to group like providers and properly ordered versions results.sort_by! do |box_result| [box_result[0], box_result[2], Gem::Version.new(box_result[1])] end results end
Cleans the directory for a box by removing the folders that are empty.
# File lib/vagrant/box_collection.rb, line 381 def clean(name) return false if exists?(name) path = File.join(directory, dir_name(name)) FileUtils.rm_rf(path) end
Find a box in the collection with the given name and provider.
@param [String] name Name of the box (logical name). @param [Array] providers Providers that the box implements. @param [String] version Version constraints to adhere to. Example:
"~> 1.0" or "= 1.0, ~> 1.1"
@return [Box] The box found, or ‘nil` if not found.
# File lib/vagrant/box_collection.rb, line 274 def find(name, providers, version) providers = Array(providers) # Build up the requirements we have requirements = version.to_s.split(",").map do |v| begin Gem::Requirement.new(v.strip) rescue Gem::Requirement::BadRequirementError raise Errors::BoxVersionInvalid, version: v.strip end end with_collection_lock do box_directory = @directory.join(dir_name(name)) if !box_directory.directory? @logger.info("Box not found: #{name} (#{providers.join(", ")})") return nil end # Keep a mapping of Gem::Version mangled versions => directories. # ie. 0.1.0.pre.alpha.2 => 0.1.0-alpha.2 # This is so we can sort version numbers properly here, but still # refer to the real directory names in path checks below and pass an # unmangled version string to Box.new version_dir_map = {} versions = box_directory.children(true).map do |versiondir| next if !versiondir.directory? next if versiondir.basename.to_s.start_with?(".") version = Gem::Version.new(versiondir.basename.to_s) version_dir_map[version.to_s] = versiondir.basename.to_s version end.compact # Traverse through versions with the latest version first versions.sort.reverse.each do |v| if !requirements.all? { |r| r.satisfied_by?(v) } # Unsatisfied version requirements next end versiondir = box_directory.join(version_dir_map[v.to_s]) providers.each do |provider| provider_dir = versiondir.join(provider.to_s) next if !provider_dir.directory? @logger.info("Box found: #{name} (#{provider})") metadata_url = nil metadata_url_file = box_directory.join("metadata_url") metadata_url = metadata_url_file.read if metadata_url_file.file? if metadata_url && @hook hook_env = @hook.call( :authenticate_box_url, box_urls: [metadata_url]) metadata_url = hook_env[:box_urls].first end return Box.new( name, provider, version_dir_map[v.to_s], provider_dir, metadata_url: metadata_url, hook: @hook ) end end end nil end
This upgrades a v1.1 - v1.4 box directory structure up to a v1.5 directory structure. This will raise exceptions if it fails in any way.
# File lib/vagrant/box_collection.rb, line 347 def upgrade_v1_1_v1_5 with_collection_lock do temp_dir = Pathname.new(Dir.mktmpdir(TEMP_PREFIX, @temp_root)) @directory.children(true).each do |boxdir| # Ignore all non-directories because they can't be boxes next if !boxdir.directory? box_name = boxdir.basename.to_s # If it is a v1 box, then we need to upgrade it first if v1_box?(boxdir) upgrade_dir = v1_upgrade(boxdir) FileUtils.mv(upgrade_dir, boxdir.join("virtualbox")) end # Create the directory for this box new_box_dir = temp_dir.join(dir_name(box_name), "0") new_box_dir.mkpath # Go through each provider and move it boxdir.children(true).each do |providerdir| FileUtils.cp_r(providerdir, new_box_dir.join(providerdir.basename)) end end # Move the folder into place @directory.rmtree FileUtils.mv(temp_dir.to_s, @directory.to_s) end end
Protected Instance Methods
Returns the directory name for the box of the given name.
@param [String] name @return [String]
# File lib/vagrant/box_collection.rb, line 393 def dir_name(name) name = name.dup name.gsub!(":", VAGRANT_COLON) if Util::Platform.windows? name.gsub!("/", VAGRANT_SLASH) name end
Checks if a box with a given name exists.
# File lib/vagrant/box_collection.rb, line 483 def exists?(box_name) all.any? { |box| box.first.eql?(box_name) } end
Returns the directory name for the box cleaned up
# File lib/vagrant/box_collection.rb, line 401 def undir_name(name) name = name.dup name.gsub!(VAGRANT_COLON, ":") name.gsub!(VAGRANT_SLASH, "/") name end
This checks if the given directory represents a V1 box on the system.
@param [Pathname] dir Directory where the box is unpacked. @return [Boolean]
# File lib/vagrant/box_collection.rb, line 413 def v1_box?(dir) # We detect a V1 box given by whether there is a "box.ovf" which # is a heuristic but is pretty accurate. dir.join("box.ovf").file? end
This upgrades the V1 box contained unpacked in the given directory and returns the directory of the upgraded version. This is destructive to the contents of the old directory. That is, the contents of the old V1 box will be destroyed or moved.
Preconditions:
-
‘dir` is a valid V1 box. Verify with {#v1_box?}
@param [Pathname] dir Directory where the V1 box is unpacked. @return [Pathname] Path to the unpackaged V2 box.
# File lib/vagrant/box_collection.rb, line 429 def v1_upgrade(dir) @logger.debug("Upgrading box in directory: #{dir}") temp_dir = Pathname.new(Dir.mktmpdir(TEMP_PREFIX, @temp_root)) @logger.debug("Temporary directory for upgrading: #{temp_dir}") # Move all the things into the temporary directory dir.children(true).each do |child| # Don't move the temp_dir next if child == temp_dir # Move every other directory into the temporary directory @logger.debug("Copying to upgrade directory: #{child}") FileUtils.mv(child, temp_dir.join(child.basename)) end # If there is no metadata.json file, make one, since this is how # we determine if the box is a V2 box. metadata_file = temp_dir.join("metadata.json") if !metadata_file.file? metadata_file.open("w") do |f| f.write(JSON.generate({ provider: "virtualbox" })) end end # Return the temporary directory temp_dir end
This locks the region given by the block with a lock on this collection.
# File lib/vagrant/box_collection.rb, line 462 def with_collection_lock @lock.synchronize do return yield end end
This is a helper that makes sure that our temporary directories are cleaned up no matter what.
@param [String] dir Path to a temporary directory @return [Object] The result of whatever the yield is
# File lib/vagrant/box_collection.rb, line 473 def with_temp_dir(dir=nil) dir ||= Dir.mktmpdir(TEMP_PREFIX, @temp_root) dir = Pathname.new(dir) yield dir ensure FileUtils.rm_rf(dir.to_s) end