vm_shepherd 1.10.1 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/vm_shepherd.rb +4 -0
- data/lib/vm_shepherd/data_object.rb +11 -0
- data/lib/vm_shepherd/shepherd.rb +2 -2
- data/lib/vm_shepherd/vcloud/deployer.rb +23 -0
- data/lib/vm_shepherd/vcloud/destroyer.rb +47 -0
- data/lib/vm_shepherd/vcloud/vapp_config.rb +78 -0
- data/lib/vm_shepherd/vcloud_manager.rb +15 -125
- data/lib/vm_shepherd/version.rb +1 -1
- data/spec/vm_shepherd/data_object_spec.rb +49 -0
- data/spec/vm_shepherd/shepherd_spec.rb +4 -4
- data/spec/vm_shepherd/vcloud/deployer_spec.rb +83 -0
- data/spec/vm_shepherd/vcloud/destroyer_spec.rb +72 -0
- data/spec/vm_shepherd/vcloud/vapp_config_spec.rb +25 -0
- data/spec/vm_shepherd/vcloud_manager_spec.rb +34 -43
- metadata +14 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 763cd128f7302c5fb7250c56c5fb6f2902659a7b
|
4
|
+
data.tar.gz: aac10b49759fcb9b8c5b385e8f9a3bb4fc6b32d6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b67e18537b886374536aaf33ec441b389c81ffeb283fcaff7ddff1c53899db708f9fceebc9b22ea90a70d29b810b56f04f6486d7689d17721cea9a8304247075
|
7
|
+
data.tar.gz: 7e0e37f43205234107f6350dde7dcbf3d6c409b4b25357fd486710b95504563cd7e3d825f7e5ab6365a8981b0913d9a99d21c1a5e4748b8c19a0ae72377ac1b1
|
data/lib/vm_shepherd.rb
CHANGED
@@ -1,7 +1,11 @@
|
|
1
1
|
require 'vm_shepherd/shepherd'
|
2
|
+
require 'vm_shepherd/data_object'
|
2
3
|
require 'vm_shepherd/aws_manager'
|
3
4
|
require 'vm_shepherd/openstack_manager'
|
4
5
|
require 'vm_shepherd/vcloud_manager'
|
6
|
+
require 'vm_shepherd/vcloud/deployer'
|
7
|
+
require 'vm_shepherd/vcloud/destroyer'
|
8
|
+
require 'vm_shepherd/vcloud/vapp_config'
|
5
9
|
require 'vm_shepherd/vsphere_manager'
|
6
10
|
|
7
11
|
module VmShepherd
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module VmShepherd
|
2
|
+
module DataObject
|
3
|
+
def ==(other_obj)
|
4
|
+
return false unless self.class === other_obj
|
5
|
+
|
6
|
+
instance_variables.all? do |ivar_name|
|
7
|
+
self.instance_variable_get(ivar_name) == other_obj.instance_variable_get(ivar_name)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
data/lib/vm_shepherd/shepherd.rb
CHANGED
@@ -184,7 +184,7 @@ module VmShepherd
|
|
184
184
|
|
185
185
|
def vcloud_deploy_options(vm_shepherd_config)
|
186
186
|
vm = vm_shepherd_config.vapp
|
187
|
-
|
187
|
+
VmShepherd::Vcloud::VappConfig.new(
|
188
188
|
name: vm.ops_manager_name,
|
189
189
|
ip: vm.ip,
|
190
190
|
gateway: vm.gateway,
|
@@ -193,7 +193,7 @@ module VmShepherd
|
|
193
193
|
ntp: vm.ntp,
|
194
194
|
catalog: vm_shepherd_config.vdc.catalog,
|
195
195
|
network: vm_shepherd_config.vdc.network,
|
196
|
-
|
196
|
+
)
|
197
197
|
end
|
198
198
|
|
199
199
|
def ami_manager
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module VmShepherd
|
2
|
+
module Vcloud
|
3
|
+
class Deployer
|
4
|
+
def self.deploy_and_power_on_vapp(client:, ovf_dir:, vapp_config:, vdc_name:)
|
5
|
+
catalog = client.create_catalog(vapp_config.catalog)
|
6
|
+
|
7
|
+
# upload template and instantiate vapp
|
8
|
+
catalog.upload_vapp_template(vdc_name, vapp_config.name, ovf_dir)
|
9
|
+
|
10
|
+
# instantiate template
|
11
|
+
network_config = VCloudSdk::NetworkConfig.new(vapp_config.network, 'Network 1')
|
12
|
+
vapp = catalog.instantiate_vapp_template(vapp_config.name, vdc_name, vapp_config.name, nil, nil, network_config)
|
13
|
+
|
14
|
+
# reconfigure vm
|
15
|
+
vm = vapp.find_vm_by_name(vapp_config.name)
|
16
|
+
vm.product_section_properties = vapp_config.build_properties
|
17
|
+
|
18
|
+
# power on vapp
|
19
|
+
vapp.power_on
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module VmShepherd
|
2
|
+
module Vcloud
|
3
|
+
class Destroyer
|
4
|
+
def initialize(client:, vdc_name:)
|
5
|
+
@client = client
|
6
|
+
@vdc_name = vdc_name
|
7
|
+
end
|
8
|
+
|
9
|
+
def delete_catalog_and_vapps(catalog, vapp_names, logger)
|
10
|
+
delete_vapps(vapp_names, logger)
|
11
|
+
delete_catalog(catalog)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def vdc
|
17
|
+
@vdc ||= @client.find_vdc_by_name(@vdc_name)
|
18
|
+
end
|
19
|
+
|
20
|
+
def delete_vapps(vapp_names, logger)
|
21
|
+
vapp_names.each do |vapp_name|
|
22
|
+
begin
|
23
|
+
delete_vapp(vapp_name)
|
24
|
+
rescue VCloudSdk::ObjectNotFoundError => e
|
25
|
+
logger.debug "Could not delete vapp '#{vapp_name}': #{e.inspect}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def delete_catalog(catalog)
|
31
|
+
@client.delete_catalog_by_name(catalog) if @client.catalog_exists?(catalog)
|
32
|
+
end
|
33
|
+
|
34
|
+
def delete_vapp(vapp_name)
|
35
|
+
vapp = vdc.find_vapp_by_name(vapp_name)
|
36
|
+
vapp.vms.map do |vm|
|
37
|
+
vm.independent_disks.map do |disk|
|
38
|
+
vm.detach_disk(disk)
|
39
|
+
vdc.delete_disk_by_name(disk.name)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
vapp.power_off
|
43
|
+
vapp.delete
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module VmShepherd
|
2
|
+
module Vcloud
|
3
|
+
class VappConfig
|
4
|
+
include VmShepherd::DataObject
|
5
|
+
attr_reader :name, :gateway, :dns, :ntp, :ip, :netmask, :catalog, :network
|
6
|
+
|
7
|
+
def initialize(name:, ip:, gateway:, netmask:, dns:, ntp:, catalog:, network:)
|
8
|
+
@name = name
|
9
|
+
@ip = ip
|
10
|
+
@gateway = gateway
|
11
|
+
@netmask = netmask
|
12
|
+
@dns = dns
|
13
|
+
@ntp = ntp
|
14
|
+
@catalog = catalog
|
15
|
+
@network = network
|
16
|
+
end
|
17
|
+
|
18
|
+
def build_properties
|
19
|
+
[
|
20
|
+
{
|
21
|
+
'type' => 'string',
|
22
|
+
'key' => 'gateway',
|
23
|
+
'value' => gateway,
|
24
|
+
'password' => 'false',
|
25
|
+
'userConfigurable' => 'true',
|
26
|
+
'Label' => 'Default Gateway',
|
27
|
+
'Description' => 'The default gateway address for the VM network. Leave blank if DHCP is desired.'
|
28
|
+
},
|
29
|
+
{
|
30
|
+
'type' => 'string',
|
31
|
+
'key' => 'DNS',
|
32
|
+
'value' => dns,
|
33
|
+
'password' => 'false',
|
34
|
+
'userConfigurable' => 'true',
|
35
|
+
'Label' => 'DNS',
|
36
|
+
'Description' => 'The domain name servers for the VM (comma separated). Leave blank if DHCP is desired.',
|
37
|
+
},
|
38
|
+
{
|
39
|
+
'type' => 'string',
|
40
|
+
'key' => 'ntp_servers',
|
41
|
+
'value' => ntp,
|
42
|
+
'password' => 'false',
|
43
|
+
'userConfigurable' => 'true',
|
44
|
+
'Label' => 'NTP Servers',
|
45
|
+
'Description' => 'Comma-delimited list of NTP servers'
|
46
|
+
},
|
47
|
+
{
|
48
|
+
'type' => 'string',
|
49
|
+
'key' => 'admin_password',
|
50
|
+
'value' => 'tempest',
|
51
|
+
'password' => 'true',
|
52
|
+
'userConfigurable' => 'true',
|
53
|
+
'Label' => 'Admin Password',
|
54
|
+
'Description' => 'This password is used to SSH into the VM. The username is "tempest".',
|
55
|
+
},
|
56
|
+
{
|
57
|
+
'type' => 'string',
|
58
|
+
'key' => 'ip0',
|
59
|
+
'value' => ip,
|
60
|
+
'password' => 'false',
|
61
|
+
'userConfigurable' => 'true',
|
62
|
+
'Label' => 'IP Address',
|
63
|
+
'Description' => 'The IP address for the VM. Leave blank if DHCP is desired.',
|
64
|
+
},
|
65
|
+
{
|
66
|
+
'type' => 'string',
|
67
|
+
'key' => 'netmask0',
|
68
|
+
'value' => netmask,
|
69
|
+
'password' => 'false',
|
70
|
+
'userConfigurable' => 'true',
|
71
|
+
'Label' => 'Netmask',
|
72
|
+
'Description' => 'The netmask for the VM network. Leave blank if DHCP is desired.'
|
73
|
+
}
|
74
|
+
]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -17,9 +17,15 @@ module VmShepherd
|
|
17
17
|
|
18
18
|
untar_vapp_template_tar(File.expand_path(vapp_template_tar_path), tmpdir)
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
VmShepherd::Vcloud::Deployer.deploy_and_power_on_vapp(
|
21
|
+
client: client,
|
22
|
+
ovf_dir: tmpdir,
|
23
|
+
vapp_config: vapp_config,
|
24
|
+
vdc_name: @vdc_name,
|
25
|
+
)
|
26
|
+
rescue => e
|
27
|
+
logger.error(e.http_body) if e.respond_to?(:http_body)
|
28
|
+
raise e
|
23
29
|
ensure
|
24
30
|
FileUtils.remove_entry_secure(tmpdir, force: true)
|
25
31
|
end
|
@@ -28,8 +34,8 @@ module VmShepherd
|
|
28
34
|
end
|
29
35
|
|
30
36
|
def destroy(vapp_names, catalog)
|
31
|
-
|
32
|
-
|
37
|
+
VmShepherd::Vcloud::Destroyer.new(client: client, vdc_name: @vdc_name).
|
38
|
+
delete_catalog_and_vapps(catalog, vapp_names, @logger)
|
33
39
|
end
|
34
40
|
|
35
41
|
def clean_environment(vapp_names, catalog)
|
@@ -40,14 +46,15 @@ module VmShepherd
|
|
40
46
|
|
41
47
|
def check_vapp_status(vapp_config)
|
42
48
|
log('Checking for existing VM') do
|
43
|
-
ip = vapp_config
|
44
|
-
|
49
|
+
ip = vapp_config.ip
|
50
|
+
system("ping -c 5 #{ip}") and raise "VM exists at #{ip}"
|
45
51
|
end
|
46
52
|
end
|
47
53
|
|
48
54
|
def untar_vapp_template_tar(vapp_template_tar_path, dir)
|
49
55
|
log("Untarring #{vapp_template_tar_path}") do
|
50
|
-
|
56
|
+
cmd = "cd #{dir} && tar xfv '#{vapp_template_tar_path}'"
|
57
|
+
system(cmd) or raise("Error executing: #{cmd}")
|
51
58
|
end
|
52
59
|
end
|
53
60
|
|
@@ -61,127 +68,10 @@ module VmShepherd
|
|
61
68
|
)
|
62
69
|
end
|
63
70
|
|
64
|
-
def deploy_vapp(ovf_dir, vapp_config)
|
65
|
-
vapp_name = vapp_config.fetch(:name)
|
66
|
-
catalog_name = vapp_config.fetch(:catalog)
|
67
|
-
network = vapp_config.fetch(:network)
|
68
|
-
# setup the catalog
|
69
|
-
client.delete_catalog_by_name(catalog_name) if client.catalog_exists?(catalog_name)
|
70
|
-
catalog = client.create_catalog(catalog_name)
|
71
|
-
|
72
|
-
# upload template and instantiate vapp
|
73
|
-
catalog.upload_vapp_template(@vdc_name, vapp_name, ovf_dir)
|
74
|
-
|
75
|
-
# instantiate template
|
76
|
-
network_config = VCloudSdk::NetworkConfig.new(network, 'Network 1')
|
77
|
-
catalog.instantiate_vapp_template(vapp_name, @vdc_name, vapp_name, nil, nil, network_config)
|
78
|
-
rescue => e
|
79
|
-
@logger.error(e.http_body) if e.respond_to?(:http_body)
|
80
|
-
raise e
|
81
|
-
end
|
82
|
-
|
83
|
-
def reconfigure_vm(vapp, vapp_config)
|
84
|
-
vapp_name = vapp_config.fetch(:name)
|
85
|
-
gateway = vapp_config.fetch(:gateway)
|
86
|
-
dns = vapp_config.fetch(:dns)
|
87
|
-
ntp = vapp_config.fetch(:ntp)
|
88
|
-
ip = vapp_config.fetch(:ip)
|
89
|
-
netmask = vapp_config.fetch(:netmask)
|
90
|
-
|
91
|
-
vm = vapp.find_vm_by_name(vapp_name)
|
92
|
-
vm.product_section_properties = build_properties(gateway: gateway, dns: dns, ntp: ntp, ip: ip, netmask: netmask)
|
93
|
-
vm
|
94
|
-
end
|
95
|
-
|
96
|
-
def build_properties(gateway:, dns:, ntp:, ip:, netmask:)
|
97
|
-
[
|
98
|
-
{
|
99
|
-
'type' => 'string',
|
100
|
-
'key' => 'gateway',
|
101
|
-
'value' => gateway,
|
102
|
-
'password' => 'false',
|
103
|
-
'userConfigurable' => 'true',
|
104
|
-
'Label' => 'Default Gateway',
|
105
|
-
'Description' => 'The default gateway address for the VM network. Leave blank if DHCP is desired.'
|
106
|
-
},
|
107
|
-
{
|
108
|
-
'type' => 'string',
|
109
|
-
'key' => 'DNS',
|
110
|
-
'value' => dns,
|
111
|
-
'password' => 'false',
|
112
|
-
'userConfigurable' => 'true',
|
113
|
-
'Label' => 'DNS',
|
114
|
-
'Description' => 'The domain name servers for the VM (comma separated). Leave blank if DHCP is desired.',
|
115
|
-
},
|
116
|
-
{
|
117
|
-
'type' => 'string',
|
118
|
-
'key' => 'ntp_servers',
|
119
|
-
'value' => ntp,
|
120
|
-
'password' => 'false',
|
121
|
-
'userConfigurable' => 'true',
|
122
|
-
'Label' => 'NTP Servers',
|
123
|
-
'Description' => 'Comma-delimited list of NTP servers'
|
124
|
-
},
|
125
|
-
{
|
126
|
-
'type' => 'string',
|
127
|
-
'key' => 'admin_password',
|
128
|
-
'value' => 'tempest',
|
129
|
-
'password' => 'true',
|
130
|
-
'userConfigurable' => 'true',
|
131
|
-
'Label' => 'Admin Password',
|
132
|
-
'Description' => 'This password is used to SSH into the VM. The username is "tempest".',
|
133
|
-
},
|
134
|
-
{
|
135
|
-
'type' => 'string',
|
136
|
-
'key' => 'ip0',
|
137
|
-
'value' => ip,
|
138
|
-
'password' => 'false',
|
139
|
-
'userConfigurable' => 'true',
|
140
|
-
'Label' => 'IP Address',
|
141
|
-
'Description' => 'The IP address for the VM. Leave blank if DHCP is desired.',
|
142
|
-
},
|
143
|
-
{
|
144
|
-
'type' => 'string',
|
145
|
-
'key' => 'netmask0',
|
146
|
-
'value' => netmask,
|
147
|
-
'password' => 'false',
|
148
|
-
'userConfigurable' => 'true',
|
149
|
-
'Label' => 'Netmask',
|
150
|
-
'Description' => 'The netmask for the VM network. Leave blank if DHCP is desired.'
|
151
|
-
}
|
152
|
-
]
|
153
|
-
end
|
154
|
-
|
155
71
|
def log(title, &blk)
|
156
72
|
@logger.debug "--- Begin: #{title.inspect} @ #{DateTime.now}"
|
157
73
|
blk.call
|
158
74
|
@logger.debug "--- End: #{title.inspect} @ #{DateTime.now}"
|
159
75
|
end
|
160
|
-
|
161
|
-
def system_or_exit(command)
|
162
|
-
log(command) do
|
163
|
-
system(command) || raise("Error executing: #{command.inspect}")
|
164
|
-
end
|
165
|
-
end
|
166
|
-
|
167
|
-
def vdc
|
168
|
-
@vdc ||= client.find_vdc_by_name(@vdc_name)
|
169
|
-
end
|
170
|
-
|
171
|
-
def delete_vapps(vapp_names)
|
172
|
-
vapp_names.each do |vapp_name|
|
173
|
-
begin
|
174
|
-
vapp = vdc.find_vapp_by_name(vapp_name)
|
175
|
-
vapp.power_off
|
176
|
-
vapp.delete
|
177
|
-
rescue VCloudSdk::ObjectNotFoundError => e
|
178
|
-
@logger.debug "Could not delete vapp '#{vapp_name}': #{e.inspect}"
|
179
|
-
end
|
180
|
-
end
|
181
|
-
end
|
182
|
-
|
183
|
-
def delete_catalog(catalog)
|
184
|
-
client.delete_catalog_by_name(catalog) if client.catalog_exists?(catalog)
|
185
|
-
end
|
186
76
|
end
|
187
77
|
end
|
data/lib/vm_shepherd/version.rb
CHANGED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'vm_shepherd/data_object'
|
2
|
+
|
3
|
+
module VmShepherd
|
4
|
+
class TestDataObject
|
5
|
+
include DataObject
|
6
|
+
|
7
|
+
attr_accessor :name
|
8
|
+
end
|
9
|
+
|
10
|
+
RSpec.describe(DataObject) do
|
11
|
+
describe '#==' do
|
12
|
+
it 'returns false for objects of a different class' do
|
13
|
+
class DifferentDataObject
|
14
|
+
include DataObject
|
15
|
+
end
|
16
|
+
|
17
|
+
expect(TestDataObject.new == DifferentDataObject.new).to be_falsey
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'returns true for objects of a descendent class' do
|
21
|
+
class DescendentDataObject < TestDataObject
|
22
|
+
include DataObject
|
23
|
+
end
|
24
|
+
|
25
|
+
expect(TestDataObject.new == DescendentDataObject.new).to be_truthy
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'returns false when any attribute is unequal' do
|
29
|
+
a = TestDataObject.new
|
30
|
+
b = TestDataObject.new
|
31
|
+
|
32
|
+
a.name = 'a'
|
33
|
+
b.name = 'b'
|
34
|
+
|
35
|
+
expect(a == b).to be_falsey
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'returns true when all attributes are equal' do
|
39
|
+
eleventy_one = TestDataObject.new
|
40
|
+
hundred_and_eleven = TestDataObject.new
|
41
|
+
|
42
|
+
eleventy_one.name = '111'
|
43
|
+
hundred_and_eleven.name = '111'
|
44
|
+
|
45
|
+
expect(eleventy_one == hundred_and_eleven).to be_truthy
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -84,7 +84,7 @@ module VmShepherd
|
|
84
84
|
|
85
85
|
expect(first_vcloud_manager).to receive(:deploy).with(
|
86
86
|
'FIRST_FAKE_PATH',
|
87
|
-
|
87
|
+
Vcloud::VappConfig.new(
|
88
88
|
name: first_config.vapp.ops_manager_name,
|
89
89
|
ip: first_config.vapp.ip,
|
90
90
|
gateway: first_config.vapp.gateway,
|
@@ -93,12 +93,12 @@ module VmShepherd
|
|
93
93
|
ntp: first_config.vapp.ntp,
|
94
94
|
catalog: first_config.vdc.catalog,
|
95
95
|
network: first_config.vdc.network,
|
96
|
-
|
96
|
+
)
|
97
97
|
)
|
98
98
|
|
99
99
|
expect(last_vcloud_manager).to receive(:deploy).with(
|
100
100
|
'LAST_FAKE_PATH',
|
101
|
-
|
101
|
+
Vcloud::VappConfig.new(
|
102
102
|
name: last_config.vapp.ops_manager_name,
|
103
103
|
ip: last_config.vapp.ip,
|
104
104
|
gateway: last_config.vapp.gateway,
|
@@ -107,7 +107,7 @@ module VmShepherd
|
|
107
107
|
ntp: last_config.vapp.ntp,
|
108
108
|
catalog: last_config.vdc.catalog,
|
109
109
|
network: last_config.vdc.network,
|
110
|
-
|
110
|
+
)
|
111
111
|
)
|
112
112
|
|
113
113
|
manager.deploy(paths: ['FIRST_FAKE_PATH', 'LAST_FAKE_PATH'])
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'vm_shepherd/data_object'
|
2
|
+
require 'vm_shepherd/vcloud/vapp_config'
|
3
|
+
require 'vm_shepherd/vcloud/deployer'
|
4
|
+
require 'ruby_vcloud_sdk'
|
5
|
+
|
6
|
+
module VmShepherd
|
7
|
+
module Vcloud
|
8
|
+
RSpec.describe Deployer do
|
9
|
+
describe '.deploy_and_power_on_vapp' do
|
10
|
+
let(:vapp_config) do
|
11
|
+
VappConfig.new(
|
12
|
+
name: 'NAME',
|
13
|
+
ip: 'IP',
|
14
|
+
gateway: 'GATEWAY',
|
15
|
+
netmask: 'NETMASK',
|
16
|
+
dns: 'DNS',
|
17
|
+
ntp: 'NTP',
|
18
|
+
catalog: 'CATALOG',
|
19
|
+
network: 'NETWORK',
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
let(:client) { instance_double(VCloudSdk::Client) }
|
24
|
+
let(:catalog) { instance_double(VCloudSdk::Catalog) }
|
25
|
+
let(:vapp) { instance_double(VCloudSdk::VApp) }
|
26
|
+
let(:vm) { instance_double(VCloudSdk::VM) }
|
27
|
+
let(:network_config) { instance_double(VCloudSdk::NetworkConfig) }
|
28
|
+
|
29
|
+
before do
|
30
|
+
allow(client).to receive(:create_catalog).and_return(catalog)
|
31
|
+
allow(catalog).to receive(:upload_vapp_template)
|
32
|
+
allow(catalog).to receive(:instantiate_vapp_template).and_return(vapp)
|
33
|
+
allow(vapp).to receive(:find_vm_by_name).and_return(vm)
|
34
|
+
allow(vm).to receive(:product_section_properties=)
|
35
|
+
allow(vapp).to receive(:power_on)
|
36
|
+
|
37
|
+
allow(VCloudSdk::NetworkConfig).to receive(:new).with('NETWORK', 'Network 1').and_return(network_config)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'creates a catalog' do
|
41
|
+
expect(client).to receive(:create_catalog).with('CATALOG')
|
42
|
+
|
43
|
+
Deployer.deploy_and_power_on_vapp(client: client, ovf_dir: nil, vapp_config: vapp_config, vdc_name: nil)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'uploads a vapp template' do
|
47
|
+
expect(catalog).to receive(:upload_vapp_template).with('VDC_NAME', 'NAME', 'OVF_DIR')
|
48
|
+
|
49
|
+
Deployer.deploy_and_power_on_vapp(client: client, ovf_dir: 'OVF_DIR', vapp_config: vapp_config, vdc_name: 'VDC_NAME')
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'instantiates the template' do
|
53
|
+
expect(catalog).to receive(:upload_vapp_template).with('VDC_NAME', 'NAME', 'OVF_DIR')
|
54
|
+
expect(catalog).to receive(:instantiate_vapp_template).with('NAME', 'VDC_NAME', 'NAME', nil, nil, network_config)
|
55
|
+
|
56
|
+
Deployer.deploy_and_power_on_vapp(client: client, ovf_dir: 'OVF_DIR', vapp_config: vapp_config, vdc_name: 'VDC_NAME')
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'reconfigures the vm' do
|
60
|
+
expect(vm).to receive(:product_section_properties=).with(vapp_config.build_properties)
|
61
|
+
|
62
|
+
Deployer.deploy_and_power_on_vapp(client: client, ovf_dir: 'OVF_DIR', vapp_config: vapp_config, vdc_name: 'VDC_NAME')
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'powers on the vapp' do
|
66
|
+
expect(vapp).to receive(:power_on)
|
67
|
+
|
68
|
+
Deployer.deploy_and_power_on_vapp(client: client, ovf_dir: 'OVF_DIR', vapp_config: vapp_config, vdc_name: 'VDC_NAME')
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'does things in order' do
|
72
|
+
Deployer.deploy_and_power_on_vapp(client: client, ovf_dir: 'OVF_DIR', vapp_config: vapp_config, vdc_name: 'VDC_NAME')
|
73
|
+
|
74
|
+
expect(client).to have_received(:create_catalog).ordered
|
75
|
+
expect(catalog).to have_received(:upload_vapp_template).ordered
|
76
|
+
expect(catalog).to have_received(:instantiate_vapp_template).ordered
|
77
|
+
expect(vm).to have_received(:product_section_properties=).ordered
|
78
|
+
expect(vapp).to have_received(:power_on).ordered
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'vm_shepherd/data_object'
|
2
|
+
require 'vm_shepherd/vcloud/vapp_config'
|
3
|
+
require 'vm_shepherd/vcloud/destroyer'
|
4
|
+
require 'ruby_vcloud_sdk'
|
5
|
+
|
6
|
+
module VmShepherd
|
7
|
+
module Vcloud
|
8
|
+
RSpec.describe Destroyer do
|
9
|
+
subject(:destroyer) { Destroyer.new(client: client, vdc_name: vdc_name) }
|
10
|
+
let(:client) { instance_double(VCloudSdk::Client) }
|
11
|
+
let(:vdc_name) { 'VDC_NAME' }
|
12
|
+
let(:fake_logger) { double(:logger, debug: nil) }
|
13
|
+
let(:vm) { instance_double(VCloudSdk::VM) }
|
14
|
+
let(:vdc) { instance_double(VCloudSdk::VDC) }
|
15
|
+
let(:vapp) { instance_double(VCloudSdk::VApp) }
|
16
|
+
let(:disk) { instance_double(VCloudSdk::InternalDisk, name: 'DISK_NAME') }
|
17
|
+
|
18
|
+
before do
|
19
|
+
allow(client).to receive(:delete_catalog_by_name)
|
20
|
+
allow(client).to receive(:catalog_exists?)
|
21
|
+
allow(client).to receive(:find_vdc_by_name).with(vdc_name).and_return(vdc)
|
22
|
+
allow(vdc).to receive(:find_vapp_by_name).with('VAPP_NAME').and_return(vapp)
|
23
|
+
allow(vdc).to receive(:delete_disk_by_name)
|
24
|
+
allow(vapp).to receive(:vms).and_return([vm])
|
25
|
+
allow(vapp).to receive(:power_off)
|
26
|
+
allow(vapp).to receive(:delete)
|
27
|
+
allow(vm).to receive(:independent_disks).and_return([disk])
|
28
|
+
allow(vm).to receive(:detach_disk)
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#delete_catalog_and_vapps' do
|
32
|
+
context 'when the catalog exists' do
|
33
|
+
before do
|
34
|
+
allow(client).to receive(:catalog_exists?).with('CATALOG_NAME').and_return(true)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'deletes the catalog' do
|
38
|
+
destroyer.delete_catalog_and_vapps('CATALOG_NAME', [], fake_logger)
|
39
|
+
|
40
|
+
expect(client).to have_received(:delete_catalog_by_name).with('CATALOG_NAME')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'when the catalog does not exist' do
|
45
|
+
before do
|
46
|
+
allow(client).to receive(:catalog_exists?).with('CATALOG_NAME').and_return(false)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'skips deleting the catalog' do
|
50
|
+
destroyer.delete_catalog_and_vapps('CATALOG_NAME', [], fake_logger)
|
51
|
+
|
52
|
+
expect(client).not_to have_received(:delete_catalog_by_name).with('CATALOG_NAME')
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'detaches and deletes persistent disks' do
|
57
|
+
destroyer.delete_catalog_and_vapps('CATALOG_NAME', ['VAPP_NAME'], fake_logger)
|
58
|
+
|
59
|
+
expect(vm).to have_received(:detach_disk).with(disk)
|
60
|
+
expect(vdc).to have_received(:delete_disk_by_name).with('DISK_NAME')
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'powers off and deletes vapps' do
|
64
|
+
destroyer.delete_catalog_and_vapps('CATALOG_NAME', ['VAPP_NAME'], fake_logger)
|
65
|
+
|
66
|
+
expect(vapp).to have_received(:power_off)
|
67
|
+
expect(vapp).to have_received(:delete)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'vm_shepherd/data_object'
|
2
|
+
require 'vm_shepherd/vcloud/vapp_config'
|
3
|
+
|
4
|
+
module VmShepherd
|
5
|
+
module Vcloud
|
6
|
+
RSpec.describe(VappConfig) do
|
7
|
+
subject(:vapp_config) do
|
8
|
+
VappConfig.new(
|
9
|
+
name: 'NAME',
|
10
|
+
ip: 'IP',
|
11
|
+
gateway: 'GATEWAY',
|
12
|
+
netmask: 'NETMASK',
|
13
|
+
dns: 'DNS',
|
14
|
+
ntp: 'NTP',
|
15
|
+
catalog: 'CATALOG',
|
16
|
+
network: 'NETWORK',
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'is a DataObject' do
|
21
|
+
expect(vapp_config).to be_a(DataObject)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -1,4 +1,8 @@
|
|
1
|
+
require 'vm_shepherd/data_object'
|
1
2
|
require 'vm_shepherd/vcloud_manager'
|
3
|
+
require 'vm_shepherd/vcloud/deployer'
|
4
|
+
require 'vm_shepherd/vcloud/destroyer'
|
5
|
+
require 'vm_shepherd/vcloud/vapp_config'
|
2
6
|
|
3
7
|
module VmShepherd
|
4
8
|
RSpec.describe VcloudManager do
|
@@ -18,7 +22,7 @@ module VmShepherd
|
|
18
22
|
|
19
23
|
describe '#deploy' do
|
20
24
|
let(:vapp_config) do
|
21
|
-
|
25
|
+
Vcloud::VappConfig.new(
|
22
26
|
ip: 'FAKE_IP',
|
23
27
|
name: 'FAKE_NAME',
|
24
28
|
gateway: 'FAKE_GATEWAY',
|
@@ -27,7 +31,7 @@ module VmShepherd
|
|
27
31
|
netmask: 'FAKE_NETMASK',
|
28
32
|
catalog: 'FAKE_VAPP_CATALOG',
|
29
33
|
network: 'FAKE_NETWORK',
|
30
|
-
|
34
|
+
)
|
31
35
|
end
|
32
36
|
|
33
37
|
let(:vapp_template_path) { 'FAKE_VAPP_TEMPLATE_PATH' }
|
@@ -39,7 +43,7 @@ module VmShepherd
|
|
39
43
|
let(:expanded_vapp_template_path) { 'FAKE_EXPANDED_VAPP_TEMPLATE_PATH' }
|
40
44
|
|
41
45
|
before do
|
42
|
-
allow(vcloud_manager).to receive(:system).with("ping -c 5 #{vapp_config.
|
46
|
+
allow(vcloud_manager).to receive(:system).with("ping -c 5 #{vapp_config.ip}").and_return(false)
|
43
47
|
|
44
48
|
allow(File).to receive(:expand_path).with(vapp_template_path).and_return(expanded_vapp_template_path)
|
45
49
|
|
@@ -71,7 +75,7 @@ module VmShepherd
|
|
71
75
|
{
|
72
76
|
'type' => 'string',
|
73
77
|
'key' => 'gateway',
|
74
|
-
'value' => vapp_config.
|
78
|
+
'value' => vapp_config.gateway,
|
75
79
|
'password' => 'false',
|
76
80
|
'userConfigurable' => 'true',
|
77
81
|
'Label' => 'Default Gateway',
|
@@ -80,7 +84,7 @@ module VmShepherd
|
|
80
84
|
{
|
81
85
|
'type' => 'string',
|
82
86
|
'key' => 'DNS',
|
83
|
-
'value' => vapp_config.
|
87
|
+
'value' => vapp_config.dns,
|
84
88
|
'password' => 'false',
|
85
89
|
'userConfigurable' => 'true',
|
86
90
|
'Label' => 'DNS',
|
@@ -89,7 +93,7 @@ module VmShepherd
|
|
89
93
|
{
|
90
94
|
'type' => 'string',
|
91
95
|
'key' => 'ntp_servers',
|
92
|
-
'value' => vapp_config.
|
96
|
+
'value' => vapp_config.ntp,
|
93
97
|
'password' => 'false',
|
94
98
|
'userConfigurable' => 'true',
|
95
99
|
'Label' => 'NTP Servers',
|
@@ -107,7 +111,7 @@ module VmShepherd
|
|
107
111
|
{
|
108
112
|
'type' => 'string',
|
109
113
|
'key' => 'ip0',
|
110
|
-
'value' => vapp_config.
|
114
|
+
'value' => vapp_config.ip,
|
111
115
|
'password' => 'false',
|
112
116
|
'userConfigurable' => 'true',
|
113
117
|
'Label' => 'IP Address',
|
@@ -116,7 +120,7 @@ module VmShepherd
|
|
116
120
|
{
|
117
121
|
'type' => 'string',
|
118
122
|
'key' => 'netmask0',
|
119
|
-
'value' => vapp_config.
|
123
|
+
'value' => vapp_config.netmask,
|
120
124
|
'password' => 'false',
|
121
125
|
'userConfigurable' => 'true',
|
122
126
|
'Label' => 'Netmask',
|
@@ -153,34 +157,8 @@ module VmShepherd
|
|
153
157
|
vcloud_manager.deploy(vapp_template_path, vapp_config)
|
154
158
|
end
|
155
159
|
|
156
|
-
describe 'catalog deletion' do
|
157
|
-
before do
|
158
|
-
allow(client).to receive(:catalog_exists?).and_return(catalog_exists)
|
159
|
-
end
|
160
|
-
|
161
|
-
context 'when the catalog exists' do
|
162
|
-
let(:catalog_exists) { true }
|
163
|
-
|
164
|
-
it 'deletes the catalog' do
|
165
|
-
expect(client).to receive(:delete_catalog_by_name).with(vapp_config.fetch(:catalog))
|
166
|
-
|
167
|
-
vcloud_manager.deploy(vapp_template_path, vapp_config)
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
|
-
context 'when the catalog does not exist' do
|
172
|
-
let(:catalog_exists) { false }
|
173
|
-
|
174
|
-
it 'does not delete the catalog' do
|
175
|
-
expect(client).not_to receive(:delete_catalog_by_name).with(vapp_config.fetch(:catalog))
|
176
|
-
|
177
|
-
vcloud_manager.deploy(vapp_template_path, vapp_config)
|
178
|
-
end
|
179
|
-
end
|
180
|
-
end
|
181
|
-
|
182
160
|
it 'creates the catalog' do
|
183
|
-
expect(client).to receive(:create_catalog).with(vapp_config.
|
161
|
+
expect(client).to receive(:create_catalog).with(vapp_config.catalog).and_return(catalog)
|
184
162
|
|
185
163
|
vcloud_manager.deploy(vapp_template_path, vapp_config)
|
186
164
|
end
|
@@ -188,7 +166,7 @@ module VmShepherd
|
|
188
166
|
it 'uploads the vApp template' do
|
189
167
|
expect(catalog).to receive(:upload_vapp_template).with(
|
190
168
|
vdc_name,
|
191
|
-
vapp_config.
|
169
|
+
vapp_config.name,
|
192
170
|
tmpdir,
|
193
171
|
).and_return(catalog)
|
194
172
|
|
@@ -197,7 +175,7 @@ module VmShepherd
|
|
197
175
|
|
198
176
|
it 'creates a VCloudSdk::NetworkConfig' do
|
199
177
|
expect(VCloudSdk::NetworkConfig).to receive(:new).with(
|
200
|
-
vapp_config.
|
178
|
+
vapp_config.network,
|
201
179
|
'Network 1',
|
202
180
|
).and_return(network_config)
|
203
181
|
|
@@ -206,9 +184,9 @@ module VmShepherd
|
|
206
184
|
|
207
185
|
it 'instantiates the vApp template' do
|
208
186
|
expect(catalog).to receive(:instantiate_vapp_template).with(
|
209
|
-
vapp_config.
|
187
|
+
vapp_config.name,
|
210
188
|
vdc_name,
|
211
|
-
vapp_config.
|
189
|
+
vapp_config.name,
|
212
190
|
nil,
|
213
191
|
nil,
|
214
192
|
network_config
|
@@ -256,7 +234,7 @@ module VmShepherd
|
|
256
234
|
end
|
257
235
|
|
258
236
|
it 'raises an error' do
|
259
|
-
expect { vcloud_manager.deploy(vapp_template_path, vapp_config) }.to raise_error("Error executing: #{tar_expand_cmd
|
237
|
+
expect { vcloud_manager.deploy(vapp_template_path, vapp_config) }.to raise_error("Error executing: #{tar_expand_cmd}")
|
260
238
|
end
|
261
239
|
|
262
240
|
it 'removes the expanded vApp template' do
|
@@ -269,11 +247,11 @@ module VmShepherd
|
|
269
247
|
|
270
248
|
context 'when a host exists at the specified IP' do
|
271
249
|
before do
|
272
|
-
allow(vcloud_manager).to receive(:system).with("ping -c 5 #{vapp_config.
|
250
|
+
allow(vcloud_manager).to receive(:system).with("ping -c 5 #{vapp_config.ip}").and_return(true)
|
273
251
|
end
|
274
252
|
|
275
253
|
it 'raises an error' do
|
276
|
-
expect { vcloud_manager.deploy(vapp_template_path, vapp_config) }.to raise_error("VM exists at #{vapp_config.
|
254
|
+
expect { vcloud_manager.deploy(vapp_template_path, vapp_config) }.to raise_error("VM exists at #{vapp_config.ip}")
|
277
255
|
end
|
278
256
|
|
279
257
|
it 'removes the expanded vApp template' do
|
@@ -290,6 +268,13 @@ module VmShepherd
|
|
290
268
|
let(:vapp) { instance_double(VCloudSdk::VApp) }
|
291
269
|
let(:vapp_name) { 'FAKE_VAPP_NAME' }
|
292
270
|
let(:vapp_catalog) { 'FAKE_VAPP_CATALOG' }
|
271
|
+
let(:vm) { instance_double(VCloudSdk::VM) }
|
272
|
+
let(:disk) { instance_double(VCloudSdk::InternalDisk, name: 'disk name') }
|
273
|
+
|
274
|
+
before do
|
275
|
+
allow(vapp).to receive(:vms).and_return([vm])
|
276
|
+
allow(vm).to receive(:independent_disks).and_return([disk])
|
277
|
+
end
|
293
278
|
|
294
279
|
context 'when the catalog exists' do
|
295
280
|
before do
|
@@ -299,6 +284,8 @@ module VmShepherd
|
|
299
284
|
it 'uses VCloudSdk::Client to delete the vApp' do
|
300
285
|
expect(client).to receive(:find_vdc_by_name).with(vdc_name).and_return(vdc)
|
301
286
|
expect(vdc).to receive(:find_vapp_by_name).with(vapp_name).and_return(vapp)
|
287
|
+
expect(vm).to receive(:detach_disk).with(disk)
|
288
|
+
expect(vdc).to receive(:delete_disk_by_name).with('disk name')
|
302
289
|
expect(vapp).to receive(:power_off)
|
303
290
|
expect(vapp).to receive(:delete)
|
304
291
|
expect(client).to receive(:delete_catalog_by_name).with(vapp_catalog)
|
@@ -319,6 +306,8 @@ module VmShepherd
|
|
319
306
|
allow(VCloudSdk::Client).to receive(:new).and_return(client)
|
320
307
|
allow(client).to receive(:find_vdc_by_name).and_return(vdc)
|
321
308
|
allow(vdc).to receive(:find_vapp_by_name).and_return(vapp)
|
309
|
+
allow(vm).to receive(:detach_disk)
|
310
|
+
allow(vdc).to receive(:delete_disk_by_name)
|
322
311
|
allow(vapp).to receive(:power_off)
|
323
312
|
allow(vapp).to receive(:delete)
|
324
313
|
|
@@ -326,7 +315,7 @@ module VmShepherd
|
|
326
315
|
end
|
327
316
|
|
328
317
|
it 'catches the error' do
|
329
|
-
allow(
|
318
|
+
allow(vdc).to receive(:find_vapp_by_name).and_raise(VCloudSdk::ObjectNotFoundError)
|
330
319
|
|
331
320
|
expect { vcloud_manager.destroy([vapp_name], vapp_catalog) }.not_to raise_error
|
332
321
|
end
|
@@ -347,6 +336,8 @@ module VmShepherd
|
|
347
336
|
it 'uses VCloudSdk::Client to delete the vApp' do
|
348
337
|
expect(client).to receive(:find_vdc_by_name).with(vdc_name).and_return(vdc)
|
349
338
|
expect(vdc).to receive(:find_vapp_by_name).with(vapp_name).and_return(vapp)
|
339
|
+
expect(vm).to receive(:detach_disk).with(disk)
|
340
|
+
expect(vdc).to receive(:delete_disk_by_name).with('disk name')
|
350
341
|
expect(vapp).to receive(:power_off)
|
351
342
|
expect(vapp).to receive(:delete)
|
352
343
|
expect(client).not_to receive(:delete_catalog_by_name).with(vapp_catalog)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: vm_shepherd
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.11.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ops Manager Team
|
@@ -168,9 +168,13 @@ files:
|
|
168
168
|
- ci/run_specs.sh
|
169
169
|
- lib/vm_shepherd.rb
|
170
170
|
- lib/vm_shepherd/aws_manager.rb
|
171
|
+
- lib/vm_shepherd/data_object.rb
|
171
172
|
- lib/vm_shepherd/openstack_manager.rb
|
172
173
|
- lib/vm_shepherd/retry_helper.rb
|
173
174
|
- lib/vm_shepherd/shepherd.rb
|
175
|
+
- lib/vm_shepherd/vcloud/deployer.rb
|
176
|
+
- lib/vm_shepherd/vcloud/destroyer.rb
|
177
|
+
- lib/vm_shepherd/vcloud/vapp_config.rb
|
174
178
|
- lib/vm_shepherd/vcloud_manager.rb
|
175
179
|
- lib/vm_shepherd/version.rb
|
176
180
|
- lib/vm_shepherd/vsphere_manager.rb
|
@@ -184,9 +188,13 @@ files:
|
|
184
188
|
- spec/spec_helper.rb
|
185
189
|
- spec/support/patched_fog.rb
|
186
190
|
- spec/vm_shepherd/aws_manager_spec.rb
|
191
|
+
- spec/vm_shepherd/data_object_spec.rb
|
187
192
|
- spec/vm_shepherd/openstack_manager_spec.rb
|
188
193
|
- spec/vm_shepherd/retry_helper_spec.rb
|
189
194
|
- spec/vm_shepherd/shepherd_spec.rb
|
195
|
+
- spec/vm_shepherd/vcloud/deployer_spec.rb
|
196
|
+
- spec/vm_shepherd/vcloud/destroyer_spec.rb
|
197
|
+
- spec/vm_shepherd/vcloud/vapp_config_spec.rb
|
190
198
|
- spec/vm_shepherd/vcloud_manager_spec.rb
|
191
199
|
- spec/vm_shepherd/vsphere_manager_spec.rb
|
192
200
|
- vm_shepherd.gemspec
|
@@ -209,7 +217,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
209
217
|
version: '0'
|
210
218
|
requirements: []
|
211
219
|
rubyforge_project:
|
212
|
-
rubygems_version: 2.4.
|
220
|
+
rubygems_version: 2.4.7
|
213
221
|
signing_key:
|
214
222
|
specification_version: 4
|
215
223
|
summary: A tool for booting and tearing down Ops Manager VMs on various Infrastructures.
|
@@ -224,8 +232,12 @@ test_files:
|
|
224
232
|
- spec/spec_helper.rb
|
225
233
|
- spec/support/patched_fog.rb
|
226
234
|
- spec/vm_shepherd/aws_manager_spec.rb
|
235
|
+
- spec/vm_shepherd/data_object_spec.rb
|
227
236
|
- spec/vm_shepherd/openstack_manager_spec.rb
|
228
237
|
- spec/vm_shepherd/retry_helper_spec.rb
|
229
238
|
- spec/vm_shepherd/shepherd_spec.rb
|
239
|
+
- spec/vm_shepherd/vcloud/deployer_spec.rb
|
240
|
+
- spec/vm_shepherd/vcloud/destroyer_spec.rb
|
241
|
+
- spec/vm_shepherd/vcloud/vapp_config_spec.rb
|
230
242
|
- spec/vm_shepherd/vcloud_manager_spec.rb
|
231
243
|
- spec/vm_shepherd/vsphere_manager_spec.rb
|