dopv 0.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 +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
|