fission 0.3.0 → 0.4.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
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