Class: Vagrant::BoxCollection

Inherits:
Object
  • Object
show all
Defined in:
lib/vagrant/box_collection.rb

Overview

Represents a collection a boxes found on disk. This provides methods for accessing/finding individual boxes, adding new boxes, or deleting boxes.

Constant Summary collapse

TEMP_PREFIX =
"vagrant-box-add-temp-"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(directory, options = nil) ⇒ BoxCollection

Initializes the collection.

Parameters:

  • directory (Pathname)

    The directory that contains the collection of boxes.



43
44
45
46
47
48
49
50
# File 'lib/vagrant/box_collection.rb', line 43

def initialize(directory, options=nil)
  options ||= {}

  @directory = directory
  @lock      = Mutex.new
  @temp_root = options[:temp_dir_root]
  @logger    = Log4r::Logger.new("vagrant::box_collection")
end

Instance Attribute Details

#directoryPathname (readonly)

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/.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.).
* .json - A simple JSON file that at the bare minimum
  contains a "provider" key that matches the provider for the
  box. This  JSON, however, can contain anything.

Returns:

  • (Pathname)


37
38
39
# File 'lib/vagrant/box_collection.rb', line 37

def directory
  @directory
end

Instance Method Details

#add(path, name, formats = nil, force = false) ⇒ Object

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.

  • BoxUpgradeRequired - You’re attempting to add a box when there is a V1 box with the same name that must first be upgraded.

Preconditions:

  • File given in ‘path` must exist.

Parameters:

  • path (Pathname)

    Path to the box file on disk.

  • name (String)

    Logical name for the box.

  • provider (Symbol)

    The provider that the box should be for. This will be verified with the ‘metadata.json` file in the box and is meant as a basic check. If this isn’t given, then whatever provider the box represents will be added.

  • force (Boolean) (defaults to: false)

    If true, any existing box with the same name and provider will be replaced.



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/vagrant/box_collection.rb', line 73

def add(path, name, formats=nil, force=false)
  formats = [formats] if formats && !formats.is_a?(Array)
  provider = nil

  with_collection_lock do
    # 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)
      next if !box

      if !force
        @logger.error("Box already exists, can't add: #{name} #{box_formats.join(", ")}")
        raise Errors::BoxAlreadyExists, :name => name, :formats => box_formats.join(", ")
      end

      # We're forcing, so just delete the old box
      @logger.info(
        "Box already exists, but forcing so removing: #{name} #{box_formats.join(", ")}")
      box.destroy!
    end

    log_provider = formats ? formats.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(formats) if formats

    # Verify that a V1 box doesn't exist. If it does, then we signal
    # to the user that we need an upgrade.
    raise Errors::BoxUpgradeRequired, :name => name if v1_box?(@directory.join(name))

    # 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", "-v", "-x", "-m", "-C", temp_dir.to_s, "-f", path.to_s)
      raise Errors::BoxUnpackageFailure, :output => result.stderr.to_s if result.exit_code != 0

      # 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, 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.["provider"]

        if formats
          found = false
          formats.each do |format|
            # Verify that the given provider matches what the box has.
            if box_provider.to_sym == format.to_sym
              found = true
              break
            end
          end

          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
        box_dir = @directory.join(name)
        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

        # Go through each child and copy them one-by-one. This avoids
        # an issue where on Windows cross-device directory copies are
        # failing for some reason. [GH-1424]
        final_temp_dir.children(true).each do |f|
          destination = final_dir.join(f.basename)
          @logger.debug("Moving: #{f} => #{destination}")
          FileUtils.mv(f, destination)
        end
      end
    end
  end

  # Return the box
  find(name, provider)
end

#allArray

This returns an array of all the boxes on the system, given by their name and their provider.

Returns:

  • (Array)

    Array of ‘[name, provider]` pairs of the boxes installed on this system. An optional third element in the array may specify `:v1` if the box is a version 1 box.



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/vagrant/box_collection.rb', line 194

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 = child.basename.to_s

      # If this is a V1 box, we still return that name, but specify
      # that the box is a V1 box.
      if v1_box?(child)
        @logger.debug("V1 box found: #{box_name}")
        results << [box_name, :virtualbox, :v1]
        next
      end

      # Otherwise, traverse the subdirectories and see what providers
      # we have.
      child.children(true).each do |provider|
        # 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})")
          results << [box_name, provider_name]
        else
          @logger.debug("Invalid box, ignoring: #{provider}")
        end
      end
    end
  end

  results
end

#find(name, providers) ⇒ Box

Find a box in the collection with the given name and provider.

Parameters:

  • name (String)

    Name of the box (logical name).

  • provider (String)

    Provider that the box implements.

Returns:

  • (Box)

    The box found, or ‘nil` if not found.



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/vagrant/box_collection.rb', line 238

def find(name, providers)
  providers = [providers].flatten

  with_collection_lock do
    providers.each do |provider|
      # First look directly for the box we're asking for.
      box_directory = @directory.join(name, provider.to_s, "metadata.json")
      @logger.info("Searching for box: #{name} (#{provider}) in #{box_directory}")
      if box_directory.file?
        @logger.info("Box found: #{name} (#{provider})")
        return Box.new(name, provider, box_directory.dirname)
      end

      # If we're looking for a VirtualBox box, then we check if there is
      # a V1 box.
      if provider.to_sym == :virtualbox
        # Check if a V1 version of this box exists, and if so, raise an
        # exception notifying the caller that the box exists but needs
        # to be upgraded. We don't do the upgrade here because it can be
        # a fairly intensive activity and don't want to immediately degrade
        # user performance on a find.
        #
        # To determine if it is a V1 box we just do a simple heuristic
        # based approach.
        @logger.info("Searching for V1 box: #{name}")
        if v1_box?(@directory.join(name))
          @logger.warn("V1 box found: #{name}")
          raise Errors::BoxUpgradeRequired, :name => name
        end
      end
    end
  end

  # Didn't find it, return nil
  @logger.info("Box not found: #{name} (#{providers.join(", ")})")
  nil
end

#upgrade(name) ⇒ Boolean

Upgrades a V1 box with the given name to a V2 box. If a box with the given name doesn’t exist, then a ‘BoxNotFound` exception will be raised. If the given box is found but is not a V1 box then `true` is returned because this just works fine.

Parameters:

  • name (String)

    Name of the box (logical name).

Returns:

  • (Boolean)

    ‘true` otherwise an exception is raised.



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/vagrant/box_collection.rb', line 283

def upgrade(name)
  with_collection_lock do
    @logger.debug("Upgrade request for box: #{name}")
    box_dir = @directory.join(name)

    # If the box doesn't exist at all, raise an exception
    raise Errors::BoxNotFound, :name => name, :provider => "virtualbox" if !box_dir.directory?

    if v1_box?(box_dir)
      @logger.debug("V1 box #{name} found. Upgrading!")

      # First we actually perform the upgrade
      temp_dir = v1_upgrade(box_dir)

      # Rename the temporary directory to the provider.
      FileUtils.mv(temp_dir.to_s, box_dir.join("virtualbox").to_s)
      @logger.info("Box '#{name}' upgraded from V1 to V2.")
    end
  end

  # We did it! Or the v1 box didn't exist so it doesn't matter.
  return true
end