fission 0.3.0 → 0.4.0.beta.1

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 (50) hide show
  1. data/.gitignore +1 -0
  2. data/CHANGELOG.md +3 -0
  3. data/README.md +1 -1
  4. data/lib/fission.rb +5 -6
  5. data/lib/fission/cli.rb +77 -7
  6. data/lib/fission/command.rb +43 -1
  7. data/lib/fission/command/clone.rb +19 -20
  8. data/lib/fission/command/delete.rb +29 -25
  9. data/lib/fission/command/snapshot_create.rb +11 -26
  10. data/lib/fission/command/snapshot_list.rb +13 -19
  11. data/lib/fission/command/snapshot_revert.rb +11 -26
  12. data/lib/fission/command/start.rb +11 -25
  13. data/lib/fission/command/status.rb +26 -10
  14. data/lib/fission/command/stop.rb +10 -21
  15. data/lib/fission/command/suspend.rb +21 -21
  16. data/lib/fission/command_helpers.rb +21 -0
  17. data/lib/fission/config.rb +35 -0
  18. data/lib/fission/fusion.rb +11 -3
  19. data/lib/fission/lease.rb +148 -0
  20. data/lib/fission/metadata.rb +55 -2
  21. data/lib/fission/response.rb +76 -0
  22. data/lib/fission/ui.rb +49 -0
  23. data/lib/fission/version.rb +1 -1
  24. data/lib/fission/vm.rb +653 -75
  25. data/spec/contexts/command.rb +12 -0
  26. data/spec/fission/cli_spec.rb +4 -11
  27. data/spec/fission/command/clone_spec.rb +45 -45
  28. data/spec/fission/command/delete_spec.rb +56 -43
  29. data/spec/fission/command/snapshot_create_spec.rb +29 -51
  30. data/spec/fission/command/snapshot_list_spec.rb +25 -26
  31. data/spec/fission/command/snapshot_revert_spec.rb +27 -53
  32. data/spec/fission/command/start_spec.rb +25 -69
  33. data/spec/fission/command/status_spec.rb +48 -13
  34. data/spec/fission/command/stop_spec.rb +25 -42
  35. data/spec/fission/command/suspend_spec.rb +54 -49
  36. data/spec/fission/command_helpers_spec.rb +30 -0
  37. data/spec/fission/command_spec.rb +19 -0
  38. data/spec/fission/config_spec.rb +24 -0
  39. data/spec/fission/fusion_spec.rb +6 -6
  40. data/spec/fission/lease_spec.rb +176 -0
  41. data/spec/fission/metadata_spec.rb +8 -8
  42. data/spec/fission/response_spec.rb +81 -0
  43. data/spec/fission/vm_spec.rb +869 -193
  44. data/spec/fission_spec.rb +0 -6
  45. data/spec/helpers/command_helpers.rb +12 -0
  46. data/spec/helpers/response_helpers.rb +21 -0
  47. data/spec/matchers/be_a_successful_response.rb +7 -0
  48. data/spec/matchers/be_an_unsuccessful_response.rb +10 -0
  49. data/spec/spec_helper.rb +7 -0
  50. metadata +24 -5
@@ -3,8 +3,23 @@ module Fission
3
3
 
4
4
  require 'cfpropertylist'
5
5
 
6
+ # Public: Gets/Sets the content (Hash).
6
7
  attr_accessor :content
7
8
 
9
+ # Public: Deletes the Fusion metadata related to a VM. The VM should not be
10
+ # running when this method is called. It's highly recommended to call this
11
+ # method without the Fusion GUI application running. If the Fusion GUI is
12
+ # running this method should succeed, but it's been observed that Fusion
13
+ # will recreate the metadata which is deleted. This leads to 'missing' VMs
14
+ # in the Fusion GUI.
15
+ #
16
+ # vm_path - The absolute path to the directory of a VM.
17
+ #
18
+ # Examples
19
+ #
20
+ # Fission::Metadata.delete_vm_info '/vms/foo.vmwarevm'
21
+ #
22
+ # Returns nothing.
8
23
  def self.delete_vm_info(vm_path)
9
24
  metadata = new
10
25
  metadata.load
@@ -13,24 +28,62 @@ module Fission
13
28
  metadata.save
14
29
  end
15
30
 
31
+ # Public: Reads the configured metadata file and populates the content
32
+ # variable with native ruby types.
33
+ #
34
+ # Examples
35
+ #
36
+ # metadata.load
37
+ #
38
+ # Returns nothing.
16
39
  def load
17
- raw_data = CFPropertyList::List.new :file => Fission.config.attributes['plist_file']
40
+ raw_data = CFPropertyList::List.new :file => Fission.config['plist_file']
18
41
  @content = CFPropertyList.native_types raw_data.value
19
42
  end
20
43
 
44
+ # Public: Saves a new version of the metadata file with the data in the
45
+ # content variable.
46
+ #
47
+ # Examples
48
+ #
49
+ # metadata.save
50
+ #
51
+ # Returns nothing.
21
52
  def save
22
53
  new_content = CFPropertyList::List.new
23
54
  new_content.value = CFPropertyList.guess @content
24
- new_content.save Fission.config.attributes['plist_file'],
55
+ new_content.save Fission.config['plist_file'],
25
56
  CFPropertyList::List::FORMAT_BINARY
26
57
  end
27
58
 
59
+ # Public: Deletes the VM information from the 'restart document path'
60
+ # metadata. The 'restart document path' dictates which GUI consoles to
61
+ # display when Fusion starts.
62
+ #
63
+ # vm_path - The absolute path to the directory of a VM.
64
+ #
65
+ # Examples
66
+ #
67
+ # metadata.delete_vm_restart_document 'vms/foo.vmwarevm'
68
+ #
69
+ # Returns nothing.
28
70
  def delete_vm_restart_document(vm_path)
29
71
  if @content.has_key?('PLRestartDocumentPaths')
30
72
  @content['PLRestartDocumentPaths'].delete_if { |p| p == vm_path }
31
73
  end
32
74
  end
33
75
 
76
+ # Public: Deletes the VM information from the 'favorites list' metadata.
77
+ # The 'favorites list' dictates which VMs are displayed in the Fusion VM
78
+ # libarary.
79
+ #
80
+ # vm_path - The absolute path to the directory of a VM.
81
+ #
82
+ # Examples
83
+ #
84
+ # metadata.delete_favorite_entry '/vms/foo.vmwarevm'
85
+ #
86
+ # Returns nothing.
34
87
  def delete_vm_favorite_entry(vm_path)
35
88
  @content['VMFavoritesListDefaults2'].delete_if { |vm| vm['path'] == vm_path }
36
89
  end
@@ -0,0 +1,76 @@
1
+ module Fission
2
+ class Response
3
+
4
+ # Public: Gets/Sets the code (Integer).
5
+ attr_accessor :code
6
+
7
+ # Public: Gets/Sets the message (String).
8
+ attr_accessor :message
9
+
10
+ # Public: Gets/Sets the data (can be any of type as needed).
11
+ attr_accessor :data
12
+
13
+ # Public: Initialize a Response object.
14
+ #
15
+ # args - Hash of arguments:
16
+ # :code - Integer which denotes the code of the Response. This is
17
+ # similar in concept to command line exit codes. The
18
+ # convention is that 0 denotes success and any other value
19
+ # is unsuccessful (default: 1).
20
+ # :message - String which denotes the message of the Response. The
21
+ # convention is that this should only be used when the
22
+ # Response is unsuccessful (default: '').
23
+ # :data - Any valid ruby object. This is used to convey any
24
+ # data that needs to be used by a caller. The convention
25
+ # is that this should only be used when the Response is
26
+ # successful (default nil).
27
+ #
28
+ # Examples
29
+ #
30
+ # Response.new :code => 0, :data => [1, 2, 3, 4]
31
+ #
32
+ # Response.new :code => 0, :data => true
33
+ #
34
+ # Response.new :code => 5, :message => 'Something went wrong'
35
+ #
36
+ # Returns a new Response instance.
37
+ def initialize(args={})
38
+ @code = args.fetch :code, 1
39
+ @message = args.fetch :message, ''
40
+ @data = args.fetch :data, nil
41
+ end
42
+
43
+ # Public: Helper method to determine if a response is successful or not.
44
+ #
45
+ # Examples
46
+ #
47
+ # response.successful?
48
+ # # => true
49
+ #
50
+ # response.successful?
51
+ # # => false
52
+ #
53
+ # Returns a Boolean.
54
+ # Returns true if the code is 0.
55
+ # Returns false if the code is any other value.
56
+ def successful?
57
+ @code == 0
58
+ end
59
+
60
+ # Public: Helper method to create a new Response object when running a
61
+ # command line tool.
62
+ #
63
+ # cmd_output - This should be the output of the command.
64
+ #
65
+ # Returns a Response.
66
+ # The Response's code attribute will be set to the value of '$?'. The
67
+ # Response's message attribute will be set to the provided command output
68
+ # if, and only if, the Response is unsuccessful.
69
+ def self.from_command(cmd_output)
70
+ response = new :code => $?.exitstatus
71
+ response.message = cmd_output unless response.successful?
72
+ response
73
+ end
74
+
75
+ end
76
+ end
data/lib/fission/ui.rb CHANGED
@@ -1,19 +1,68 @@
1
1
  module Fission
2
2
  class UI
3
+
4
+ # Internal: Returns the stdout value.
3
5
  attr_reader :stdout
4
6
 
7
+ # Internal: Initialize a UI object.
8
+ #
9
+ # stdout - The object to use for stdout (default: $stdout). This provides
10
+ # an easy way to capture/silence output if needed.
11
+ #
12
+ # Examples
13
+ #
14
+ # Fission::UI.new
15
+ #
16
+ # str_io = StringIO.new
17
+ # Fission::UI.new str_io
5
18
  def initialize(stdout=$stdout)
6
19
  @stdout = stdout
7
20
  end
8
21
 
22
+ # Internal: Outputs the specified argument to the configured stdout object.
23
+ # The 'puts' method will be called on the stdout object.
24
+ #
25
+ # s - The String to output.
26
+ #
27
+ # Examples
28
+ #
29
+ # ui.output "foo bar\n"
30
+ #
31
+ # Returns nothing.
9
32
  def output(s)
10
33
  @stdout.puts s
11
34
  end
12
35
 
36
+ # Internal: Outputs the specified arguments printf style. The 'printf'
37
+ # method will be called on the stdout object. Currently, this assuems there
38
+ # are two data items.
39
+ #
40
+ # string - The printf String.
41
+ # key - The String for the first data item.
42
+ # value - The String for the second data item.
43
+ #
44
+ # Examples
45
+ #
46
+ # ui.output_printf "%s %s\n", 'foo', bar
47
+ #
48
+ # Returns nothing.
13
49
  def output_printf(string, key, value)
14
50
  @stdout.send :printf, string, key, value
15
51
  end
16
52
 
53
+ # Internal: Outputs the specified argument to the configured stdout object
54
+ # and exits with the specified exit code.
55
+ #
56
+ # s - The String to output.
57
+ # exit_code - The Integer exit code.
58
+ #
59
+ # Examples
60
+ #
61
+ # ui.output_and_exit 'something went wrong', 99
62
+ #
63
+ # ui.output_and_exit 'all done', 0
64
+ #
65
+ # Returns nothing.
17
66
  def output_and_exit(s, exit_code)
18
67
  output s
19
68
  exit exit_code
@@ -1,3 +1,3 @@
1
1
  module Fission
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0.beta.1"
3
3
  end
data/lib/fission/vm.rb CHANGED
@@ -1,158 +1,654 @@
1
1
  module Fission
2
2
  class VM
3
+
4
+ # Public: Gets the name of the VM as a String.
3
5
  attr_reader :name
4
6
 
5
7
  def initialize(name)
6
8
  @name = name
7
9
  end
8
10
 
11
+ # Public: Creates a snapshot for a VM. The VM must be running in order
12
+ # to create a snapshot. Snapshot names must be unique.
13
+ #
14
+ # name - The desired name of the snapshot. The name must be unique.
15
+ #
16
+ # Examples
17
+ #
18
+ # @vm.create_snapshot('foo_snap_1')
19
+ #
20
+ # Returns a Response with the result.
21
+ # If successful, the Response's data attribute will be nil.
22
+ # If there is an error, an unsuccessful Response will be returned.
9
23
  def create_snapshot(name)
10
- command = "#{Fission.config.attributes['vmrun_cmd']} snapshot #{conf_file.gsub ' ', '\ '} \"#{name}\" 2>&1"
11
- output = `#{command}`
24
+ unless exists?
25
+ return Response.new :code => 1, :message => 'VM does not exist'
26
+ end
12
27
 
13
- if $?.exitstatus == 0
14
- Fission.ui.output "Snapshot '#{name}' created"
15
- else
16
- Fission.ui.output "There was an error creating the snapshot."
17
- Fission.ui.output_and_exit "The error was:\n#{output}", 1
28
+ running_response = running?
29
+ return running_response unless running_response.successful?
30
+
31
+ unless running_response.data
32
+ message = 'The VM must be running in order to take a snapshot.'
33
+ return Response.new :code => 1, :message => message
18
34
  end
35
+
36
+ conf_file_response = conf_file
37
+ return conf_file_response unless conf_file_response.successful?
38
+
39
+ snapshots_response = snapshots
40
+ return snapshots_response unless snapshots_response.successful?
41
+
42
+ if snapshots_response.data.include? name
43
+ message = "There is already a snapshot named '#{name}'."
44
+ return Response.new :code => 1, :message => message
45
+ end
46
+
47
+ command = "#{vmrun_cmd} snapshot "
48
+ command << "#{conf_file_response.data} \"#{name}\" 2>&1"
49
+
50
+ Response.from_command(`#{command}`)
19
51
  end
20
52
 
53
+ # Public: Reverts the VM to the specified snapshot. The snapshot to revert
54
+ # to must exist and the Fusion GUI must not be running.
55
+ #
56
+ # name - The snapshot name to revert to.
57
+ #
58
+ # Examples
59
+ #
60
+ # @vm.revert_to_snapshot('foo_snap_1')
61
+ #
62
+ # Returns a Response with the result.
63
+ # If successful, the Response's data attribute will be nil.
64
+ # If there is an error, an unsuccessful Response will be returned.
21
65
  def revert_to_snapshot(name)
22
- command = "#{Fission.config.attributes['vmrun_cmd']} revertToSnapshot #{conf_file.gsub ' ', '\ '} \"#{name}\" 2>&1"
23
- output = `#{command}`
66
+ unless exists?
67
+ return Response.new :code => 1, :message => 'VM does not exist'
68
+ end
24
69
 
25
- if $?.exitstatus == 0
26
- Fission.ui.output "Reverted to snapshot '#{name}'"
27
- else
28
- Fission.ui.output "There was an error reverting to the snapshot."
29
- Fission.ui.output_and_exit "The error was:\n#{output}", 1
70
+ if Fusion.running?
71
+ message = 'It looks like the Fusion GUI is currently running. '
72
+ message << 'A VM cannot be reverted to a snapshot when the Fusion GUI is running. '
73
+ message << 'Exit the Fusion GUI and try again.'
74
+ return Response.new :code => 1, :message => message
30
75
  end
76
+
77
+ conf_file_response = conf_file
78
+ return conf_file_response unless conf_file_response.successful?
79
+
80
+ snapshots_response = snapshots
81
+ return snapshots_response unless snapshots_response.successful?
82
+
83
+ unless snapshots_response.data.include? name
84
+ message = "Unable to find a snapshot named '#{name}'."
85
+ return Response.new :code => 1, :message => message
86
+ end
87
+
88
+ command = "#{vmrun_cmd} revertToSnapshot "
89
+ command << "#{conf_file_response.data} \"#{name}\" 2>&1"
90
+
91
+ Response.from_command(`#{command}`)
31
92
  end
32
93
 
94
+ # Public: List the snapshots for a VM.
95
+ #
96
+ # Examples
97
+ #
98
+ # @vm.snapshots.data
99
+ # # => ['snap 1', 'snap 2']
100
+ #
101
+ # Returns a Response with the result.
102
+ # If successful, the Repsonse's data attribute will be an Array of the
103
+ # snapshot names (String).
104
+ # If there is an error, an unsuccessful Response will be returned.
33
105
  def snapshots
34
- command = "#{Fission.config.attributes['vmrun_cmd']} listSnapshots #{conf_file.gsub ' ', '\ '} 2>&1"
106
+ unless exists?
107
+ return Response.new :code => 1, :message => 'VM does not exist'
108
+ end
109
+
110
+ conf_file_response = conf_file
111
+ return conf_file_response unless conf_file_response.successful?
112
+
113
+ command = "#{vmrun_cmd} listSnapshots "
114
+ command << "#{conf_file_response.data} 2>&1"
35
115
  output = `#{command}`
36
116
 
37
- if $?.exitstatus == 0
117
+ response = Response.new :code => $?.exitstatus
118
+
119
+ if response.successful?
38
120
  snaps = output.split("\n").select { |s| !s.include? 'Total snapshots:' }
39
- snaps.map { |s| s.strip }
121
+ response.data = snaps.map { |s| s.strip }
40
122
  else
41
- Fission.ui.output "There was an error getting the list of snapshots."
42
- Fission.ui.output_and_exit "The error was:\n#{output}", 1
123
+ response.message = output
43
124
  end
125
+
126
+ response
44
127
  end
45
128
 
46
- def start(args={})
47
- command = "#{Fission.config.attributes['vmrun_cmd']} start #{conf_file.gsub ' ', '\ '} "
129
+ # Public: Starts a VM. The VM must not be running in order to start it.
130
+ #
131
+ # options - Hash of options:
132
+ # :headless - Boolean which specifies to start the VM without a
133
+ # GUI console. The Fusion GUI must not be running in
134
+ # order to start the VM headless.
135
+ # (default: false)
136
+ #
137
+ # Examples
138
+ #
139
+ # @vm.start
140
+ #
141
+ # @vm.start :headless => true
142
+ #
143
+ # Returns a Response with the result.
144
+ # If successful, the Response's data attribute will be nil.
145
+ # If there is an error, an unsuccessful Response will be returned.
146
+ def start(options={})
147
+ unless exists?
148
+ return Response.new :code => 1, :message => 'VM does not exist'
149
+ end
48
150
 
49
- if !args[:headless].blank? && args[:headless]
50
- command << "nogui 2>&1"
51
- else
52
- command << "gui 2>&1"
151
+ running_response = running?
152
+ return running_response unless running_response.successful?
153
+
154
+ if running_response.data
155
+ return Response.new :code => 1, :message => 'VM is already running'
53
156
  end
54
157
 
55
- output = `#{command}`
158
+ conf_file_response = conf_file
159
+ return conf_file_response unless conf_file_response.successful?
56
160
 
57
- if $?.exitstatus == 0
58
- Fission.ui.output "VM started"
59
- else
60
- Fission.ui.output "There was a problem starting the VM. The error was:\n#{output}"
161
+ unless options[:headless].blank?
162
+ if Fusion.running?
163
+ message = 'It looks like the Fusion GUI is currently running. '
164
+ message << 'A VM cannot be started in headless mode when the Fusion GUI is running. '
165
+ message << 'Exit the Fusion GUI and try again.'
166
+ return Response.new :code => 1, :message => message
167
+ end
61
168
  end
169
+
170
+ command = "#{vmrun_cmd} start "
171
+ command << "#{conf_file_response.data} "
172
+
173
+ command << (options[:headless].blank? ? 'gui ' : 'nogui ')
174
+ command << '2>&1'
175
+
176
+ Response.from_command(`#{command}`)
62
177
  end
63
178
 
64
- def stop
65
- command = "#{Fission.config.attributes['vmrun_cmd']} stop #{conf_file.gsub ' ', '\ '} 2>&1"
66
- output = `#{command}`
179
+ # Public: Stops a VM. The VM must be running in order to stop it.
180
+ #
181
+ # options - Hash of options:
182
+ # :hard - Boolean which specifies to power off the VM (instead of
183
+ # attempting to initiate a graceful shutdown). This is
184
+ # the equivalent of passing 'hard' to the vmrun stop
185
+ # command.
186
+ # (default: false)
187
+ #
188
+ # Examples
189
+ #
190
+ # @vm.stop
191
+ #
192
+ # @vm.stop :hard => true
193
+ #
194
+ # Returns a Response with the result.
195
+ # If successful, the Response's data attribute will be nil.
196
+ # If there is an error, an unsuccessful Response will be returned.
197
+ def stop(options={})
198
+ unless exists?
199
+ return Response.new :code => 1, :message => 'VM does not exist'
200
+ end
67
201
 
68
- if $?.exitstatus == 0
69
- Fission.ui.output "VM stopped"
70
- else
71
- Fission.ui.output "There was a problem stopping the VM. The error was:\n#{output}"
202
+ running_response = running?
203
+ return running_response unless running_response.successful?
204
+
205
+ unless running_response.data
206
+ return Response.new :code => 1, :message => 'VM is not running'
72
207
  end
208
+
209
+ conf_file_response = conf_file
210
+ return conf_file_response unless conf_file_response.successful?
211
+
212
+ command = "#{vmrun_cmd} stop "
213
+ command << "#{conf_file_response.data} "
214
+ command << 'hard ' unless options[:hard].blank?
215
+ command << '2>&1'
216
+
217
+ Response.from_command(`#{command}`)
73
218
  end
74
219
 
220
+ # Public: Suspends a VM. The VM must be running in order to suspend it.
221
+ #
222
+ # Examples
223
+ #
224
+ # @vm.suspend
225
+ #
226
+ # Returns a Response with the result.
227
+ # If successful, the Response's data attribute will be nil.
228
+ # If there is an error, an unsuccessful Response will be returned.
75
229
  def suspend
76
- command = "#{Fission.config.attributes['vmrun_cmd']} suspend #{conf_file.gsub ' ', '\ '} 2>&1"
77
- output = `#{command}`
230
+ unless exists?
231
+ return Response.new :code => 1, :message => 'VM does not exist'
232
+ end
233
+
234
+ running_response = running?
235
+ return running_response unless running_response.successful?
236
+
237
+ unless running_response.data
238
+ return Response.new :code => 1, :message => 'VM is not running'
239
+ end
240
+
241
+ conf_file_response = conf_file
242
+ return conf_file_response unless conf_file_response.successful?
243
+
244
+ command = "#{vmrun_cmd} suspend "
245
+ command << "#{conf_file_response.data} 2>&1"
246
+
247
+ Response.from_command(`#{command}`)
248
+ end
249
+
250
+ # Public: Provides the MAC addresses for a VM.
251
+ #
252
+ # Examples:
253
+ #
254
+ # @vm.mac_addresses.data
255
+ # # => ['00:0c:29:1d:6a:64', '00:0c:29:1d:6a:75']
256
+ #
257
+ # Returns a Response with the result.
258
+ # If successful, the Response's data attribute will be an Array of the MAC
259
+ # addresses found. If no MAC addresses are found, the Response's data
260
+ # attribute will be an empty Array.
261
+ # If there is an error, an unsuccessful Response will be returned.
262
+ def mac_addresses
263
+ network_response = network_info
264
+ return network_response unless network_response.successful?
265
+
266
+ response = Response.new :code => 0
267
+ response.data = network_response.data.values.collect { |n| n['mac_address'] }
268
+
269
+ response
270
+ end
271
+
272
+ # Public: Network information for a VM. Includes interface name, associated
273
+ # MAC address, and IP address (if applicable).
274
+ #
275
+ # Examples:
276
+ #
277
+ # # if IP addresses are found in the Fusion DHCP lease file
278
+ # response = @vm.network_info.data
279
+ # # => { 'ethernet0' => { 'mac_address' => '00:0c:29:1d:6a:64',
280
+ # 'ip_address' => '127.0.0.1' },
281
+ # 'ethernet1' => { 'mac_address' => '00:0c:29:1d:6a:75',
282
+ # 'ip_address' => '127.0.0.2' } }
283
+ #
284
+ # # if IP addresses are not found in the Fusion DHCP lease file
285
+ # response = @vm.network_info.data
286
+ # # => { 'ethernet0' => { 'mac_address' => '00:0c:29:1d:6a:64',
287
+ # 'ip_address' => nil },
288
+ # 'ethernet1' => { 'mac_address' => '00:0c:29:1d:6a:75',
289
+ # 'ip_address' => nil } }
290
+ #
291
+ # Returns a Response with the result.
292
+ # If successful, the Response's data attribute will be a Hash with the
293
+ # interface identifiers as the keys and the associated MAC address. If an
294
+ # IP address was found in the Fusion DHCP lease file, then it will
295
+ # be included. If an IP address was not found, then the IP address value
296
+ # will be nil. If there are no network interfaces, the Response's data
297
+ # attribute will be an empty Hash.
298
+ # If there is an error, an unsuccessful Response will be returned.
299
+ def network_info
300
+ conf_file_response = conf_file
301
+ return conf_file_response unless conf_file_response.successful?
302
+
303
+ response = Response.new :code => 0, :data => {}
304
+
305
+ interface_pattern = /^ethernet\d+/
306
+ mac_pattern = /(\w\w[:-]\w\w[:-]\w\w[:-]\w\w[:-]\w\w[:-]\w\w)/
307
+
308
+ File.open conf_file_response.data, 'r' do |f|
309
+ f.grep(mac_pattern).each do |line|
310
+ int = line.scan(interface_pattern)[0]
311
+ mac = line.scan(mac_pattern)[0].first
312
+ response.data[int] = {}
313
+ response.data[int]['mac_address'] = mac
314
+
315
+ lease_response = Fission::Lease.find_by_mac_address mac
316
+ return lease_response unless lease_response.successful?
317
+
318
+ response.data[int]['ip_address'] = nil
319
+
320
+ if lease_response.data
321
+ response.data[int]['ip_address'] = lease_response.data.ip_address
322
+ end
78
323
 
79
- if $?.exitstatus == 0
80
- Fission.ui.output "VM suspended"
324
+ end
325
+ end
326
+
327
+ response
328
+ end
329
+
330
+ # Public: Provides the state of the VM.
331
+ #
332
+ # Examples
333
+ #
334
+ # @vm.state.data
335
+ # # => 'running'
336
+ #
337
+ # @vm.state.data
338
+ # # => 'not running'
339
+ #
340
+ # @vm.state.data
341
+ # # => 'suspended'
342
+ #
343
+ # Returns a Response with the result.
344
+ # If the Response is successful, the Response's data attribute will
345
+ # be a String of the state. If the VM is currently powered on, the state
346
+ # will be 'running'. If the VM is deemed to be suspended, the state will be
347
+ # 'suspended'. If the VM is not running and not deemed to be suspended, the
348
+ # state will be 'not running'.
349
+ # If there is an error, an unsuccessful Response will be returned.
350
+ def state
351
+ running_response = running?
352
+ return running_response unless running_response.successful?
353
+
354
+ response = Response.new :code => 0, :data => 'not running'
355
+
356
+ if running_response.data
357
+ response.data = 'running'
81
358
  else
82
- Fission.ui.output "There was a problem suspending the VM. The error was:\n#{output}"
359
+ suspended_response = suspended?
360
+ return suspended_response unless suspended_response.successful?
361
+
362
+ response.data = 'suspended' if suspended_response.data
363
+ end
364
+
365
+ response
366
+ end
367
+
368
+ # Public: Determines if the VM exists or not. This method looks for the
369
+ # VM's conf file ('.vmx') to determine if the VM exists or not.
370
+ #
371
+ # Examples
372
+ #
373
+ # @vm.exists?
374
+ # # => true
375
+ #
376
+ # Returns a Boolean.
377
+ def exists?
378
+ conf_file.successful?
379
+ end
380
+
381
+ # Public: Determines if a VM is suspended.
382
+ #
383
+ # Examples
384
+ #
385
+ # @vm.suspended?.data
386
+ # # => true
387
+ #
388
+ # Returns a Response with the result.
389
+ # If successful, the Response's data attribute will be a Boolean. If the VM
390
+ # is not running, then this method will look for a '.vmem' file in the VM's
391
+ # directory. If a '.vmem' file exists and it matches the name of the VM,
392
+ # then the VM is considered to be suspended. If the VM is running or if a
393
+ # matching '.vmem' file is not found, then the VM is not considered to be
394
+ # suspended.
395
+ # If there is an error, an unsuccessful Response will be returned.
396
+ def suspended?
397
+ running_response = running?
398
+ return running_response unless running_response.successful?
399
+
400
+ response = Response.new :code => 0, :data => false
401
+ response.data = suspend_file_exists? unless running_response.data
402
+
403
+ response
404
+ end
405
+
406
+ # Public: Determines if a VM has a suspend file ('.vmem') in it's directory.
407
+ # This only looks for a suspend file which matches the name of the VM.
408
+ #
409
+ # Examples
410
+ #
411
+ # @vm.suspend_file_exists?
412
+ # # => true
413
+ #
414
+ # Returns a Boolean.
415
+ def suspend_file_exists?
416
+ File.file? File.join(path, "#{@name}.vmem")
417
+ end
418
+
419
+ # Public: Determines if a VM is running.
420
+ #
421
+ # Examples
422
+ #
423
+ # @vm.running?.data
424
+ # # => true
425
+ #
426
+ # Returns a Response with the result.
427
+ # If successful, the Response's data attribute will be a Boolean.
428
+ # If there is an error, an unsuccessful Response will be returned.
429
+ def running?
430
+ all_running_response = self.class.all_running
431
+ return all_running_response unless all_running_response.successful?
432
+
433
+ response = Response.new :code => 0, :data => false
434
+
435
+ if all_running_response.data.collect { |v| v.name }.include? @name
436
+ response.data = true
83
437
  end
438
+
439
+ response
84
440
  end
85
441
 
442
+ # Public: Determines the path to the VM's config file ('.vmx').
443
+ #
444
+ # Examples
445
+ #
446
+ # @vm.conf_file.data
447
+ # # => '/my_vms/foo/foo.vmx'
448
+ #
449
+ # Returns a Response with the result.
450
+ # If successful, the Response's data attribute will be a String which will
451
+ # be escaped for spaces (' ').
452
+ # If there is a single '.vmx' file in the VM's directory, regardless if
453
+ # the name of '.vmx' file matches the VM name, the Response's data
454
+ # attribute will the be the path to the '.vmx' file.
455
+ # If there are multiple '.vmx' files found in the VM's directory, there are
456
+ # a couple of different possible outcomes.
457
+ # If one of the file names matches the VM directory name, then the
458
+ # Response's data attribute will be the path to the matching '.vmx' file.
459
+ # If none of the file names match the VM directory name, then this is deemed
460
+ # an error condition and an unsuccessful Response will be returned.
461
+ # If there is an error, an unsuccessful Response will be returned.
86
462
  def conf_file
87
- vmx_path = File.join(self.class.path(@name), "*.vmx")
88
- conf_files = Dir.glob(vmx_path)
463
+ vmx_path = File.join path, "*.vmx"
464
+ conf_files = Dir.glob vmx_path
465
+
466
+ response = Response.new
89
467
 
90
468
  case conf_files.count
91
469
  when 0
92
- Fission.ui.output_and_exit "Unable to find a config file for VM '#{@name}' (in '#{vmx_path}')", 1
470
+ response.code = 1
471
+ response.message = "Unable to find a config file for VM '#{@name}' (in '#{vmx_path}')"
93
472
  when 1
94
- conf_files.first
473
+ response.code = 0
474
+ response.data = conf_files.first
95
475
  else
96
476
  if conf_files.include?(File.join(File.dirname(vmx_path), "#{@name}.vmx"))
97
- File.join(File.dirname(vmx_path), "#{@name}.vmx")
477
+ response.code = 0
478
+ response.data = File.join(File.dirname(vmx_path), "#{@name}.vmx")
98
479
  else
480
+ response.code = 1
99
481
  output = "Multiple config files found for VM '#{@name}' ("
100
482
  output << conf_files.sort.map { |f| "'#{File.basename(f)}'" }.join(', ')
101
483
  output << " in '#{File.dirname(vmx_path)}')"
102
- Fission.ui.output_and_exit output, 1
484
+ response.message = output
103
485
  end
104
486
  end
487
+
488
+ response.data.gsub! ' ', '\ ' if response.successful?
489
+
490
+ response
105
491
  end
106
492
 
493
+ # Public: Provides the expected path to a VM's directory. This does not
494
+ # imply that the VM or path exists.
495
+ #
496
+ # name - The name of the VM to provide the path for.
497
+ #
498
+ # Examples
499
+ #
500
+ # @vm.path
501
+ # # => '/vm/foo.vmwarevm'
502
+ #
503
+ # Returns the path (String) to the VM's directory.
504
+ def path
505
+ File.join Fission.config['vm_dir'], "#{@name}.vmwarevm"
506
+ end
507
+
508
+ # Public: Provides all of the VMs which are located in the VM directory.
509
+ #
510
+ # Examples
511
+ #
512
+ # Fission::VM.all.data
513
+ # # => [<Fission::VM:0x007fd6fa24c5d8 @name="foo">,
514
+ # <Fission::VM:0x007fd6fa23c5e8 @name="bar">]
515
+ #
516
+ # Returns a Response with the result.
517
+ # If successful, the Response's data attribute will be an Array of VM
518
+ # objects. If no VMs are found, the Response's data attribute will be an
519
+ # empty Array.
520
+ # If there is an error, an unsuccessful Response will be returned.
107
521
  def self.all
108
- vm_dirs = Dir[File.join Fission.config.attributes['vm_dir'], '*.vmwarevm'].select do |d|
522
+ vm_dirs = Dir[File.join Fission.config['vm_dir'], '*.vmwarevm'].select do |d|
109
523
  File.directory? d
110
524
  end
111
525
 
112
- vm_dirs.map { |d| File.basename d, '.vmwarevm' }
526
+ response = Response.new :code => 0
527
+ response.data = vm_dirs.collect { |d| new(File.basename d, '.vmwarevm') }
528
+
529
+ response
113
530
  end
114
531
 
532
+ # Public: Provides all of the VMs which are currently running.
533
+ #
534
+ # Examples
535
+ #
536
+ # Fission::VM.all_running.data
537
+ # # => [<Fission::VM:0x007fd6fa24c5d8 @name="foo">,
538
+ # <Fission::VM:0x007fd6fa23c5e8 @name="bar">]
539
+ #
540
+ # Returns a Response with the result.
541
+ # If successful, the Response's data attribute will be an Array of VM
542
+ # objects which are running. If no VMs are running, the Response's data
543
+ # attribute will be an empty Array.
544
+ # If there is an error, an unsuccessful Response will be returned.
115
545
  def self.all_running
116
- command = "#{Fission.config.attributes['vmrun_cmd']} list"
546
+ command = "#{Fission.config['vmrun_cmd']} list"
117
547
 
118
548
  output = `#{command}`
119
549
 
120
- if $?.exitstatus == 0
550
+ response = Response.new :code => $?.exitstatus
551
+
552
+ if response.successful?
121
553
  vms = output.split("\n").select do |vm|
122
554
  vm.include?('.vmx') && File.exists?(vm) && File.extname(vm) == '.vmx'
123
555
  end
124
556
 
125
- vms.map { |vm| File.basename(File.dirname(vm), '.vmwarevm') }
557
+ response.data = vms.collect do |vm|
558
+ new File.basename(File.dirname(vm), '.vmwarevm')
559
+ end
126
560
  else
127
- Fission.ui.output_and_exit "Unable to determine the list of running VMs", 1
561
+ response.message = output
128
562
  end
129
- end
130
563
 
131
- def self.exists?(vm_name)
132
- File.directory? path(vm_name)
564
+ response
133
565
  end
134
566
 
135
- def self.path(vm_name)
136
- File.join Fission.config.attributes['vm_dir'], "#{vm_name}.vmwarevm"
137
- end
567
+ # Public: Creates a new VM which is a clone of an existing VM. As Fusion
568
+ # doesn't provide a native cloning mechanism, this is a best effort. This
569
+ # essentially is a directory copy with updates to relevant files. It's
570
+ # recommended to clone VMs which are not running.
571
+ #
572
+ # source_vm_name - The name of the VM to clone.
573
+ # target_vm_name - The name of the VM to be created.
574
+ #
575
+ # Examples
576
+ #
577
+ # Fission::VM.clone 'foo', 'bar'
578
+ #
579
+ # Returns a Response with the result.
580
+ # If successful, the Response's data attribute will be nil.
581
+ # If there is an error, an unsuccessful Response will be returned.
582
+ def self.clone(source_vm_name, target_vm_name)
583
+ source_vm = new source_vm_name
584
+ target_vm = new target_vm_name
585
+
586
+ unless source_vm.exists?
587
+ return Response.new :code => 1, :message => 'VM does not exist'
588
+ end
589
+
590
+ if target_vm.exists?
591
+ return Response.new :code => 1, :message => 'VM already exists'
592
+ end
138
593
 
139
- def self.clone(source_vm, target_vm)
140
- Fission.ui.output "Cloning #{source_vm} to #{target_vm}"
141
- FileUtils.cp_r path(source_vm), path(target_vm)
594
+ FileUtils.cp_r source_vm.path, target_vm.path
142
595
 
143
- Fission.ui.output "Configuring #{target_vm}"
144
- rename_vm_files source_vm, target_vm
145
- update_config source_vm, target_vm
596
+ rename_vm_files source_vm.name, target_vm.name
597
+ update_config source_vm.name, target_vm.name
598
+
599
+ Response.new :code => 0
146
600
  end
147
601
 
148
- def self.delete(vm_name)
149
- Fission.ui.output "Deleting vm #{vm_name}"
150
- FileUtils.rm_rf path(vm_name)
151
- Fission::Metadata.delete_vm_info(path(vm_name))
602
+ # Public: Deletes a VM. The VM must not be running in order to delete it.
603
+ # As there are a number issues with the Fusion command line tool for
604
+ # deleting VMs, this is a best effort. The VM must not be running when this
605
+ # method is called. This essentially deletes the VM directory and attempts
606
+ # to remove the relevant entries from the Fusion plist file. It's highly
607
+ # recommended to delete VMs without the Fusion GUI running. If the Fusion
608
+ # GUI is running this method should succeed, but it's been observed that
609
+ # Fusion will recreate the plist data which is deleted. This leads to
610
+ # 'missing' VMs in the Fusion GUI.
611
+ #
612
+ # Examples
613
+ #
614
+ # @vm.delete
615
+ #
616
+ # Returns a Response with the result.
617
+ # If successful, the Response's data attribute will be nil.
618
+ # If there is an error, an unsuccessful Response will be returned.
619
+ def delete
620
+ unless exists?
621
+ return Response.new :code => 1, :message => 'VM does not exist'
622
+ end
623
+
624
+ running_response = running?
625
+ return running_response unless running_response.successful?
626
+
627
+ if running_response.data
628
+ message = 'The VM must not be running in order to delete it.'
629
+ return Response.new :code => 1, :message => message
630
+ end
631
+
632
+ FileUtils.rm_rf path
633
+ Metadata.delete_vm_info path
634
+
635
+ Response.new :code => 0
152
636
  end
153
637
 
154
638
  private
639
+ # Internal: Renames the files of a newly cloned VM.
640
+ #
641
+ # from - The VM name that was used as the source of the clone.
642
+ # to - The name of the newly cloned VM.
643
+ #
644
+ # Examples
645
+ #
646
+ # Fission::VM.rename_vm_files 'foo', 'bar'
647
+ #
648
+ # Returns nothing.
155
649
  def self.rename_vm_files(from, to)
650
+ to_vm = new to
651
+
156
652
  files_to_rename(from, to).each do |file|
157
653
  text_to_replace = File.basename(file, File.extname(file))
158
654
 
@@ -162,18 +658,34 @@ module Fission
162
658
  end
163
659
  end
164
660
 
165
- unless File.exists?(File.join(path(to), file.gsub(text_to_replace, to)))
166
- FileUtils.mv File.join(path(to), file),
167
- File.join(path(to), file.gsub(text_to_replace, to))
661
+ unless File.exists?(File.join(to_vm.path, file.gsub(text_to_replace, to)))
662
+ FileUtils.mv File.join(to_vm.path, file),
663
+ File.join(to_vm.path, file.gsub(text_to_replace, to))
168
664
  end
169
665
  end
170
666
  end
171
667
 
668
+ # Internal: Provides the list of files which need to be renamed in a newly
669
+ # cloned VM directory.
670
+ #
671
+ # from - The VM name that was used as the source of the clone.
672
+ # to - The name of the newly cloned VM.
673
+ #
674
+ # Examples
675
+ #
676
+ # Fission::VM.files_to_rename 'foo', 'bar'
677
+ # # => ['/vms/vm1/foo.vmdk', '/vms/vm1/foo.vmx', 'vms/vm1/blah.other']
678
+ #
679
+ # Returns an Array containing the paths (String) to the files to rename.
680
+ # The paths which match the from name will preceed any other files found in
681
+ # the newly cloned VM directory.
172
682
  def self.files_to_rename(from, to)
683
+ to_vm = new to
684
+
173
685
  files_which_match_source_vm = []
174
686
  other_files = []
175
687
 
176
- Dir.entries(path(to)).each do |f|
688
+ Dir.entries(to_vm.path).each do |f|
177
689
  unless f == '.' || f == '..'
178
690
  f.include?(from) ? files_which_match_source_vm << f : other_files << f
179
691
  end
@@ -182,19 +694,85 @@ module Fission
182
694
  files_which_match_source_vm + other_files
183
695
  end
184
696
 
697
+ # Internal: Provides the list of file extensions for VM related files.
698
+ #
699
+ # Examples
700
+ #
701
+ # Fission::VM.vm_file_extension
702
+ # # => ['.nvram', '.vmdk', '.vmem']
703
+ #
704
+ # Returns an Array containing the file extensions of VM realted files.
705
+ # The file extensions returned are Strings and include a '.'.
185
706
  def self.vm_file_extensions
186
707
  ['.nvram', '.vmdk', '.vmem', '.vmsd', '.vmss', '.vmx', '.vmxf']
187
708
  end
188
709
 
710
+ # Internal: Updates config files for a newly cloned VM. This will update any
711
+ # files with the extension of '.vmx', '.vmxf', and '.vmdk'. Any binary
712
+ # '.vmdk' files will be skipped.
713
+ #
714
+ # from - The VM name that was used as the source of the clone.
715
+ # to - The name of the newly cloned VM.
716
+ #
717
+ # Examples
718
+ #
719
+ # Fission::VM.update_config 'foo', 'bar'
720
+ #
721
+ # Returns nothing.
189
722
  def self.update_config(from, to)
723
+ to_vm = new to
724
+
190
725
  ['.vmx', '.vmxf', '.vmdk'].each do |ext|
191
- file = File.join path(to), "#{to}#{ext}"
726
+ file = File.join to_vm.path, "#{to}#{ext}"
192
727
 
193
728
  unless File.binary?(file)
194
729
  text = (File.read file).gsub from, to
195
730
  File.open(file, 'w'){ |f| f.print text }
196
731
  end
732
+
733
+ clean_up_conf_file(file) if ext == '.vmx'
734
+ end
735
+ end
736
+
737
+ # Internal: Cleans up the conf file (*.vmx) for a newly cloned VM. This
738
+ # includes removing generated MAC addresses, setting up for a new UUID, and
739
+ # disable VMware tools warning.
740
+ #
741
+ # conf_file_path - Aboslute path to the VM's conf file (.vmx).
742
+ #
743
+ # Examples
744
+ #
745
+ # VM.clean_up_conf_file '/vms/foo/foo.vmx'
746
+ #
747
+ # Returns nothing.
748
+ def self.clean_up_conf_file(conf_file_path)
749
+ conf_items_patterns = [ /^tools\.remindInstall.*\n/,
750
+ /^uuid\.action.*\n/,
751
+ /^ethernet\.+generatedAddress.*\n/ ]
752
+
753
+ content = File.read conf_file_path
754
+
755
+ conf_items_patterns.each do |pattern|
756
+ content.gsub(pattern, '').strip
197
757
  end
758
+
759
+ content << "\n"
760
+ content << "tools.remindInstall = \"FALSE\"\n"
761
+ content << "uuid.action = \"create\"\n"
762
+
763
+ File.open(conf_file_path, 'w') { |f| f.print content }
764
+ end
765
+
766
+ # Internal: Helper for getting the configured vmrun_cmd value.
767
+ #
768
+ # Examples
769
+ #
770
+ # @vm.vmrun_cmd
771
+ # # => "/foo/bar/vmrun -T fusion"
772
+ #
773
+ # Returns a String for the configured value of Fission.config['vmrun_cmd'].
774
+ def vmrun_cmd
775
+ Fission.config['vmrun_cmd']
198
776
  end
199
777
 
200
778
  end