dopv 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/.rspec +2 -0
- data/ChangeLog.md +456 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +260 -0
- data/Guardfile +22 -0
- data/LICENSE.txt +177 -0
- data/README.md +214 -0
- data/Rakefile +6 -0
- data/bin/dopv +4 -0
- data/dopv.gemspec +52 -0
- data/lib/dopv.rb +166 -0
- data/lib/dopv/cli.rb +54 -0
- data/lib/dopv/cli/command_add.rb +37 -0
- data/lib/dopv/cli/command_export.rb +26 -0
- data/lib/dopv/cli/command_import.rb +32 -0
- data/lib/dopv/cli/command_list.rb +18 -0
- data/lib/dopv/cli/command_remove.rb +29 -0
- data/lib/dopv/cli/command_run.rb +38 -0
- data/lib/dopv/cli/command_update.rb +35 -0
- data/lib/dopv/cli/command_validate.rb +30 -0
- data/lib/dopv/infrastructure.rb +40 -0
- data/lib/dopv/infrastructure/providers/baremetal.rb +12 -0
- data/lib/dopv/infrastructure/providers/base.rb +422 -0
- data/lib/dopv/infrastructure/providers/openstack.rb +308 -0
- data/lib/dopv/infrastructure/providers/ovirt.rb +228 -0
- data/lib/dopv/infrastructure/providers/vsphere.rb +322 -0
- data/lib/dopv/log.rb +14 -0
- data/lib/dopv/persistent_disk.rb +128 -0
- data/lib/dopv/plan.rb +17 -0
- data/lib/dopv/state_store.rb +87 -0
- data/lib/dopv/version.rb +3 -0
- data/spec/data/hooks/test_hook_script_1 +9 -0
- data/spec/data/hooks/test_hook_script_2 +10 -0
- data/spec/data/plans/test-plan-1.yaml +140 -0
- data/spec/spec_helper.rb +112 -0
- data/spec/unit/dopv/dopv_spec.rb +7 -0
- data/spec/unit/dopv/persistent_disk_spec.rb +38 -0
- data/spec/unit/dopv/plan_spec.rb +34 -0
- data/spec/unit/dopv/version_spec.rb +17 -0
- metadata +401 -0
@@ -0,0 +1,322 @@
|
|
1
|
+
require 'rbvmomi'
|
2
|
+
require 'digest/sha2'
|
3
|
+
require 'fog'
|
4
|
+
|
5
|
+
module Dopv
|
6
|
+
module Infrastructure
|
7
|
+
|
8
|
+
class Vsphere < Base
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
def_delegators :@plan, :product_id, :organization_name
|
12
|
+
|
13
|
+
def initialize(node_config, data_disks_db)
|
14
|
+
super(node_config, data_disks_db)
|
15
|
+
|
16
|
+
@compute_connection_opts = {
|
17
|
+
:provider => 'vsphere',
|
18
|
+
:vsphere_username => provider_username,
|
19
|
+
:vsphere_password => provider_password,
|
20
|
+
:vsphere_server => provider_host,
|
21
|
+
:vsphere_port => provider_port,
|
22
|
+
:vsphere_expected_pubkey_hash => provider_pubkey_hash
|
23
|
+
}
|
24
|
+
|
25
|
+
@node_creation_opts = {
|
26
|
+
'name' => nodename,
|
27
|
+
'datacenter' => datacenter.name,
|
28
|
+
'template_path' => image,
|
29
|
+
'dest_folder' => dest_folder || '',
|
30
|
+
'numCPUs' => cores,
|
31
|
+
'memoryMB' => memory.mebibytes.to_i
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def dest_folder
|
38
|
+
@dest_folder ||= infrastructure_properties.dest_folder
|
39
|
+
end
|
40
|
+
|
41
|
+
def timezone
|
42
|
+
super || '085'
|
43
|
+
end
|
44
|
+
|
45
|
+
def node_instance_stopped?(node_instance)
|
46
|
+
!node_instance.ready?
|
47
|
+
end
|
48
|
+
|
49
|
+
def create_node_instance
|
50
|
+
::Dopv::log.info("Node #{nodename}: Creating node instance.")
|
51
|
+
@node_creation_opts['datastore'] = infrastructure_properties.default_pool unless infrastructure_properties.default_pool.nil?
|
52
|
+
vm = compute_provider.vm_clone(@node_creation_opts.merge(
|
53
|
+
'power_on' => false,
|
54
|
+
'wait' => true))
|
55
|
+
compute_provider.servers.get(vm['new_vm']['id'])
|
56
|
+
end
|
57
|
+
|
58
|
+
def customize_node_instance(node_instance)
|
59
|
+
::Dopv::log.info("Node #{nodename}: Customizing node.")
|
60
|
+
# Settings related to each network interface
|
61
|
+
ip_settings = interfaces_config.collect do |i|
|
62
|
+
ip_setting = ::RbVmomi::VIM::CustomizationIPSettings.new
|
63
|
+
if i.ip == :dhcp
|
64
|
+
ip_setting.ip = ::RbVmomi::VIM::CustomizationDhcpIpGenerator.new
|
65
|
+
else
|
66
|
+
ip_setting.ip = ::RbVmomi::VIM::CustomizationFixedIp('ipAddress' => i.ip)
|
67
|
+
ip_setting.subnetMask = i.netmask
|
68
|
+
ip_setting.gateway = [i.gateway] if i.set_gateway?
|
69
|
+
end
|
70
|
+
ip_setting
|
71
|
+
end
|
72
|
+
|
73
|
+
# Adapters mapping
|
74
|
+
nic_setting_map = ip_settings.collect { |s| RbVmomi::VIM::CustomizationAdapterMapping.new('adapter' => s) }
|
75
|
+
|
76
|
+
# Global network settings
|
77
|
+
global_ip_settings = RbVmomi::VIM::CustomizationGlobalIPSettings.new(
|
78
|
+
:dnsServerList => dns.name_servers,
|
79
|
+
:dnsSuffixList => dns.search_domains
|
80
|
+
)
|
81
|
+
|
82
|
+
# Identity settings
|
83
|
+
identity_settings = case guest_id_to_os_family(node_instance)
|
84
|
+
when :linux
|
85
|
+
RbVmomi::VIM::CustomizationLinuxPrep.new(
|
86
|
+
:domain => domainname,
|
87
|
+
:hostName => RbVmomi::VIM::CustomizationFixedName.new(:name => hostname)
|
88
|
+
)
|
89
|
+
when :windows
|
90
|
+
password_settings = (RbVmomi::VIM::CustomizationPassword.new(
|
91
|
+
:plainText => true,
|
92
|
+
:value => administrator_password
|
93
|
+
) rescue nil)
|
94
|
+
RbVmomi::VIM::CustomizationSysprep.new(
|
95
|
+
:guiRunOnce => nil,
|
96
|
+
:guiUnattended => RbVmomi::VIM::CustomizationGuiUnattended.new(
|
97
|
+
:autoLogon => false,
|
98
|
+
:autoLogonCount => 1,
|
99
|
+
:password => password_settings,
|
100
|
+
:timeZone => timezone
|
101
|
+
),
|
102
|
+
:identification => RbVmomi::VIM::CustomizationIdentification.new(
|
103
|
+
:domainAdmin => nil,
|
104
|
+
:domainAdminPassword => nil,
|
105
|
+
:joinDomain => nil
|
106
|
+
),
|
107
|
+
:userData => RbVmomi::VIM::CustomizationUserData.new(
|
108
|
+
:computerName => RbVmomi::VIM::CustomizationFixedName.new(:name => hostname),
|
109
|
+
:fullName => administrator_fullname,
|
110
|
+
:orgName => organization_name,
|
111
|
+
:productId => product_id
|
112
|
+
)
|
113
|
+
)
|
114
|
+
else
|
115
|
+
raise ProviderError, "Unsupported guest OS type"
|
116
|
+
end
|
117
|
+
|
118
|
+
custom_spec = RbVmomi::VIM::CustomizationSpec.new(
|
119
|
+
:identity => identity_settings,
|
120
|
+
:globalIPSettings => global_ip_settings,
|
121
|
+
:nicSettingMap => nic_setting_map
|
122
|
+
)
|
123
|
+
custom_spec.options = RbVmomi::VIM::CustomizationWinOptions.new(
|
124
|
+
:changeSID => true,
|
125
|
+
:deleteAccounts => false
|
126
|
+
) if guest_id_to_os_family(node_instance) == :windows
|
127
|
+
|
128
|
+
custom_spec
|
129
|
+
end
|
130
|
+
|
131
|
+
def start_node_instance(node_instance)
|
132
|
+
customization_spec = super(node_instance)
|
133
|
+
customize_node_task(node_instance, customization_spec)
|
134
|
+
node_instance.start
|
135
|
+
end
|
136
|
+
|
137
|
+
def add_node_nics(node_instance)
|
138
|
+
::Dopv::log.info("Node #{nodename}: Trying to add interfaces.")
|
139
|
+
|
140
|
+
# Remove all interfaces defined by the template
|
141
|
+
remove_node_nics(node_instance)
|
142
|
+
|
143
|
+
# Create interfaces from scratch
|
144
|
+
interfaces_config.each do |i|
|
145
|
+
log_msg = i.virtual_switch.nil? ?
|
146
|
+
"Node #{nodename}: Creating interface #{i.name} in #{i.network}." :
|
147
|
+
"Node #{nodename}: Creating interface #{i.name} in #{i.network} (#{i.virtual_switch})."
|
148
|
+
::Dopv::log.debug(log_msg)
|
149
|
+
attrs = {
|
150
|
+
:name => i.name,
|
151
|
+
:datacenter => node_instance.datacenter,
|
152
|
+
:network => i.network,
|
153
|
+
:virtualswitch => i.virtual_switch,
|
154
|
+
:type => 'VirtualVmxnet3'
|
155
|
+
}
|
156
|
+
add_node_nic(node_instance, attrs)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def add_node_volume(node_instance, config)
|
161
|
+
volume = node_instance.volumes.create(
|
162
|
+
:datastore => config.pool,
|
163
|
+
:size => config.size.kibibytes.to_i,
|
164
|
+
:mode => 'persistent',
|
165
|
+
:thin => true
|
166
|
+
)
|
167
|
+
node_instance.volumes.reload
|
168
|
+
volume.name = config.name
|
169
|
+
volume
|
170
|
+
end
|
171
|
+
|
172
|
+
def destroy_node_volume(node_instance, volume)
|
173
|
+
volume_instance = node_instance.volumes.find { |v| v.filename == volume.id }
|
174
|
+
volume_instance.destroy
|
175
|
+
node_instance.volumes.reload
|
176
|
+
end
|
177
|
+
|
178
|
+
def attach_node_volume(node_instance, volume)
|
179
|
+
backing_info = RbVmomi::VIM::VirtualDiskFlatVer2BackingInfo.new(
|
180
|
+
:datastore => volume.pool,
|
181
|
+
:fileName => volume.id,
|
182
|
+
:diskMode => 'persistent'
|
183
|
+
)
|
184
|
+
|
185
|
+
virtual_disk = RbVmomi::VIM::VirtualDisk.new(
|
186
|
+
:controllerKey => node_instance.scsi_controller.key,
|
187
|
+
:unitNumber => node_instance.volumes.collect { |v| v.unit_number }.max + 1,
|
188
|
+
:key => -1,
|
189
|
+
:backing => backing_info,
|
190
|
+
:capacityInKB => volume.size * 1048576
|
191
|
+
)
|
192
|
+
|
193
|
+
device_spec = RbVmomi::VIM::VirtualDeviceConfigSpec.new(
|
194
|
+
:operation => :add,
|
195
|
+
:device => virtual_disk
|
196
|
+
)
|
197
|
+
|
198
|
+
config_spec = RbVmomi::VIM::VirtualMachineConfigSpec.new(:deviceChange => [device_spec])
|
199
|
+
|
200
|
+
reconfig_node_task(node_instance, config_spec)
|
201
|
+
|
202
|
+
node_instance.volumes.reload
|
203
|
+
end
|
204
|
+
|
205
|
+
def detach_node_volume(node_instance, volume)
|
206
|
+
volume = node_instance.volumes.all.find { |v| v.filename == volume.id }
|
207
|
+
|
208
|
+
virtual_disk = RbVmomi::VIM::VirtualDisk.new(
|
209
|
+
:controllerKey => volume.server.scsi_controller.key,
|
210
|
+
:unitNumber => volume.unit_number,
|
211
|
+
:key => volume.key,
|
212
|
+
:capacityInKB => volume.size
|
213
|
+
)
|
214
|
+
|
215
|
+
device_spec = RbVmomi::VIM::VirtualDeviceConfigSpec.new(
|
216
|
+
:operation => :remove,
|
217
|
+
:device => virtual_disk
|
218
|
+
)
|
219
|
+
|
220
|
+
config_spec = RbVmomi::VIM::VirtualMachineConfigSpec.new(:deviceChange => [device_spec])
|
221
|
+
|
222
|
+
reconfig_node_task(node_instance, config_spec)
|
223
|
+
|
224
|
+
node_instance.volumes.reload
|
225
|
+
end
|
226
|
+
|
227
|
+
def record_node_data_volume(volume)
|
228
|
+
::Dopv::log.debug("Node #{nodename}: Recording volume #{volume.name} into DB.")
|
229
|
+
volume = {
|
230
|
+
:name => volume.name,
|
231
|
+
:id => volume.filename,
|
232
|
+
:pool => volume.datastore,
|
233
|
+
:size => volume.size * 1048576 # Size must be in gibibytes
|
234
|
+
}
|
235
|
+
super(volume)
|
236
|
+
end
|
237
|
+
|
238
|
+
def guest_id_to_os_family(node_instance)
|
239
|
+
# Based on http://pubs.vmware.com/vsphere-50/index.jsp?topic=/com.vmware.wssdk.apiref.doc_50/vim.vm.GuestOsDescriptor.GuestOsIdentifier.html
|
240
|
+
lookup_table = {
|
241
|
+
:debian6_64Guest => :linux,
|
242
|
+
:debian6_Guest => :linux,
|
243
|
+
:rhel4_64Guest => :linux,
|
244
|
+
:rhel4Guest => :linux,
|
245
|
+
:rhel5_64Guest => :linux,
|
246
|
+
:rhel5Guest => :linux,
|
247
|
+
:rhel6_64Guest => :linux,
|
248
|
+
:rhel6Guest => :linux,
|
249
|
+
:rhel7_64Guest => :linux,
|
250
|
+
:rhel7Guest => :linux,
|
251
|
+
:oracleLinux64Guest => :linux,
|
252
|
+
:oracleLinuxGuest => :linux,
|
253
|
+
:ubuntu64Guest => :linux,
|
254
|
+
:ubuntuGuest => :linux,
|
255
|
+
:windows7_64Guest => :windows,
|
256
|
+
:windows7Guest => :windows,
|
257
|
+
:windows7Server64Guest => :windows,
|
258
|
+
:windows8_64Guest => :windows,
|
259
|
+
:windows8Guest => :windows,
|
260
|
+
:windows8Server64Guest => :windows
|
261
|
+
}
|
262
|
+
|
263
|
+
node_instance.guest_id ?
|
264
|
+
lookup_table[node_instance.guest_id.to_sym] : nil
|
265
|
+
end
|
266
|
+
|
267
|
+
def reconfig_node_task(node_instance, reconfig_spec)
|
268
|
+
node_ref = compute_provider.send(:get_vm_ref, node_instance.id)
|
269
|
+
node_ref.ReconfigVM_Task(:spec => reconfig_spec).wait_for_completion
|
270
|
+
end
|
271
|
+
|
272
|
+
def customize_node_task(node_instance, customization_spec)
|
273
|
+
node_ref = compute_provider.send(:get_vm_ref, node_instance.id)
|
274
|
+
node_ref.CustomizeVM_Task(:spec => customization_spec).wait_for_completion
|
275
|
+
end
|
276
|
+
|
277
|
+
def provider_pubkey_hash
|
278
|
+
unless @provider_pubkey_hash
|
279
|
+
connection = ::RbVmomi::VIM.new(
|
280
|
+
:host => provider_host,
|
281
|
+
:port => provider_port,
|
282
|
+
:ssl => provider_ssl?,
|
283
|
+
:ns => 'urn:vim25',
|
284
|
+
:rev => '4.0',
|
285
|
+
:insecure => true
|
286
|
+
)
|
287
|
+
@provider_pubkey_hash ||= ::Digest::SHA2.hexdigest(connection.http.peer_cert.public_key.to_s)
|
288
|
+
connection.close
|
289
|
+
end
|
290
|
+
@provider_pubkey_hash
|
291
|
+
end
|
292
|
+
|
293
|
+
def get_node_ip_addresses(node_instance, params={})
|
294
|
+
begin
|
295
|
+
raise ProviderError, "VMware Tools not installed" unless node_instance.tools_installed?
|
296
|
+
params = {:maxtime => 300, :delay => 10}.merge(params)
|
297
|
+
::Dopv::log.debug("Node #{nodename}: Waiting on VMware Tools for #{params[:maxtime]} seconds.")
|
298
|
+
reload_node_instance(node_instance)
|
299
|
+
node_instance.wait_for(params[:maxtime]){|vm| vm.ready?}
|
300
|
+
node_instance.wait_for(params[:maxtime]){|vm| vm.tools_running?}
|
301
|
+
node_ref = compute_provider.send(:get_vm_ref, node_instance.id)
|
302
|
+
node_ref_guest_net = nil
|
303
|
+
start_time = Time.now.to_f
|
304
|
+
while (Time.now.to_f - start_time) < params[:maxtime]
|
305
|
+
unless node_ref.guest_ip
|
306
|
+
sleep params[:delay]
|
307
|
+
else
|
308
|
+
node_ref_guest_net = node_ref.guest.net
|
309
|
+
break
|
310
|
+
end
|
311
|
+
end
|
312
|
+
raise ProviderError, "VMware Tools not ready yet" unless node_ref_guest_net
|
313
|
+
node_ref_guest_net.map(&:ipAddress).flatten.uniq.compact
|
314
|
+
|
315
|
+
rescue Exception => e
|
316
|
+
::Dopv::log.debug("Node #{nodename}: Unable to get all IP Addresses, Error: #{e.message}.")
|
317
|
+
[node_instance.public_ip_address].compact
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
data/lib/dopv/log.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Dopv
|
4
|
+
module PersistentDisk
|
5
|
+
class PersistentDiskError < StandardError; end
|
6
|
+
|
7
|
+
class Entry
|
8
|
+
DISK_DESC_KEYS = [:id, :name, :node, :pool, :size]
|
9
|
+
|
10
|
+
attr_accessor :name, :id, :pool, :size, :node
|
11
|
+
|
12
|
+
def initialize(attrs)
|
13
|
+
if attrs.is_a?(Hash) && attrs.keys.sort == DISK_DESC_KEYS
|
14
|
+
@node = attrs[:node]
|
15
|
+
@name = attrs[:name]
|
16
|
+
@id = attrs[:id]
|
17
|
+
@pool = attrs[:pool]
|
18
|
+
@size = attrs[:size].to_i
|
19
|
+
else
|
20
|
+
raise PersistentDiskError, "Invalid disk entry"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def ==(other)
|
25
|
+
case other
|
26
|
+
when Entry
|
27
|
+
@node == other.node && @name == other.name && @id == other.id && @pool == other.pool
|
28
|
+
when Hash
|
29
|
+
@node == other[:node] && @name == other[:name] && @id == other[:id] && @pool == other[:pool]
|
30
|
+
else
|
31
|
+
false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def update(attrs={})
|
36
|
+
raise PersistentDiskError, "Update attributes must be a hash" unless attrs.is_a?(Hash)
|
37
|
+
@node = attrs[:node] if attrs[:node]
|
38
|
+
@name = attrs[:name] if attrs[:name]
|
39
|
+
@id = attrs[:id] if attrs[:id]
|
40
|
+
@pool = attrs[:pool] if attrs[:pool]
|
41
|
+
@size = attrs[:size].to_i if attrs[:size]
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_s
|
46
|
+
"Disk: #{@name}\n Node: #{@node}\n Id: #{@id}\n Pool: #{@pool}\n Size: #{@size}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_hash
|
50
|
+
{ :name => @name, :id => @id, :pool => @pool, :size => @size }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class DB
|
55
|
+
|
56
|
+
def initialize(state_store, node_name)
|
57
|
+
@state_store = state_store
|
58
|
+
@node_name = node_name
|
59
|
+
@state_store.transaction do
|
60
|
+
@state_store[:data_volumes] ||= {}
|
61
|
+
@state_store[:data_volumes][@node_name] ||= []
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def volumes
|
66
|
+
@state_store.transaction(true) { entries }.dup
|
67
|
+
end
|
68
|
+
|
69
|
+
def append(entry)
|
70
|
+
@state_store.transaction do
|
71
|
+
entries.each do |disk|
|
72
|
+
if disk == entry
|
73
|
+
raise PersistentDiskError, "Disk #{disk.name} already exists for node #{disk.node}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
if entry.is_a?(Entry)
|
77
|
+
@entries << entry
|
78
|
+
@state_store[:data_volumes][@node_name] << entry.to_hash
|
79
|
+
else
|
80
|
+
@entries << Entry.new(entry)
|
81
|
+
@state_store[:data_volumes][@node_name] << entry
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def <<(entry)
|
87
|
+
append(entry)
|
88
|
+
end
|
89
|
+
alias_method :add, :<<
|
90
|
+
|
91
|
+
def update(entry, attrs={})
|
92
|
+
@state_store.transaction do
|
93
|
+
index = entries.index {|stored_entry| stored_entry = entry}
|
94
|
+
if index.nil?
|
95
|
+
raise PersistentDiskError, "Entry update: Disk entry not found #{entry.to_s}"
|
96
|
+
end
|
97
|
+
@entries[index].update(attrs)
|
98
|
+
@state_store[:data_volumes][@node_name][index] = @entries[index]
|
99
|
+
@entries[index]
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def delete(entry)
|
104
|
+
@state_store.transaction do
|
105
|
+
index = entries.index {|stored_entry| stored_entry = entry}
|
106
|
+
if index.nil?
|
107
|
+
raise PersistentDiskError, "Entry update: Disk entry not found #{entry.to_s}"
|
108
|
+
end
|
109
|
+
|
110
|
+
@entries.delete_at(index)
|
111
|
+
@state_store[:data_volumes][@node_name].delete_at(index)
|
112
|
+
entry
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def entries
|
119
|
+
@entries ||= @state_store[:data_volumes][@node_name].map do |raw_entry|
|
120
|
+
symbolized_entry = Hash[raw_entry.map {|k1, v1| [k1.to_sym, v1] }]
|
121
|
+
merged_entry = symbolized_entry.merge({:node => @node_name})
|
122
|
+
Dopv::PersistentDisk::Entry.new(merged_entry)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|