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