tenderloin 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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