tenderloin 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. data/LICENCE +21 -0
  2. data/README.md +50 -0
  3. data/Version +1 -0
  4. data/bin/loin +5 -0
  5. data/config/default.rb +26 -0
  6. data/lib/tenderloin/actions/base.rb +93 -0
  7. data/lib/tenderloin/actions/box/add.rb +22 -0
  8. data/lib/tenderloin/actions/box/destroy.rb +14 -0
  9. data/lib/tenderloin/actions/box/download.rb +63 -0
  10. data/lib/tenderloin/actions/box/unpackage.rb +46 -0
  11. data/lib/tenderloin/actions/runner.rb +138 -0
  12. data/lib/tenderloin/actions/vm/boot.rb +52 -0
  13. data/lib/tenderloin/actions/vm/destroy.rb +18 -0
  14. data/lib/tenderloin/actions/vm/halt.rb +14 -0
  15. data/lib/tenderloin/actions/vm/import.rb +32 -0
  16. data/lib/tenderloin/actions/vm/move_hard_drive.rb +53 -0
  17. data/lib/tenderloin/actions/vm/provision.rb +71 -0
  18. data/lib/tenderloin/actions/vm/reload.rb +17 -0
  19. data/lib/tenderloin/actions/vm/shared_folders.rb +47 -0
  20. data/lib/tenderloin/actions/vm/start.rb +17 -0
  21. data/lib/tenderloin/actions/vm/up.rb +55 -0
  22. data/lib/tenderloin/box.rb +143 -0
  23. data/lib/tenderloin/busy.rb +73 -0
  24. data/lib/tenderloin/cli.rb +59 -0
  25. data/lib/tenderloin/commands.rb +154 -0
  26. data/lib/tenderloin/config.rb +144 -0
  27. data/lib/tenderloin/downloaders/base.rb +13 -0
  28. data/lib/tenderloin/downloaders/file.rb +21 -0
  29. data/lib/tenderloin/downloaders/http.rb +47 -0
  30. data/lib/tenderloin/env.rb +156 -0
  31. data/lib/tenderloin/fusion_vm.rb +85 -0
  32. data/lib/tenderloin/ssh.rb +49 -0
  33. data/lib/tenderloin/util.rb +51 -0
  34. data/lib/tenderloin/vm.rb +63 -0
  35. data/lib/tenderloin/vmx_file.rb +28 -0
  36. data/lib/tenderloin.rb +14 -0
  37. data/script/tenderloin-ssh-expect.sh +23 -0
  38. data/templates/Tenderfile +8 -0
  39. data/test/tenderloin/actions/base_test.rb +32 -0
  40. data/test/tenderloin/actions/box/add_test.rb +37 -0
  41. data/test/tenderloin/actions/box/destroy_test.rb +18 -0
  42. data/test/tenderloin/actions/box/download_test.rb +118 -0
  43. data/test/tenderloin/actions/box/unpackage_test.rb +100 -0
  44. data/test/tenderloin/actions/runner_test.rb +262 -0
  45. data/test/tenderloin/actions/vm/boot_test.rb +55 -0
  46. data/test/tenderloin/actions/vm/destroy_test.rb +24 -0
  47. data/test/tenderloin/actions/vm/down_test.rb +32 -0
  48. data/test/tenderloin/actions/vm/export_test.rb +88 -0
  49. data/test/tenderloin/actions/vm/forward_ports_test.rb +50 -0
  50. data/test/tenderloin/actions/vm/halt_test.rb +27 -0
  51. data/test/tenderloin/actions/vm/import_test.rb +36 -0
  52. data/test/tenderloin/actions/vm/move_hard_drive_test.rb +108 -0
  53. data/test/tenderloin/actions/vm/package_test.rb +181 -0
  54. data/test/tenderloin/actions/vm/provision_test.rb +103 -0
  55. data/test/tenderloin/actions/vm/reload_test.rb +44 -0
  56. data/test/tenderloin/actions/vm/resume_test.rb +27 -0
  57. data/test/tenderloin/actions/vm/shared_folders_test.rb +117 -0
  58. data/test/tenderloin/actions/vm/start_test.rb +28 -0
  59. data/test/tenderloin/actions/vm/suspend_test.rb +27 -0
  60. data/test/tenderloin/actions/vm/up_test.rb +98 -0
  61. data/test/tenderloin/box_test.rb +139 -0
  62. data/test/tenderloin/busy_test.rb +83 -0
  63. data/test/tenderloin/commands_test.rb +269 -0
  64. data/test/tenderloin/config_test.rb +123 -0
  65. data/test/tenderloin/downloaders/base_test.rb +20 -0
  66. data/test/tenderloin/downloaders/file_test.rb +32 -0
  67. data/test/tenderloin/downloaders/http_test.rb +40 -0
  68. data/test/tenderloin/env_test.rb +345 -0
  69. data/test/tenderloin/ssh_test.rb +103 -0
  70. data/test/tenderloin/util_test.rb +64 -0
  71. data/test/tenderloin/vm_test.rb +89 -0
  72. data/test/test_helper.rb +92 -0
  73. metadata +241 -0
@@ -0,0 +1,71 @@
1
+ module Tenderloin
2
+ module Actions
3
+ module VM
4
+ class Provision < Base
5
+ def execute!
6
+ chown_provisioning_folder
7
+ setup_json
8
+ setup_solo_config
9
+ run_chef_solo
10
+ end
11
+
12
+ def chown_provisioning_folder
13
+ logger.info "Setting permissions on provisioning folder..."
14
+ SSH.execute do |ssh|
15
+ ssh.exec!("sudo chown #{Tenderloin.config.ssh.username} #{Tenderloin.config.chef.provisioning_path}")
16
+ end
17
+ end
18
+
19
+ def setup_json
20
+ logger.info "Generating JSON and uploading..."
21
+
22
+ # Set up initial configuration
23
+ data = {
24
+ :config => Tenderloin.config,
25
+ :directory => Tenderloin.config.vm.project_directory,
26
+ }
27
+
28
+ # And wrap it under the "tenderloin" namespace
29
+ data = { :tenderloin => data }
30
+
31
+ # Merge with the "extra data" which isn't put under the
32
+ # tenderloin namespace by default
33
+ data.merge!(Tenderloin.config.chef.json)
34
+
35
+ json = data.to_json
36
+
37
+ SSH.upload!(StringIO.new(json), File.join(Tenderloin.config.chef.provisioning_path, "dna.json"))
38
+ end
39
+
40
+ def setup_solo_config
41
+ solo_file = <<-solo
42
+ file_cache_path "#{Tenderloin.config.chef.provisioning_path}"
43
+ cookbook_path "#{cookbooks_path}"
44
+ solo
45
+
46
+ logger.info "Uploading chef-solo configuration script..."
47
+ SSH.upload!(StringIO.new(solo_file), File.join(Tenderloin.config.chef.provisioning_path, "solo.rb"))
48
+ end
49
+
50
+ def run_chef_solo
51
+ logger.info "Running chef recipes..."
52
+ SSH.execute do |ssh|
53
+ ssh.exec!("cd #{Tenderloin.config.chef.provisioning_path} && sudo chef-solo -c solo.rb -j dna.json") do |channel, data, stream|
54
+ # TODO: Very verbose. It would be easier to save the data and only show it during
55
+ # an error, or when verbosity level is set high
56
+ logger.info("#{stream}: #{data}")
57
+ end
58
+ end
59
+ end
60
+
61
+ def cookbooks_path
62
+ File.join(Tenderloin.config.chef.provisioning_path, "cookbooks")
63
+ end
64
+
65
+ def collect_shared_folders
66
+ ["tenderloin-provisioning", File.expand_path(Tenderloin.config.chef.cookbooks_path, Env.root_path), cookbooks_path]
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,17 @@
1
+ module Tenderloin
2
+ module Actions
3
+ module VM
4
+ class Reload < Base
5
+ def prepare
6
+ steps = [SharedFolders, Boot]
7
+ steps.unshift(Halt) if @runner.vm.running?
8
+ steps << Provision if Tenderloin.config.chef.enabled
9
+
10
+ steps.each do |action_klass|
11
+ @runner.add_action(action_klass)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,47 @@
1
+ module Tenderloin
2
+ module Actions
3
+ module VM
4
+ class SharedFolders < Base
5
+ def shared_folders
6
+ shared_folders = @runner.invoke_callback(:collect_shared_folders)
7
+
8
+ # Basic filtering of shared folders. Basically only verifies that
9
+ # the result is an array of 3 elements. In the future this should
10
+ # also verify that the host path exists, the name is valid,
11
+ # and that the guest path is valid.
12
+ shared_folders.collect do |folder|
13
+ if folder.is_a?(Array) && folder.length == 3
14
+ folder
15
+ else
16
+ nil
17
+ end
18
+ end.compact
19
+ end
20
+
21
+ def before_boot
22
+
23
+ end
24
+
25
+ def after_boot
26
+ logger.info "Creating shared folders metadata..."
27
+
28
+ shared_folders.each do |name, hostpath, guestpath|
29
+ @runner.fusion_vm.share_folder(name, hostpath)
30
+ @runner.fusion_vm.enable_shared_folders
31
+ end
32
+
33
+ logger.info "Linking shared folders..."
34
+
35
+ Tenderloin::SSH.execute(@runner.fusion_vm.ip) do |ssh|
36
+ shared_folders.each do |name, hostpath, guestpath|
37
+ logger.info "-- #{name}: #{guestpath}"
38
+ ssh.exec!("sudo ln -s /mnt/hgfs/#{name} #{guestpath}")
39
+ ssh.exec!("sudo chown #{Tenderloin.config.ssh.username} #{guestpath}")
40
+ end
41
+ end
42
+ end
43
+
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,17 @@
1
+ module Tenderloin
2
+ module Actions
3
+ module VM
4
+ class Start < Base
5
+ def prepare
6
+ # Start is a "meta-action" so it really just queues up a bunch
7
+ # of other actions in its place:
8
+ steps = [SharedFolders, Boot]
9
+
10
+ steps.each do |action_klass|
11
+ @runner.add_action(action_klass)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,55 @@
1
+ module Tenderloin
2
+ module Actions
3
+ module VM
4
+ class Up < Base
5
+ def prepare
6
+ # If the dotfile is not a file, raise error
7
+ if File.exist?(Env.dotfile_path) && !File.file?(Env.dotfile_path)
8
+ raise ActionException.new(<<-msg)
9
+ The dotfile which Tenderloin uses to store the UUID of the project's
10
+ virtual machine already exists and is not a file! The dotfile is
11
+ currently configured to be `#{Env.dotfile_path}`
12
+
13
+ To change this value, please see `config.tenderloin.dotfile_name`
14
+ msg
15
+ end
16
+
17
+ # Up is a "meta-action" so it really just queues up a bunch
18
+ # of other actions in its place:
19
+ steps = [Import, SharedFolders, Boot]
20
+ steps << Provision if Tenderloin.config.chef.enabled
21
+ steps.insert(0, MoveHardDrive) if Tenderloin.config.vm.hd_location
22
+
23
+ steps.each do |action_klass|
24
+ @runner.add_action(action_klass)
25
+ end
26
+ end
27
+
28
+ def after_import
29
+ persist
30
+ setup_uuid_mac
31
+ end
32
+
33
+ def persist
34
+ logger.info "Persisting the VM UUID (#{@runner.vm_id})..."
35
+ Env.persist_vm(@runner.vm_id)
36
+ end
37
+
38
+ def setup_uuid_mac
39
+ logger.info "Resetting VMX UUID, MAC and Display Name..."
40
+
41
+ VMXFile.with_vmx_data(@runner.vmx_path) do |data|
42
+ data.delete "ethernet0.addressType"
43
+ data.delete "uuid.location"
44
+ data.delete "uuid.bios"
45
+ data.delete "ethernet0.generatedAddress"
46
+ data.delete "ethernet1.generatedAddress"
47
+ data.delete "ethernet0.generatedAddressOffset"
48
+ data.delete "ethernet1.generatedAddressOffset"
49
+ data['displayName'] = "tenderloin-" + @runner.vm_id
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,143 @@
1
+ module Tenderloin
2
+ # Represents a "box," which is simply a packaged tenderloin environment.
3
+ # Boxes are simply `tar` files which contain an exported VirtualBox
4
+ # virtual machine, at the least. They are created with `tenderloin package`
5
+ # and may contain additional files if specified by the creator. This
6
+ # class serves to help manage these boxes, although most of the logic
7
+ # is kicked out to actions.
8
+ #
9
+ # What can the {Box} class do?
10
+ #
11
+ # * Find boxes
12
+ # * Add existing boxes (from some URI)
13
+ # * Delete existing boxes
14
+ #
15
+ # # Finding Boxes
16
+ #
17
+ # Using the {Box.find} method, you can search for existing boxes. This
18
+ # method will return `nil` if none is found or an instance of {Box}
19
+ # otherwise.
20
+ #
21
+ # box = Tenderloin::Box.find("base")
22
+ # if box.nil?
23
+ # puts "Box not found!"
24
+ # else
25
+ # puts "Box exists at #{box.directory}"
26
+ # end
27
+ #
28
+ # # Adding a Box
29
+ #
30
+ # Boxes can be added from any URI. Some schemas aren't supported; if this
31
+ # is the case, the error will output to the logger.
32
+ #
33
+ # Tenderloin::Box.add("foo", "http://myfiles.com/foo.box")
34
+ #
35
+ # # Destroying a box
36
+ #
37
+ # Boxes can be deleted as well. This method is _final_ and there is no way
38
+ # to undo this action once it is completed.
39
+ #
40
+ # box = Tenderloin::Box.find("foo")
41
+ # box.destroy
42
+ #
43
+ class Box < Actions::Runner
44
+ # The name of the box.
45
+ attr_accessor :name
46
+
47
+ # The URI for a new box. This is not available for existing boxes.
48
+ attr_accessor :uri
49
+
50
+ # The temporary path to the downloaded or copied box. This should
51
+ # only be used internally.
52
+ attr_accessor :temp_path
53
+
54
+ class <<self
55
+ # Returns an array of all created boxes, as strings.
56
+ #
57
+ # @return [Array<String>]
58
+ def all
59
+ results = []
60
+
61
+ Dir.open(Env.boxes_path) do |dir|
62
+ dir.each do |d|
63
+ next if d == "." || d == ".." || !File.directory?(File.join(Env.boxes_path, d))
64
+ results << d.to_s
65
+ end
66
+ end
67
+
68
+ results
69
+ end
70
+
71
+ # Finds a box with the given name. This method searches for a box
72
+ # with the given name, returning `nil` if none is found or returning
73
+ # a {Box} instance otherwise.
74
+ #
75
+ # @param [String] name The name of the box
76
+ # @return [Box] Instance of {Box} representing the box found
77
+ def find(name)
78
+ return nil unless File.directory?(directory(name))
79
+ new(name)
80
+ end
81
+
82
+ # Adds a new box with given name from the given URI. This method
83
+ # begins the process of adding a box from a given URI by setting up
84
+ # the {Box} instance and calling {#add}.
85
+ #
86
+ # @param [String] name The name of the box
87
+ # @param [String] uri URI to the box file
88
+ def add(name, uri)
89
+ box = new
90
+ box.name = name
91
+ box.uri = uri
92
+ box.add
93
+ end
94
+
95
+ # Returns the directory to a box of the given name. The name given
96
+ # as a parameter is not checked for existence; this method simply
97
+ # returns the directory which would be used if the box did exist.
98
+ #
99
+ # @param [String] name Name of the box whose directory you're interested in.
100
+ # @return [String] Full path to the box directory.
101
+ def directory(name)
102
+ File.join(Env.boxes_path, name)
103
+ end
104
+ end
105
+
106
+ # Creates a new box instance. Given an optional `name` parameter,
107
+ # newly created instance will have that name, otherwise it defaults
108
+ # to `nil`.
109
+ #
110
+ # **Note:** This method does not actually _create_ the box, but merely
111
+ # returns a new, abstract representation of it. To add a box, see {#add}.
112
+ def initialize(name=nil)
113
+ @name = name
114
+ end
115
+
116
+ # Returns path to the vmx file of the box. This contains the config
117
+ #
118
+ # @return [String]
119
+ def vmx_file
120
+ File.join(directory, Tenderloin.config.vm.box_vmx)
121
+ end
122
+
123
+ # Begins the process of adding a box to the tenderloin installation. This
124
+ # method requires that `name` and `uri` be set. The logic of this method
125
+ # is kicked out to the {Actions::Box::Add add box} action.
126
+ def add
127
+ execute!(Actions::Box::Add)
128
+ end
129
+
130
+ # Beings the process of destroying this box.
131
+ def destroy
132
+ execute!(Actions::Box::Destroy)
133
+ end
134
+
135
+ # Returns the directory to the location of this boxes content in the local
136
+ # filesystem.
137
+ #
138
+ # @return [String]
139
+ def directory
140
+ self.class.directory(self.name)
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,73 @@
1
+ module Tenderloin
2
+ def self.busy?
3
+ Busy.busy?
4
+ end
5
+
6
+ def self.busy(&block)
7
+ Busy.busy(&block)
8
+ end
9
+
10
+ class Busy
11
+ extend Tenderloin::Util
12
+
13
+ @@busy = false
14
+ @@mutex = Mutex.new
15
+ @@trap_thread = nil
16
+
17
+ class << self
18
+ def busy?
19
+ @@busy
20
+ end
21
+
22
+ def busy=(val)
23
+ @@busy = val
24
+ end
25
+
26
+ def busy(&block)
27
+ @@mutex.synchronize do
28
+ begin
29
+ Signal.trap("INT") { wait_for_not_busy }
30
+ Busy.busy = true
31
+ runner = Thread.new(block) { block.call }
32
+ runner.join
33
+ ensure
34
+ # In the case an exception is thrown, make sure we restore
35
+ # busy back to some sane state.
36
+ Busy.busy = false
37
+
38
+ # Make sure that the trap thread completes, if it is running
39
+ trap_thread.join if trap_thread
40
+
41
+ # And restore the INT trap to the default
42
+ Signal.trap("INT", "DEFAULT")
43
+ end
44
+ end
45
+ end
46
+
47
+ def wait_for_not_busy(sleeptime=5)
48
+ @@trap_thread ||= Thread.new do
49
+ # Wait while the app is busy
50
+ loop do
51
+ break unless busy?
52
+ logger.info "Waiting for tenderloin to clean itself up..."
53
+ sleep sleeptime
54
+ end
55
+
56
+ # Exit out of the entire script
57
+ logger.info "Exiting tenderloin..."
58
+ exit
59
+ end
60
+ end
61
+
62
+ # Used for testing
63
+ def reset_trap_thread!
64
+ @@trap_thread = nil
65
+ end
66
+
67
+ # Returns the trap thread
68
+ def trap_thread
69
+ @@trap_thread
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,59 @@
1
+ require 'thor'
2
+ require 'tenderloin'
3
+
4
+ module Tenderloin
5
+
6
+ class CLI < Thor
7
+ class_option :file, :aliases => :'-f', :default => "Tenderfile"
8
+
9
+ no_tasks do
10
+ def setup
11
+ $ROOTFILE_NAME = options[:file].dup.freeze
12
+ end
13
+ end
14
+
15
+ desc "up [--file <tenderfile>]", "Boots the VM"
16
+ def up()
17
+ setup
18
+ Tenderloin::Commands.up
19
+ end
20
+
21
+ desc "halt [--file <tenderfile>]", "Force shuts down the running VM"
22
+ def halt()
23
+ setup
24
+ Tenderloin::Commands.halt
25
+ end
26
+
27
+ desc "destroy [--file <tenderfile>]", "Shuts down and deletes the VM"
28
+ def destroy()
29
+ setup
30
+ Tenderloin::Commands.destroy
31
+ end
32
+
33
+ desc "box [--file <tenderfile>]", "Manages base boxes"
34
+ # TODO - make the box command use real args/a subcommand
35
+ def box(arg1=nil, arg2=nil, arg3=nil, arg4=nil)
36
+ setup
37
+ Tenderloin::Commands.box([arg1, arg2, arg3, arg4].compact)
38
+ end
39
+
40
+ desc "init [--file <tenderfile>]", "Creates a new Tenderfile"
41
+ def init()
42
+ setup
43
+ Tenderloin::Commands.init
44
+ end
45
+
46
+ desc "reload [--file <tenderfile>]", "Reboots & re-provisions the VM"
47
+ def reload()
48
+ setup
49
+ Tenderloin::Commands.reload
50
+ end
51
+
52
+ desc "ssh [--file <tenderfile>]", "SSH's in to the VM"
53
+ def ssh()
54
+ setup
55
+ Tenderloin::Commands.ssh
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,154 @@
1
+ module Tenderloin
2
+ # Contains all the command-line commands invoked by the
3
+ # binaries. Having them all in one location assists with
4
+ # documentation and also takes the commands out of some of
5
+ # the other classes.
6
+ class Commands
7
+ extend Tenderloin::Util
8
+
9
+ class << self
10
+ # Initializes a directory for use with tenderloin. This command copies an
11
+ # initial `Tenderfile` into the current working directory so you can
12
+ # begin using tenderloin. The configuration file contains some documentation
13
+ # to get you started.
14
+ def init
15
+ rootfile_path = File.join(Dir.pwd, $ROOTFILE_NAME)
16
+ if File.exist?(rootfile_path)
17
+ error_and_exit(<<-error)
18
+ It looks like this directory is already setup for tenderloin! (A #{$ROOTFILE_NAME}
19
+ already exists.)
20
+ error
21
+ end
22
+
23
+ # Copy over the rootfile template into this directory
24
+ FileUtils.cp(File.join(PROJECT_ROOT, "templates", $ROOTFILE_NAME), rootfile_path)
25
+ end
26
+
27
+ # Bring up a tenderloin instance. This handles everything from importing
28
+ # the base VM, setting up shared folders, forwarded ports, etc to
29
+ # provisioning the instance with chef. {up} also starts the instance,
30
+ # running it in the background.
31
+ def up
32
+ Env.load!
33
+
34
+ if Env.persisted_vm
35
+ logger.info "VM already created. Starting VM if its not already running..."
36
+ Env.persisted_vm.start
37
+ else
38
+ Env.require_box
39
+ VM.execute!(Actions::VM::Up)
40
+ end
41
+ end
42
+
43
+ # Tear down a tenderloin instance. This not only shuts down the instance
44
+ # (if its running), but also deletes it from the system, including the
45
+ # hard disks associated with it.
46
+ #
47
+ # This command requires that an instance already be brought up with
48
+ # `tenderloin up`.
49
+ def destroy
50
+ Env.load!
51
+ Env.require_persisted_vm
52
+ Env.persisted_vm.destroy
53
+ end
54
+
55
+ # Reload the environment. This is almost equivalent to the {up} command
56
+ # except that it doesn't import the VM and do the initialize bootstrapping
57
+ # of the instance. Instead, it forces a shutdown (if its running) of the
58
+ # VM, updates the metadata (shared folders, forwarded ports), restarts
59
+ # the VM, and then reruns the provisioning if enabled.
60
+ def reload
61
+ Env.load!
62
+ Env.require_persisted_vm
63
+ Env.persisted_vm.execute!(Actions::VM::Reload)
64
+ end
65
+
66
+ # SSH into the tenderloin instance. This will setup an SSH connection into
67
+ # the tenderloin instance, replacing the running ruby process with the SSH
68
+ # connection.
69
+ #
70
+ # This command requires that an instance already be brought up with
71
+ # `tenderloin up`.
72
+ def ssh
73
+ Env.load!
74
+ Env.require_persisted_vm
75
+ SSH.connect :host => Env.persisted_vm.fusion_vm.ip
76
+ end
77
+
78
+ # Halts a running tenderloin instance. This forcibly halts the instance;
79
+ # it is the equivalent of pulling the power on a machine. The instance
80
+ # can be restarted again with {up}.
81
+ #
82
+ # This command requires than an instance already be brought up with
83
+ # `tenderloin up`.
84
+ def halt
85
+ Env.load!
86
+ Env.require_persisted_vm
87
+ Env.persisted_vm.execute!(Actions::VM::Halt)
88
+ end
89
+
90
+ # Manages the `tenderloin box` command, allowing the user to add
91
+ # and remove boxes. This single command, given an array, determines
92
+ # which action to take and calls the respective action method
93
+ # (see {box_add} and {box_remove})
94
+ def box(argv)
95
+ Env.load!
96
+
97
+ sub_commands = ["list", "add", "remove"]
98
+
99
+ if !sub_commands.include?(argv[0])
100
+ error_and_exit(<<-error)
101
+ Please specify a valid action to take on the boxes, either
102
+ `add` or `remove`. Examples:
103
+
104
+ tenderloin box add name uri
105
+ tenderloin box remove name
106
+ error
107
+ end
108
+
109
+ send("box_#{argv[0]}", *argv[1..-1])
110
+ end
111
+
112
+ # Lists all added boxes
113
+ def box_list
114
+ boxes = Box.all.sort
115
+
116
+ wrap_output do
117
+ if !boxes.empty?
118
+ puts "Installed Tenderloin Boxes:\n\n"
119
+ boxes.each do |box|
120
+ puts box
121
+ end
122
+ else
123
+ puts "No Tenderloin Boxes Added!"
124
+ end
125
+ end
126
+ end
127
+
128
+ # Adds a box to the local filesystem, given a URI.
129
+ def box_add(name, path)
130
+ Box.add(name, path)
131
+ end
132
+
133
+ # Removes a box.
134
+ def box_remove(name)
135
+ box = Box.find(name)
136
+ if box.nil?
137
+ error_and_exit(<<-error)
138
+ The box you're attempting to remove does not exist!
139
+ error
140
+ return # for tests
141
+ end
142
+
143
+ box.destroy
144
+ end
145
+
146
+ private
147
+
148
+ def act_on_vm(&block)
149
+ yield Env.persisted_vm
150
+ Env.persisted_vm.execute!
151
+ end
152
+ end
153
+ end
154
+ end