bosh_warden_cpi 1.5.0.pre.3 → 1.2513.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,107 @@
1
+ module Bosh::WardenCloud
2
+ class DiskUtils
3
+
4
+ include Bosh::WardenCloud::Helpers
5
+
6
+ UMOUNT_GUARD_RETRIES = 60
7
+ UMOUNT_GUARD_SLEEP = 3
8
+
9
+ def initialize(disk_root, stemcell_root, fs_type)
10
+ @disk_root = disk_root
11
+ @fs_type = fs_type
12
+ @stemcell_root = stemcell_root
13
+
14
+ unless Dir.exist?(disk_root)
15
+ FileUtils.mkdir_p(disk_root)
16
+ end
17
+ end
18
+
19
+ def create_disk(disk_id, size)
20
+ raise ArgumentError, 'disk size <= 0' unless size > 0
21
+
22
+ image_file = image_path(disk_id)
23
+ FileUtils.touch(image_file)
24
+ File.truncate(image_file, size << 20) # 1 MB == 1<<20 Byte
25
+ sh "/sbin/mkfs -t #{@fs_type} -F #{image_file} 2>&1"
26
+
27
+ rescue => e
28
+ if image_file
29
+ FileUtils.rm_f image_file if File.exist?(image_file)
30
+ end
31
+ raise e
32
+ end
33
+
34
+ def delete_disk(disk_id)
35
+ FileUtils.rm_f image_path(disk_id)
36
+ end
37
+
38
+ def disk_exist?(disk_id)
39
+ File.exist?(image_path(disk_id))
40
+ end
41
+
42
+ def mount_disk(path, disk_id)
43
+ unless Dir.exist?(path)
44
+ FileUtils.mkdir_p(path)
45
+ end
46
+
47
+ disk_img = image_path(disk_id)
48
+ sudo "mount #{disk_img} #{path} -o loop"
49
+ end
50
+
51
+ def umount_disk(path)
52
+ umount_guard path
53
+ end
54
+
55
+ def stemcell_unpack(image_path, stemcell_id)
56
+ stemcell_dir = stemcell_path(stemcell_id)
57
+ unless Dir.exist?(stemcell_dir)
58
+ FileUtils.mkdir_p(stemcell_dir)
59
+ end
60
+ raise "#{image_path} not exist for creating stemcell" unless File.exist?(image_path)
61
+ sudo "tar -C #{stemcell_dir} -xzf #{image_path} 2>&1"
62
+ rescue => e
63
+ sudo "rm -rf #{stemcell_dir}"
64
+ raise e
65
+ end
66
+
67
+ def stemcell_delete(stemcell_id)
68
+ stemcell_dir = stemcell_path(stemcell_id)
69
+ sudo "rm -rf #{stemcell_dir}"
70
+ end
71
+
72
+ def image_path(disk_id)
73
+ File.join(@disk_root, "#{disk_id}.img")
74
+ end
75
+
76
+ def stemcell_path(stemcell_id)
77
+ File.join(@stemcell_root, stemcell_id)
78
+ end
79
+
80
+ private
81
+
82
+ def mount_entry(partition)
83
+ `mount`.lines.select { |l| l.match(/#{partition}/) }.first
84
+ end
85
+
86
+ # Retry the umount for GUARD_RETRIES +1 times
87
+ def umount_guard(mountpoint)
88
+ umount_attempts = UMOUNT_GUARD_RETRIES
89
+
90
+ loop do
91
+ return if mount_entry(mountpoint).nil?
92
+ sudo "umount #{mountpoint}" do |result|
93
+ if result.success?
94
+ return
95
+ elsif umount_attempts != 0
96
+ sleep UMOUNT_GUARD_SLEEP
97
+ umount_attempts -= 1
98
+ else
99
+ raise "Failed to umount #{mountpoint}: #{result.output}"
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ end
106
+
107
+ end
@@ -2,58 +2,105 @@ module Bosh::WardenCloud
2
2
 
3
3
  module Helpers
4
4
 
5
+ DEFAULT_SETTINGS_FILE = '/var/vcap/bosh/settings.json'
6
+
5
7
  def cloud_error(message)
6
8
  @logger.error(message) if @logger
7
9
  raise Bosh::Clouds::CloudError, message
8
10
  end
9
11
 
10
- def uuid(klass=nil)
12
+ def uuid(klass = nil)
11
13
  id = SecureRandom.uuid
12
-
13
14
  if klass
14
- id = "%s-%s" % [klass, id]
15
+ id = sprintf('%s-%s', klass, id)
15
16
  end
16
-
17
17
  id
18
18
  end
19
19
 
20
-
21
20
  def sudo(cmd)
22
- logger.info "run 'sudo -n #{cmd}'"
23
- Bosh::Exec.sh "sudo -n #{cmd}"
21
+ sh(cmd, true)
22
+ end
23
+
24
+ def sh(cmd, su = false)
25
+ runcmd = su == true ? "sudo -n #{cmd}" : cmd
26
+ @logger.info "run '#{runcmd}'" if @logger
27
+ Bosh::Exec.sh("#{runcmd}", yield: :on_false) do |result|
28
+ yield result if block_given?
29
+ end
30
+ end
31
+
32
+ def with_warden
33
+ client = Warden::Client.new(@warden_unix_path)
34
+ client.connect
35
+ ret = yield client
36
+ ret
37
+ ensure
38
+ client.disconnect if client
39
+ end
40
+
41
+ def agent_settings_file
42
+ DEFAULT_SETTINGS_FILE
24
43
  end
25
44
 
26
- def sh(cmd)
27
- logger.info "run '#{cmd}'"
28
- Bosh::Exec.sh "#{cmd}"
45
+ def generate_agent_env(vm_id, agent_id, networks, environment)
46
+ vm_env = {
47
+ 'name' => vm_id,
48
+ 'id' => vm_id
49
+ }
50
+
51
+ env = {
52
+ 'vm' => vm_env,
53
+ 'agent_id' => agent_id,
54
+ 'networks' => networks,
55
+ 'disks' => { 'persistent' => {} },
56
+ }
57
+ env['env'] = environment if environment
58
+ env.merge!(@agent_properties)
59
+ env
29
60
  end
30
61
 
31
- ##
32
- # This method generates a script that is run inside a container, to get an
33
- # available device path.
34
- #
35
- # This is hacky. The attached device is already formatted. In order to trick
36
- # bosh agent not to format the disk again, we touch an empty device file and
37
- # mknod the real partition file that is already formatted. Bosh agent will
38
- # mount skip the format process and directly mount the partition file.
39
- #
40
- # e.g.
41
- # Device file is like /dev/sda
42
- # Partition file is like /dev/sda1
43
- def attach_script(device_number, device_prefix)
44
- script = <<-EOF
45
- for i in a b c d e f g h; do (stat #{device_prefix}${i} > /dev/null 2>&1) || break; done
46
- touch #{device_prefix}${i}
47
- mknod #{device_prefix}${i}1 b 7 #{device_number} > /dev/null 2>&1 && echo "#{device_prefix}${i}"
48
- EOF
62
+ def get_agent_env(handle)
63
+ body = with_warden do |client|
64
+ request = Warden::Protocol::RunRequest.new
65
+ request.handle = handle
66
+ request.privileged = true
67
+ request.script = "cat #{agent_settings_file}"
68
+ client.call(request).stdout
69
+ end
70
+ env = Yajl::Parser.parse(body)
71
+ env
49
72
  end
50
73
 
51
- def partition_path(device_path)
52
- "#{device_path}1"
74
+ def set_agent_env(handle, env)
75
+ tempfile = File.new("/tmp/agent-setting-#{Time.now.to_f}-#{Kernel.rand(100_000)}",'w')
76
+ tempfile.write(Yajl::Encoder.encode(env))
77
+ tempfile_in = "/tmp/#{Kernel.rand(100_000)}"
78
+ tempfile.close
79
+ # Here we copy the setting file to temp file in container, then mv it to
80
+ # /var/vcap/bosh by privileged user.
81
+ with_warden do |client|
82
+ request = Warden::Protocol::CopyInRequest.new
83
+ request.handle = handle
84
+ request.src_path = tempfile.path
85
+ request.dst_path = tempfile_in
86
+ client.call(request)
87
+
88
+ request = Warden::Protocol::RunRequest.new
89
+ request.handle = handle
90
+ request.privileged = true
91
+ request.script = "mv #{tempfile_in} #{agent_settings_file}"
92
+ client.call(request)
93
+ end
53
94
  end
54
95
 
55
- def process_user
56
- Etc.getpwuid(Process.uid).name
96
+ def start_agent(handle)
97
+ with_warden do |client|
98
+ request = Warden::Protocol::SpawnRequest.new
99
+ request.handle = handle
100
+ request.privileged = true
101
+ request.script = '/usr/sbin/runsvdir-start'
102
+ client.call(request)
103
+ end
57
104
  end
58
105
 
59
106
  end
@@ -1,7 +1,5 @@
1
1
  module Bosh
2
- module Clouds
3
- class Warden
4
- VERSION = '1.5.0.pre.3'
5
- end
2
+ module WardenCloud
3
+ VERSION = '1.2513.0'
6
4
  end
7
5
  end
data/lib/cloud/warden.rb CHANGED
@@ -1,40 +1,22 @@
1
- require "yajl"
2
- require "sequel"
3
- require "fileutils"
4
- require "tempfile"
5
- require "securerandom"
6
- require "etc"
1
+ require 'yajl'
2
+ require 'fileutils'
3
+ require 'tempfile'
4
+ require 'securerandom'
7
5
 
8
- require "common/exec"
9
- require "common/thread_pool"
10
- require "common/thread_formatter"
6
+ require 'common/exec'
7
+ require 'common/thread_pool'
8
+ require 'common/thread_formatter'
11
9
 
12
- require "cloud"
13
- require "cloud/warden/helpers"
14
- require "cloud/warden/device_pool"
15
- require "cloud/warden/cloud"
16
- require "cloud/warden/version"
17
- require "cloud/warden/models/vm"
18
- require "cloud/warden/models/disk"
10
+ require 'cloud'
11
+ require 'cloud/warden/helpers'
12
+ require 'cloud/warden/cloud'
13
+ require 'cloud/warden/diskutils'
14
+ require 'cloud/warden/version'
19
15
 
20
- require "warden/client"
16
+ require 'warden/client'
21
17
 
22
18
  module Bosh
23
19
  module Clouds
24
- class Warden
25
- extend Forwardable
26
-
27
- def_delegators :@delegate,
28
- :create_stemcell, :delete_stemcell,
29
- :create_vm, :delete_vm, :reboot_vm,
30
- :configure_networks,
31
- :create_disk, :delete_disk,
32
- :attach_disk, :detach_disk,
33
- :validate_deployment
34
-
35
- def initialize(options)
36
- @delegate = WardenCloud::Cloud.new(options)
37
- end
38
- end
20
+ Warden = Bosh::WardenCloud::Cloud
39
21
  end
40
22
  end
@@ -0,0 +1,20 @@
1
+ require 'rspec'
2
+ require 'logger'
3
+ require 'tmpdir'
4
+ require 'cloud'
5
+ require 'cloud/warden'
6
+
7
+ def asset(file)
8
+ File.join(File.dirname(__FILE__), 'assets', file)
9
+ end
10
+
11
+ def mock_sh (cmd, su = false, times = 1, success = true)
12
+ zero_exit_status = double('Process::Status', exit_status: 0)
13
+ result = double('Result', :success? => success)
14
+ prefix = (su == true)? 'sudo -n ' : ''
15
+ expect(Bosh::Exec).to receive(:sh).exactly(times).times.with(/#{prefix}#{cmd}/, yield: :on_false).and_yield(result).and_return(zero_exit_status)
16
+ end
17
+
18
+ RSpec.configure do |conf|
19
+ conf.before(:each) { allow(Bosh::Clouds::Config).to receive(:logger).and_return(double.as_null_object) }
20
+ end
@@ -0,0 +1,264 @@
1
+ require 'spec_helper'
2
+
3
+ describe Bosh::WardenCloud::Cloud do
4
+
5
+ DEFAULT_HANDLE = 'vm-uuid-1234'
6
+ DEFAULT_AGENT_ID = 'agent-abcd'
7
+ DEFAULT_STEMCELL_ID = 'stemcell-uuid'
8
+
9
+ before :each do
10
+ @logger = Bosh::Clouds::Config.logger
11
+ @disk_root = Dir.mktmpdir('warden-cpi-disk')
12
+ @stemcell_path = Dir.mktmpdir('stemcell-disk')
13
+ @stemcell_root = File.join(@stemcell_path, DEFAULT_STEMCELL_ID)
14
+ @disk_util = double('DiskUtils')
15
+ @warden_client = double(Warden::Client)
16
+ allow(@disk_util).to receive(:stemcell_path).and_return(@stemcell_root)
17
+ allow(Bosh::WardenCloud::DiskUtils).to receive(:new).with(@disk_root, @stemcell_path, 'ext4').and_return(@disk_util)
18
+ allow(Warden::Client).to receive(:new).and_return(@warden_client)
19
+
20
+ cloud_options = {
21
+ 'disk' => {
22
+ 'root' => @disk_root,
23
+ 'fs' => 'ext4',
24
+ },
25
+ 'stemcell' => {
26
+ 'root' => @stemcell_path,
27
+ },
28
+ 'agent' => {
29
+ 'mbus' => 'nats://nats:nats@192.168.50.4:21084',
30
+ 'blobstore' => {
31
+ 'provider' => 'simple',
32
+ 'options' => 'option'
33
+ } ,
34
+ 'ntp' => []
35
+ }
36
+ }
37
+ @cloud = Bosh::Clouds::Provider.create(:warden, cloud_options)
38
+
39
+ allow(@warden_client).to receive(:connect) {}
40
+ allow(@warden_client).to receive(:disconnect) {}
41
+ end
42
+
43
+ after :each do
44
+ FileUtils.rm_rf @stemcell_path
45
+ FileUtils.rm_rf @disk_root
46
+ end
47
+
48
+ context 'initialize' do
49
+ it 'can be created using Bosh::Clouds::Provider' do
50
+ expect(@cloud).to be_an_instance_of(Bosh::Clouds::Warden)
51
+ end
52
+ end
53
+
54
+ context 'create_vm' do
55
+ before :each do
56
+ allow(@cloud).to receive(:uuid).with('vm') { DEFAULT_HANDLE }
57
+ Dir.mkdir(@stemcell_root)
58
+ allow(@warden_client).to receive(:call) do |req|
59
+ res = req.create_response
60
+ case req
61
+ when Warden::Protocol::CreateRequest
62
+ expect(req.network).to eq('1.1.1.1')
63
+ expect(req.rootfs).to equal(@stemcell_root)
64
+ expect(req.bind_mounts[0].src_path).to match(/#{@disk_root}\/bind_mount_points/)
65
+ expect(req.bind_mounts[1].src_path).to match(/#{@disk_root}\/ephemeral_mount_point/)
66
+ expect(req.bind_mounts[0].dst_path).to eq('/warden-cpi-dev')
67
+ expect(req.bind_mounts[1].dst_path).to eq('/var/vcap/data')
68
+ expect(req.bind_mounts[0].mode).to eq(Warden::Protocol::CreateRequest::BindMount::Mode::RW)
69
+ expect(req.bind_mounts[1].mode).to eq(Warden::Protocol::CreateRequest::BindMount::Mode::RW)
70
+ res.handle = DEFAULT_HANDLE
71
+ when Warden::Protocol::CopyInRequest
72
+ raise 'Container not found' unless req.handle == DEFAULT_HANDLE
73
+ env = Yajl::Parser.parse(File.read(req.src_path))
74
+ expect(env['agent_id']).to eq(DEFAULT_AGENT_ID)
75
+ expect(env['vm']['name']).not_to be_nil
76
+ expect(env['vm']['id']).not_to be_nil
77
+ expect(env['mbus']).not_to be_nil
78
+ expect(env['ntp']).to be_an_instance_of Array
79
+ expect(env['blobstore']).to be_an_instance_of Hash
80
+ when Warden::Protocol::RunRequest
81
+ # Ignore
82
+ when Warden::Protocol::SpawnRequest
83
+ expect(req.script).to eq('/usr/sbin/runsvdir-start')
84
+ expect(req.privileged).to be true
85
+ when Warden::Protocol::DestroyRequest
86
+ expect(req.handle).to eq(DEFAULT_HANDLE)
87
+ @destroy_called = true
88
+ else
89
+ raise "#{req} not supported"
90
+ end
91
+ res
92
+ end
93
+ end
94
+
95
+ it 'can create vm' do
96
+ expect(@cloud).to receive(:sudo).exactly(3)
97
+ network_spec = {
98
+ 'nic1' => { 'ip' => '1.1.1.1', 'type' => 'static' },
99
+ }
100
+ @cloud.create_vm(DEFAULT_AGENT_ID, DEFAULT_STEMCELL_ID, nil, network_spec)
101
+ end
102
+
103
+ it 'should raise error for invalid stemcell' do
104
+ allow(@disk_util).to receive(:stemcell_path).and_return('invalid_dir')
105
+ expect {
106
+ @cloud.create_vm('agent_id', 'invalid_stemcell_id', nil, {})
107
+ }.to raise_error Bosh::Clouds::CloudError
108
+ end
109
+
110
+ it 'should raise error for more than 1 nics' do
111
+ expect {
112
+ network_spec = {
113
+ 'nic1' => { 'ip' => '1.1.1.1', 'type' => 'static' },
114
+ 'nic2' => { 'type' => 'dynamic' },
115
+ }
116
+ @cloud.create_vm('agent_id', 'invalid_stemcell_id', nil, network_spec)
117
+ }.to raise_error ArgumentError
118
+ end
119
+
120
+ it 'should clean up when an error raised' do
121
+ class FakeError < StandardError; end
122
+ allow(@cloud).to receive(:sudo) {}
123
+ allow(@cloud).to receive(:set_agent_env) { raise FakeError.new }
124
+ network_spec = {
125
+ 'nic1' => { 'ip' => '1.1.1.1', 'type' => 'static' },
126
+ }
127
+ begin
128
+ @cloud.create_vm(DEFAULT_AGENT_ID, DEFAULT_STEMCELL_ID, nil, network_spec)
129
+ rescue FakeError
130
+ else
131
+ raise 'Expected FakeError'
132
+ end
133
+ expect(@destroy_called).to be true
134
+ end
135
+ end
136
+
137
+ context 'delete_vm' do
138
+ it 'can delete vm' do
139
+ allow(@warden_client).to receive(:call) do |req|
140
+ res = req.create_response
141
+ case req
142
+ when Warden::Protocol::DestroyRequest
143
+ expect(req.handle).to eq(DEFAULT_HANDLE)
144
+ when Warden::Protocol::ListRequest
145
+ res.handles = DEFAULT_HANDLE
146
+ else
147
+ raise "#{req} not supported"
148
+ end
149
+ res
150
+ end
151
+ mock_sh("umount #{@disk_root}/bind_mount_points/#{DEFAULT_HANDLE}", true)
152
+ mock_sh("rm -rf #{@disk_root}/ephemeral_mount_point/#{DEFAULT_HANDLE}", true)
153
+ mock_sh("rm -rf #{@disk_root}/bind_mount_points/#{DEFAULT_HANDLE}", true)
154
+ @cloud.delete_vm(DEFAULT_HANDLE)
155
+ end
156
+
157
+ it 'should proceed even delete a vm which not exist' do
158
+ allow(@cloud).to receive(:has_vm?).with('vm_not_existed').and_return(false)
159
+ mock_sh("rm -rf #{@disk_root}/ephemeral_mount_point/vm_not_existed", true)
160
+ mock_sh("rm -rf #{@disk_root}/bind_mount_points/vm_not_existed", true)
161
+ expect {
162
+ @cloud.delete_vm('vm_not_existed')
163
+ }.to_not raise_error
164
+ end
165
+
166
+ it 'can delete a vm with disk attached' do
167
+ allow(@warden_client).to receive(:call) do |req|
168
+ res = req.create_response
169
+ case req
170
+ when Warden::Protocol::DestroyRequest
171
+ expect(req.handle).to eq(DEFAULT_HANDLE)
172
+ when Warden::Protocol::ListRequest
173
+ res.handles = DEFAULT_HANDLE
174
+ else
175
+ raise "#{req} not supported"
176
+ end
177
+ res
178
+ end
179
+ mock_sh("umount #{@disk_root}/bind_mount_points/#{DEFAULT_HANDLE}", true)
180
+ mock_sh("rm -rf #{@disk_root}/ephemeral_mount_point/#{DEFAULT_HANDLE}", true)
181
+ mock_sh("rm -rf #{@disk_root}/bind_mount_points/#{DEFAULT_HANDLE}", true)
182
+ @cloud.delete_vm(DEFAULT_HANDLE)
183
+ end
184
+ end
185
+
186
+ context 'has_vm' do
187
+ before :each do
188
+ allow(@warden_client).to receive(:call) do |req|
189
+ res = req.create_response
190
+ case req
191
+ when Warden::Protocol::ListRequest
192
+ res.handles = DEFAULT_HANDLE
193
+ else
194
+ raise "#{req} not supported"
195
+ end
196
+ res
197
+ end
198
+ end
199
+
200
+ it 'return true when container exist' do
201
+ expect(@cloud.has_vm?(DEFAULT_HANDLE)).to be true
202
+ end
203
+
204
+ it 'return false when container not exist' do
205
+ expect(@cloud.has_vm?('vm_not_exist')).to be false
206
+ end
207
+ end
208
+
209
+ context 'stemcells' do
210
+ before :each do
211
+ allow(@cloud).to receive(:uuid).with('stemcell') { DEFAULT_STEMCELL_ID }
212
+ end
213
+
214
+ it 'invoke disk_utils to create stemcell with uuid' do
215
+ expect(@disk_util).to receive(:stemcell_unpack).with('imgpath', DEFAULT_STEMCELL_ID)
216
+ @cloud.create_stemcell('imgpath', nil)
217
+ end
218
+
219
+ it 'invoke disk_utils to delete stemcell with uuid' do
220
+ expect(@disk_util).to receive(:stemcell_delete).with(DEFAULT_STEMCELL_ID)
221
+ @cloud.delete_stemcell(DEFAULT_STEMCELL_ID)
222
+ end
223
+ end
224
+
225
+ context 'disk create/delete/attach/detach' do
226
+ before :each do
227
+ allow(@cloud).to receive(:uuid).with('disk') { 'disk-uuid-1234' }
228
+ end
229
+
230
+ it 'invoke disk_utils to create disk with uuid' do
231
+ expect(@disk_util).to receive(:create_disk).with('disk-uuid-1234', 1024)
232
+ @cloud.create_disk(1024, nil)
233
+ end
234
+
235
+ it 'invoke disk_utils to delete disk with uuid' do
236
+ expect(@disk_util).to receive(:disk_exist?).with('disk-uuid-1234').and_return(true)
237
+ expect(@disk_util).to receive(:delete_disk).with('disk-uuid-1234')
238
+ @cloud.delete_disk('disk-uuid-1234')
239
+ end
240
+
241
+ it 'invoke disk_utils to mount disk and setup agent env when attach disk' do
242
+ allow(@cloud).to receive(:get_agent_env) { { 'disks' => { 'persistent' => {} } } }
243
+ expected_env = { 'disks' => { 'persistent' => { 'disk-uuid-1234' => '/warden-cpi-dev/disk-uuid-1234' } } }
244
+ expected_mountpoint = File.join(@disk_root, 'bind_mount_points', 'vm-uuid-1234', 'disk-uuid-1234')
245
+ expect(@disk_util).to receive(:disk_exist?).with('disk-uuid-1234').and_return(true)
246
+ expect(@disk_util).to receive(:mount_disk).with(expected_mountpoint, 'disk-uuid-1234')
247
+ expect(@cloud).to receive(:set_agent_env).with('vm-uuid-1234', expected_env)
248
+ expect(@cloud).to receive(:has_vm?).with('vm-uuid-1234').and_return(true)
249
+ @cloud.attach_disk('vm-uuid-1234', 'disk-uuid-1234')
250
+ end
251
+
252
+ it 'invoke disk_utils to umount disk and remove agent env when detach disk' do
253
+ allow(@cloud).to receive(:get_agent_env) { { 'disks' => { 'persistent' => { 'disk-uuid-1234' => '/warden-cpi-dev/disk-uuid-1234' } } } }
254
+ expected_env = { 'disks' => { 'persistent' => { 'disk-uuid-1234' => nil } } }
255
+ expected_mountpoint = File.join(@disk_root, 'bind_mount_points', 'vm-uuid-1234', 'disk-uuid-1234')
256
+ expect(@disk_util).to receive(:disk_exist?).with('disk-uuid-1234').and_return(true)
257
+ expect(@disk_util).to receive(:umount_disk).with(expected_mountpoint)
258
+ expect(@cloud).to receive(:set_agent_env).with('vm-uuid-1234', expected_env)
259
+ expect(@cloud).to receive(:has_vm?).with('vm-uuid-1234').and_return(true)
260
+ @cloud.detach_disk('vm-uuid-1234', 'disk-uuid-1234')
261
+ end
262
+ end
263
+
264
+ end