diff --git a/app/models/manageiq/providers/vmware/cloud_manager/orchestration_service_option_converter.rb b/app/models/manageiq/providers/vmware/cloud_manager/orchestration_service_option_converter.rb index c16191406..8ab5f0da3 100644 --- a/app/models/manageiq/providers/vmware/cloud_manager/orchestration_service_option_converter.rb +++ b/app/models/manageiq/providers/vmware/cloud_manager/orchestration_service_option_converter.rb @@ -3,77 +3,125 @@ class Vmware::CloudManager::OrchestrationServiceOptionConverter < ::ServiceOrche include Vmdb::Logging def stack_create_options + template = ManageIQ::Providers::Vmware::CloudManager::OvfTemplate.new(self.class.get_template(@dialog_options).content) options = { :deploy => stack_parameters['deploy'] == 't', :powerOn => stack_parameters['powerOn'] == 't' } options[:vdc_id] = @dialog_options['dialog_availability_zone'] unless @dialog_options['dialog_availability_zone'].blank? - - options.merge!(customize_vapp_template(collect_vm_params)) + options.merge!(customize_vapp_template(collect_vm_params(template), collect_vapp_net_params(template))) end private # customize_vapp_template will prepare the options in a format suitable for the fog-vcloud-director. - # This mainly results in creating two top level objects in a hash. The :InstantiationParams, contains - # a single :NetworkConfig element with the array of all references to the networks used in the - # deployed vApp. We are using IDs here and let fog library create concrete HREFs that are required - # by the vCloud director. The second object is an array of source items. Each source item references - # a single VM from the vApp template, customises its name and optionally sets network info. - def customize_vapp_template(vm_params) - network_config = {} - - source_vms = vm_params.collect do |vm_id, vm_opts| - src_vm = { :vm_id => "vm-#{vm_id}" } - src_vm[:name] = vm_opts["instance_name"] if vm_opts.key?("instance_name") - - network_id = vm_opts["vdc_network"] - unless network_id.nil? - # Create new network config if it hasn't been created before. - network_config[network_id] ||= { - :networkName => network_id, - :networkId => network_id, - :fenceMode => "bridged" + # See https://github.com/xlab-si/fog-vcloud-director/blob/master/docs/examples-vapp-instantiate.md + def customize_vapp_template(vm_params, vapp_net_params) + source_vms = vm_params.map do |_, vm_opts| + src_vm = { + :vm_id => "vm-#{vm_opts[:vm_id]}", + :networks => parse_nics(vm_opts), + :hardware => { + :cpu => { :num_cores => vm_opts['num_cores'], :cores_per_socket => vm_opts['cores_per_socket'] }, + :memory => { :quantity_mb => vm_opts['memory_mb'] }, + :disk => parse_disks(vm_opts) } - - # Add network configuration to the source VM. - src_vm[:networks] = [ - :networkName => network_id, - :IsConnected => true, - :IpAddressAllocationMode => "DHCP" - ] - end - + } + src_vm[:name] = vm_opts["instance_name"] if vm_opts.key?("instance_name") + src_vm[:guest_customization] = { :ComputerName => vm_opts['hostname'] } if vm_opts.key?("hostname") src_vm end - # Create options suitable for VMware vCloud provider. - custom_opts = { - :source_vms => source_vms + vapp_networks = vapp_net_params.map do |_, opts| + { + :name => opts[:vapp_net_name], + :parent => opts['parent'], + :fence_mode => opts['fence_mode'], + :subnet => parse_subnets(opts) + } + end + + { + :source_vms => source_vms, + :vapp_networks => vapp_networks } - custom_opts[:InstantiationParams] = { - :NetworkConfig => network_config.values - } unless network_config.empty? + end - custom_opts + def collect_vm_params(template) + vm_params = collect_stack_parameters( + %w(instance_name vdc_network num_cores cores_per_socket memory_mb disk_mb hostname nic_network nic_mode nic_ip_address) + ) + # Reverse lookup by indeces. + vm_params.each do |vm_idx, obj| + obj[:vm_id] = template.vm_id_from_idx(vm_idx) + obj['disk_mb'].each do |disk| + disk[:disk_id] = template.disk_id_from_idx(vm_idx, *disk[:subkeys]) + end + end + vm_params end - def collect_vm_params - allowed_vm_params = %w(instance_name vdc_network) - stack_parameters.each_with_object({}) do |(key, value), vm_params| - allowed_vm_params.each do |param| - # VM-specific parameters are named as instance_name-. The - # following will test the param name for this kind of pattern and use - # the to store the configuration about this VM. - param_match = key.match(/#{param}-([0-9a-f-]*)/) + def collect_vapp_net_params(template) + vapp_net_params = collect_stack_parameters(%w(gateway netmask dns1 dns2 parent fence_mode)) + # Reverse lookup by indeces. + vapp_net_params.each do |vapp_net_idx, obj| + obj[:vapp_net_name] = template.vapp_net_name_from_idx(vapp_net_idx) + end + vapp_net_params + end + + def collect_stack_parameters(allowed) + stack_parameters.each_with_object({}) do |(k, value), params| + allowed.each do |param| + param_match = k.match(/#{param}(-[0-9]+)?(-[0-9]+)?(-[0-9]+)?/) next if param_match.nil? - vm_id = param_match.captures.first - vm_params[vm_id] ||= {} - # Store the parameter value. - vm_params[vm_id][param] = value + keys = param_match.captures.compact.map { |c| Integer(c.sub(/^-/, '')) } + params[keys.first] ||= {} + + if keys.count > 1 + params[keys.first][param] ||= [] + params[keys.first][param] << { :subkeys => keys[1..-1], :value => value } + params[keys.first][param].sort_by! { |el| el[:subkeys] } + else + params[keys.first][param] = value + end end end end + + def parse_disks(opts) + return if opts['disk_mb'].blank? + opts['disk_mb'].map { |disk| { :id => disk[:disk_id], :capacity_mb => disk[:value] } } + end + + def parse_subnets(opts) + return unless opts['gateway'] + Array.new(opts['gateway'].size) do |idx| + { + :gateway => option_value(opts['gateway'], [idx]), + :netmask => option_value(opts['netmask'], [idx]), + :dns1 => option_value(opts['dns1'], [idx]), + :dns2 => option_value(opts['dns2'], [idx]), + } + end + end + + def parse_nics(opts) + return unless opts['nic_network'] + Array.new(opts['nic_network'].size) do |idx| + { + :networkName => option_value(opts['nic_network'], [idx]).presence || 'none', + :IpAddressAllocationMode => option_value(opts['nic_mode'], [idx]), + :IpAddress => option_value(opts['nic_ip_address'], [idx]), + :IsConnected => true + } + end + end + + def option_value(opts_group, subkeys) + opt = opts_group.detect { |o| o[:subkeys] == subkeys } + opt[:value] unless opt.nil? + end end end diff --git a/app/models/manageiq/providers/vmware/cloud_manager/orchestration_stack.rb b/app/models/manageiq/providers/vmware/cloud_manager/orchestration_stack.rb index 29583b532..20be0fe1d 100644 --- a/app/models/manageiq/providers/vmware/cloud_manager/orchestration_stack.rb +++ b/app/models/manageiq/providers/vmware/cloud_manager/orchestration_stack.rb @@ -2,13 +2,14 @@ class ManageIQ::Providers::Vmware::CloudManager::OrchestrationStack < ManageIQ:: require_nested :Status def self.raw_create_stack(orchestration_manager, stack_name, template, options = {}) + log_prefix = "stack=[#{stack_name}]" orchestration_manager.with_provider_connection do |service| create_options = {:stack_name => stack_name, :template => template.ems_ref}.merge(options) - + $vcloud_log.info("#{log_prefix} create_options: #{create_options}") service.instantiate_template(create_options) end rescue => err - $vcloud_log.error("stack=[#{stack_name}], error: #{err}") + $vcloud_log.error("#{log_prefix} error: #{err}") raise MiqException::MiqOrchestrationProvisionError, err.to_s, err.backtrace end diff --git a/app/models/manageiq/providers/vmware/cloud_manager/orchestration_template.rb b/app/models/manageiq/providers/vmware/cloud_manager/orchestration_template.rb index a8a9fcab3..9b89bab3b 100644 --- a/app/models/manageiq/providers/vmware/cloud_manager/orchestration_template.rb +++ b/app/models/manageiq/providers/vmware/cloud_manager/orchestration_template.rb @@ -2,33 +2,29 @@ class ManageIQ::Providers::Vmware::CloudManager::OrchestrationTemplate < Orchest def parameter_groups # Define vApp's general purpose parameters. groups = [OrchestrationTemplate::OrchestrationParameterGroup.new( - :label => "vApp Parameters", + :label => 'vApp Parameters', :parameters => vapp_parameters, )] - # Parse template's OVF file - ovf_doc = MiqXml.load(content) - # Collect VM-specific parameters from the OVF template if it is a valid one. - groups.concat(vm_param_groups(ovf_doc.root)) unless ovf_doc.root.nil? - - groups + template = ManageIQ::Providers::Vmware::CloudManager::OvfTemplate.new(content) + groups + vapp_net_param_groups(template) + vm_param_groups(template) end def vapp_parameters [ OrchestrationTemplate::OrchestrationParameter.new( - :name => "deploy", - :label => "Deploy vApp", - :data_type => "boolean", + :name => 'deploy', + :label => 'Deploy vApp', + :data_type => 'boolean', :default_value => true, :constraints => [ OrchestrationTemplate::OrchestrationParameterBoolean.new ] ), OrchestrationTemplate::OrchestrationParameter.new( - :name => "powerOn", - :label => "Power On vApp", - :data_type => "boolean", + :name => 'powerOn', + :label => 'Power On vApp', + :data_type => 'boolean', :default_value => false, :constraints => [ OrchestrationTemplate::OrchestrationParameterBoolean.new @@ -37,39 +33,200 @@ def vapp_parameters ] end - def vm_param_groups(ovf) - groups = [] - # Parse the XML template document for specific vCloud attributes. - ovf.each_element("//vcloud:GuestCustomizationSection") do |el| - vm_id = el.elements["vcloud:VirtualMachineId"].text - vm_name = el.elements["vcloud:ComputerName"].text - - groups << OrchestrationTemplate::OrchestrationParameterGroup.new( - :label => vm_name, - :parameters => [ - # Name of the provisioned instance. + def vm_param_groups(template) + template.vms.each_with_index.map do |vm, vm_idx| + vm_parameters = [ + OrchestrationTemplate::OrchestrationParameter.new( + :name => param_name('instance_name', [vm_idx]), + :label => 'Instance name', + :data_type => 'string', + :required => true, + :default_value => vm.name + ), + OrchestrationTemplate::OrchestrationParameter.new( + :name => param_name('hostname', [vm_idx]), + :label => 'Instance Hostname', + :description => 'Can only contain alphanumeric characters and hypens', + :data_type => 'string', + :required => true, + :default_value => vm.hostname, + :constraints => [ + OrchestrationTemplate::OrchestrationParameterPattern.new( + :pattern => '^\S+$', + :description => 'No spaces allowed' + ) + ] + ), + OrchestrationTemplate::OrchestrationParameter.new( + :name => param_name('num_cores', [vm_idx]), + :label => 'Number of virtual CPUs', + :data_type => 'integer', + :required => true, + :default_value => vm.num_cores, + :constraints => [ + OrchestrationTemplate::OrchestrationParameterRange.new( + :min_value => 1, + :max_value => 128, + :description => 'Must be between 1 and 128' + ) + ] + ), + OrchestrationTemplate::OrchestrationParameter.new( + :name => param_name('cores_per_socket', [vm_idx]), + :label => 'Cores per socket', + :description => 'Must be divisor of number of virtual CPUs', + :data_type => 'integer', + :required => true, + :default_value => vm.cores_per_socket, + :constraints => [ + OrchestrationTemplate::OrchestrationParameterRange.new( + :min_value => 1, + :max_value => 128, + :description => 'Must be between 1 and 128' + ) + ] + ), + OrchestrationTemplate::OrchestrationParameter.new( + :name => param_name('memory_mb', [vm_idx]), + :label => 'Total memory (MB)', + :description => 'Must not be less than 4MB', + :data_type => 'integer', + :required => true, + :default_value => vm.memory_mb, + :constraints => [ + OrchestrationTemplate::OrchestrationParameterRange.new(:min_value => 4) + ] + ) + ] + + # Disks. + vm.disks.each_with_index do |disk, disk_idx| + vm_parameters << OrchestrationTemplate::OrchestrationParameter.new( + :name => param_name('disk_mb', [vm_idx, disk_idx]), + :label => "Disk #{disk.address} (MB)", + :description => "Must not be less than original Disk #{disk.address} size (#{disk.capacity_mb}MB)", + :data_type => 'integer', + :required => true, + :default_value => disk.capacity_mb, + :constraints => [ + OrchestrationTemplate::OrchestrationParameterRange.new(:min_value => disk.capacity_mb) + ] + ) + end + + # NICs. + vm.nics.each_with_index do |nic, nic_idx| + vm_parameters += [ OrchestrationTemplate::OrchestrationParameter.new( - :name => "instance_name-#{vm_id}", - :label => "Instance name", - :data_type => "string", - :default_value => vm_name + :name => param_name('nic_network', [vm_idx, nic_idx]), + :label => "NIC##{nic.idx} Network", + :data_type => 'string', + :default_value => nic.network, + :constraints => [ + OrchestrationTemplate::OrchestrationParameterAllowed.new(:allowed_values => template.vapp_network_names) + ] ), - - # List of available VDC networks. OrchestrationTemplate::OrchestrationParameter.new( - :name => "vdc_network-#{vm_id}", - :label => "Network", - :data_type => "string", - :default_value => "(default)", + :name => param_name('nic_mode', [vm_idx, nic_idx]), + :label => "NIC##{nic.idx} Mode", + :data_type => 'string', + :default_value => nic.mode, + :required => true, :constraints => [ - OrchestrationTemplate::OrchestrationParameterAllowedDynamic.new(:fqname => "/Cloud/Orchestration/Operations/Methods/Available_Vdc_Networks") + OrchestrationTemplate::OrchestrationParameterAllowed.new( + :allowed_values => { + 'DHCP' => 'DHCP', + 'MANUAL' => 'Static - Manual', + 'POOL' => 'Static - IP Pool' + } + ) ] - ) + ), + OrchestrationTemplate::OrchestrationParameter.new( + :name => param_name('nic_ip_address', [vm_idx, nic_idx]), + :label => "NIC##{nic.idx} IP Address", + :description => "Ignored unless Mode is set to 'Static - Manual'", + :data_type => 'string', + :default_value => nic.ip_address, + :constraints => [ip_constraint] + ), ] + end + + OrchestrationTemplate::OrchestrationParameterGroup.new( + :label => "VM Instance Parameters for '#{vm.name}'", + :parameters => vm_parameters ) end + end + + def vapp_net_param_groups(template) + template.vapp_networks.each_with_index.map do |vapp_net, vapp_net_idx| + vapp_net_parameters = [ + OrchestrationTemplate::OrchestrationParameter.new( + :name => param_name('parent', [vapp_net_idx]), + :label => 'Parent Network', + :data_type => 'string', + :constraints => [ + OrchestrationTemplate::OrchestrationParameterAllowedDynamic.new(:fqname => '/Cloud/Orchestration/Operations/Methods/Available_Vdc_Networks') + ] + ), + OrchestrationTemplate::OrchestrationParameter.new( + :name => param_name('fence_mode', [vapp_net_idx]), + :label => 'Fence Mode', + :data_type => 'string', + :default_value => vapp_net.mode, + :required => true, + :constraints => [ + OrchestrationTemplate::OrchestrationParameterAllowed.new( + :allowed_values => { + 'isolated' => 'Isolated', + 'bridged' => 'Bridged', + 'natRouted' => 'NAT' + } + ) + ] + ) + ] + + vapp_net.subnets.each_with_index do |subnet, subnet_idx| + vapp_net_parameters += [ + OrchestrationTemplate::OrchestrationParameter.new( + :name => param_name('gateway', [vapp_net_idx, subnet_idx]), + :label => 'Gateway', + :data_type => 'string', + :default_value => subnet.gateway, + :constraints => [ip_constraint] + ), + OrchestrationTemplate::OrchestrationParameter.new( + :name => param_name('netmask', [vapp_net_idx, subnet_idx]), + :label => "Netmask", + :data_type => "string", + :default_value => subnet.netmask, + :constraints => [ip_constraint] + ), + OrchestrationTemplate::OrchestrationParameter.new( + :name => param_name('dns1', [vapp_net_idx, subnet_idx]), + :label => 'DNS 1', + :data_type => 'string', + :default_value => subnet.dns1, + :constraints => [ip_constraint] + ), + OrchestrationTemplate::OrchestrationParameter.new( + :name => param_name('dns2', [vapp_net_idx, subnet_idx]), + :label => 'DNS 2', + :data_type => 'string', + :default_value => subnet.dns2, + :constraints => [ip_constraint] + ) + ] + end - groups + OrchestrationTemplate::OrchestrationParameterGroup.new( + :label => "vApp Network Parameters for '#{vapp_net.name}'", + :parameters => vapp_net_parameters + ) + end end def deployment_options(_manager_class = nil) @@ -85,7 +242,21 @@ def deployment_options(_manager_class = nil) ] ) - super << availability_opt + vapp_template = OrchestrationTemplate::OrchestrationParameter.new( + :name => 'stack_template', + :label => 'vApp Template', + :description => 'vApp Template that this Service bases on', + :data_type => 'string', + :required => true, + :default_value => id, + :constraints => [ + OrchestrationTemplate::OrchestrationParameterAllowed.new( + :allowed_values => { id => name } + ) + ] + ) + + super << availability_opt << vapp_template end def self.eligible_manager_types @@ -104,4 +275,15 @@ def validate_format def self.display_name(number = 1) n_('vApp Template', 'vApp Templates', number) end + + def param_name(param, indeces = []) + "#{param}-#{indeces.join('-')}".chomp('-') + end + + def ip_constraint + OrchestrationTemplate::OrchestrationParameterPattern.new( + :pattern => '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$', + :description => 'IP address' + ) + end end diff --git a/app/models/manageiq/providers/vmware/cloud_manager/ovf_template.rb b/app/models/manageiq/providers/vmware/cloud_manager/ovf_template.rb new file mode 100644 index 000000000..304982719 --- /dev/null +++ b/app/models/manageiq/providers/vmware/cloud_manager/ovf_template.rb @@ -0,0 +1,206 @@ +class ManageIQ::Providers::Vmware::CloudManager::OvfTemplate + attr_accessor :vms, :vapp_networks + class OvfParseError < StandardError; end + OvfVM = Struct.new(:id, :name, :num_cores, :cores_per_socket, :memory_mb, :hostname, :disks, :nics) + OvfDisk = Struct.new(:id, :address, :capacity_mb) + OvfNIC = Struct.new(:idx, :network, :mode, :ip_address) + OvfVappNetwork = Struct.new(:name, :mode, :subnets) + OvfSubnet = Struct.new(:gateway, :netmask, :dns1, :dns2) + + def initialize(ovf_string) + @vms = [] + @vapp_networks = [] + parse(ovf_string) + end + + def vapp_network_names + @vapp_networks.map(&:name) + end + + def vm_id_from_idx(vm_idx) + @vms[vm_idx].id if @vms[vm_idx] + end + + def disk_id_from_idx(vm_idx, disk_idx) + return unless @vms[vm_idx] + return unless @vms[vm_idx].disks + @vms[vm_idx].disks[disk_idx].id if @vms[vm_idx].disks[disk_idx] + end + + def vapp_net_name_from_idx(vapp_net_idx) + @vapp_networks[vapp_net_idx].name if @vapp_networks[vapp_net_idx] + end + + private + + def parse(ovf_string) + ovf = MiqXml.load(ovf_string) + raise OvfParseError('OVF XML not valid xml') unless ovf + parse_vms(ovf.root) + parse_vapp_networks(ovf.root) + end + + def parse_vms(ovf) + ovf.each_element(vapp_xpaths(:vms)) do |el| + vm = OvfVM.new + vm.id = text(el, vm_xpaths(:id)) + vm.hostname = text(el, vm_xpaths(:hostname)) + vm.name = text(el, vm_xpaths(:name), :default => vm.hostname) + vm.num_cores = int(el, vm_xpaths(:num_cores)) + vm.cores_per_socket = int(el, vm_xpaths(:cores_per_socket), :default => vm.num_cores) + vm.memory_mb = int(el, vm_xpaths(:memory_mb), :default => 1024) + + # Disks. + vm.disks = [] + el.each_element(vm_xpaths(:disks)) do |d| + disk = OvfDisk.new + disk.id = text(d, disk_xpaths(:id)) + disk.address = text(d, disk_xpaths(:address)) + disk.capacity_mb = int(d, disk_xpaths(:capacity), :default => 0) / 2**20 # B -> MB + vm.disks << disk + end + + # NICs. + vm.nics = [] + el.each_element(vm_xpaths(:nics)) do |n| + nic = OvfNIC.new + nic.idx = text(n, nic_xpaths(:idx)) + nic.network = text_attr(n, nic_xpaths(:network_attr)) + nic.mode = text(n, nic_xpaths(:mode)) + nic.ip_address = text(n, nic_xpaths(:ip), :default => nil) + + nic.network = nil if nic.network == 'none' + nic.mode = 'DHCP' if nic.mode == 'NONE' + vm.nics << nic + end + + @vms << vm + end + end + + def parse_vapp_networks(ovf) + ovf.each_element(vapp_xpaths(:vapp_networks)) do |el| + vapp_net = OvfVappNetwork.new + vapp_net.name = text_attr(el, vapp_net_xpaths(:name_attr)) + vapp_net.mode = text(el, vapp_net_xpaths(:mode), :default => 'isolated') + + vapp_net.subnets = [] + el.find_match(vapp_net_xpaths(:ip_scopes)).each do |ip_scope| + subnet = OvfSubnet.new + subnet.gateway = text(ip_scope, ip_scope_xpaths(:gateway)) + subnet.netmask = text(ip_scope, ip_scope_xpaths(:netmask)) + subnet.dns1 = text(ip_scope, ip_scope_xpaths(:dns1)) + subnet.dns2 = text(ip_scope, ip_scope_xpaths(:dns2)) + vapp_net.subnets << subnet + end + + @vapp_networks << vapp_net + end + end + + def text(el, xpath, default: '') + (match = el.elements[xpath]) ? match.text : default + end + + def text_attr(el, xpath, default: '') + (match = el.elements[xpath]) ? match.value : default + end + + def int(el, xpath, default: 1) + (match = el.elements[xpath]) ? Integer(match.text) : default + end + + # Example: https://pubs.vmware.com/vcd-80/index.jsp?topic=%2Fcom.vmware.vcloud.api.reference.doc_90%2Fdoc%2Flanding-user_operations.html + # ResourceType definitions: https://blogs.vmware.com/vapp/2009/11/virtual-hardware-in-ovf-part-1.html + def vm_xpaths(key) + case key + when :id + './vcloud:GuestCustomizationSection/vcloud:VirtualMachineId' + when :name + './ovf:Name' + when :num_cores + "./ovf:VirtualHardwareSection/ovf:Item[rasd:ResourceType = '3']/rasd:VirtualQuantity" + when :cores_per_socket + "./ovf:VirtualHardwareSection/ovf:Item[rasd:ResourceType = '3']/vmw:CoresPerSocket" + when :memory_mb + "./ovf:VirtualHardwareSection/ovf:Item[rasd:ResourceType = '4']/rasd:VirtualQuantity" + when :disks + "./ovf:VirtualHardwareSection/ovf:Item[rasd:ResourceType = '17']" + when :hostname + "./vcloud:GuestCustomizationSection/vcloud:ComputerName" + when :nics + './/vcloud:NetworkConnection' + else + '' + end + end + + def vapp_xpaths(key) + case key + when :vapp_networks + "//vcloud:NetworkConfig[not(@networkName = 'none')]" + when :vapp_network_names_attr + "//vcloud:NetworkConfig[not(@networkName = 'none')]/@networkName" + when :vms + "//ovf:VirtualSystem" + else + '' + end + end + + def disk_xpaths(key) + case key + when :id + './rasd:InstanceID' + when :address + './rasd:AddressOnParent' + when :capacity + './rasd:VirtualQuantity' + else + '' + end + end + + def nic_xpaths(key) + case key + when :idx + './vcloud:NetworkConnectionIndex' + when :network_attr + '@network' + when :mode + './vcloud:IpAddressAllocationMode' + when :ip + './vcloud:IpAddress' + else + '' + end + end + + def vapp_net_xpaths(key) + case key + when :name_attr + '@networkName' + when :mode + './vcloud:Configuration/vcloud:FenceMode' + when :ip_scopes + './vcloud:Configuration/vcloud:IpScopes/vcloud:IpScope' + else + '' + end + end + + def ip_scope_xpaths(key) + case key + when :gateway + './vcloud:Gateway' + when :netmask + './vcloud:Netmask' + when :dns1 + './vcloud:Dns1' + when :dns2 + './vcloud:Dns2' + else + '' + end + end +end diff --git a/spec/fixtures/orchestration_templates/vmware_parameters_ovf.xml b/spec/fixtures/orchestration_templates/vmware_parameters_ovf.xml index f48eb4192..2aab7fb1c 100644 --- a/spec/fixtures/orchestration_templates/vmware_parameters_ovf.xml +++ b/spec/fixtures/orchestration_templates/vmware_parameters_ovf.xml @@ -28,6 +28,52 @@ false + + VM Network + + + + false + 192.168.254.1 + 255.255.255.0 + true + + + 192.168.254.100 + 192.168.254.199 + + + + + isolated + false + + false + + + + + + + true + 192.168.43.1 + 255.255.255.0 + 192.168.43.1 + true + + + 192.168.43.2 + 192.168.43.99 + + + + + + bridged + false + + false + Lease settings section @@ -91,6 +137,18 @@ byte + + 1 + Hard disk + Hard disk 2 + + 2001 + 2 + 17 + 42949672960 + byte + + 0 IDE Controller @@ -166,7 +224,7 @@ true true false - VM1 + vm-1 Specifies the available VM network connections @@ -195,10 +253,22 @@ vmx-10 - 00:50:56:01:00:4c + 00:50:56:01:00:5c 0 + true + RedHat Private network 43 + E1000s ethernet adapter on "RedHat Private network 43" + Network adapter 0 + 1 + E1000E + 10 + + + + 00:50:56:01:00:4c + 1 false - none + none Vmxnet3 ethernet adapter on "none" Network adapter 0 1 @@ -305,13 +375,20 @@ false true false - VM2 + vm-2 Specifies the available VM network connections 0 - + 0 + 192.168.43.100 + true + 00:50:56:01:00:5c + MANUAL + + + 1 false 00:50:56:01:00:4c NONE diff --git a/spec/models/manageiq/providers/vmware/cloud_manager/orchestration_service_option_converter_spec.rb b/spec/models/manageiq/providers/vmware/cloud_manager/orchestration_service_option_converter_spec.rb new file mode 100644 index 000000000..e6fe0baf1 --- /dev/null +++ b/spec/models/manageiq/providers/vmware/cloud_manager/orchestration_service_option_converter_spec.rb @@ -0,0 +1,138 @@ +describe ManageIQ::Providers::Vmware::CloudManager::OrchestrationServiceOptionConverter do + let(:converter) { described_class.new(nil) } + let(:valid_template) { FactoryGirl.create(:orchestration_template_vmware_cloud_in_xml) } + let(:dialog_options) do + { + # 1st vapp network + 'dialog_param_parent-0' => nil, + 'dialog_param_fence_mode-0' => 'isolated', + 'dialog_param_gateway-0-0' => '192.168.0.0', + 'dialog_param_netmask-0-0' => '255.255.255.0', + 'dialog_param_dns1-0-0' => '8.8.8.8', + 'dialog_param_dns2-0-0' => nil, + # 2nd vapp network + 'dialog_param_parent-1' => 'b915be99-1471-4e51-bcde-da2da791b98f', + 'dialog_param_fence_mode-1' => 'bridged', + 'dialog_param_gateway-1-0' => '192.168.0.1', + 'dialog_param_netmask-1-0' => '255.255.255.1', + 'dialog_param_dns1-1-0' => '1.2.3.4', + 'dialog_param_dns2-1-0' => '4.3.2.1', + # 1st VM + 'dialog_param_instance_name-0' => 'my VM1', + 'dialog_param_hostname-0' => 'my-vm-1', + 'dialog_param_num_cores-0' => 8, + 'dialog_param_cores_per_socket-0' => 4, + 'dialog_param_memory_mb-0' => 8192, + 'dialog_param_disk_mb-0-0' => 40_960, + 'dialog_param_disk_mb-0-1' => 20_480, + 'dialog_param_nic_network-0-0' => 'VM Network', + 'dialog_param_nic_mode-0-0' => 'MANUAL', + 'dialog_param_nic_ip_address-0-0' => '192.168.0.100', + # 2nd VM + 'dialog_param_instance_name-1' => 'my VM2', + 'dialog_param_hostname-1' => 'my-vm-2', + 'dialog_param_num_cores-1' => 4, + 'dialog_param_cores_per_socket-1' => 1, + 'dialog_param_memory_mb-1' => 2048, + 'dialog_param_disk_mb-1-0' => 4096, + 'dialog_param_nic_network-1-0' => 'RedHat Private network 43', + 'dialog_param_nic_mode-1-0' => 'DHCP', + 'dialog_param_nic_ip_address-1-0' => nil, + 'dialog_param_nic_network-1-1' => 'VM Network', + 'dialog_param_nic_mode-1-1' => 'POOL', + 'dialog_param_nic_ip_address-1-1' => nil + } + end + + describe '.stack_create_options' do + before do + allow(described_class).to receive(:get_template).and_return(valid_template) + converter.instance_variable_set(:@dialog_options, dialog_options) + end + + it 'vapp networks' do + options = converter.stack_create_options + expect(options).not_to be_nil + expect(options[:vapp_networks]).not_to be_nil + expect(options[:vapp_networks].count).to eq(2) + expect(options[:vapp_networks][0]).to have_attributes( + :name => 'VM Network', + :parent => nil, + :fence_mode => 'isolated', + :subnet => [ + { + :gateway => '192.168.0.0', + :netmask => '255.255.255.0', + :dns1 => '8.8.8.8', + :dns2 => nil + } + ] + ) + expect(options[:vapp_networks][1]).to have_attributes( + :name => 'RedHat Private network 43', + :parent => 'b915be99-1471-4e51-bcde-da2da791b98f', + :fence_mode => 'bridged', + :subnet => [ + { + :gateway => '192.168.0.1', + :netmask => '255.255.255.1', + :dns1 => '1.2.3.4', + :dns2 => '4.3.2.1' + } + ] + ) + end + + it 'vms' do + options = converter.stack_create_options + expect(options).not_to be_nil + expect(options[:source_vms]).not_to be_nil + expect(options[:source_vms].count).to eq(2) + expect(options[:source_vms][0]).to eq( + :name => 'my VM1', + :vm_id => 'vm-e9b55b85-640b-462c-9e7a-d18c47a7a5f3', + :guest_customization => { :ComputerName => 'my-vm-1' }, + :hardware => { + :cpu => { :num_cores => 8, :cores_per_socket => 4 }, + :memory => { :quantity_mb => 8192 }, + :disk => [ + { :id => '2000', :capacity_mb => 40_960 }, + { :id => '2001', :capacity_mb => 20_480 } + ] + }, + :networks => [ + { + :networkName => 'VM Network', + :IpAddressAllocationMode => 'MANUAL', + :IpAddress => '192.168.0.100', + :IsConnected => true + } + ] + ) + expect(options[:source_vms][1]).to eq( + :name => 'my VM2', + :vm_id => 'vm-04f85cca-3f8d-43b4-8473-7aa099f95c1b', + :guest_customization => { :ComputerName => 'my-vm-2' }, + :hardware => { + :cpu => { :num_cores => 4, :cores_per_socket => 1 }, + :memory => { :quantity_mb => 2048 }, + :disk => [{ :id => '2000', :capacity_mb => 4096 }] + }, + :networks => [ + { + :networkName => 'RedHat Private network 43', + :IpAddressAllocationMode => 'DHCP', + :IpAddress => nil, + :IsConnected => true + }, + { + :networkName => 'VM Network', + :IpAddressAllocationMode => 'POOL', + :IpAddress => nil, + :IsConnected => true + } + ] + ) + end + end +end diff --git a/spec/models/manageiq/providers/vmware/cloud_manager/orchestration_template_spec.rb b/spec/models/manageiq/providers/vmware/cloud_manager/orchestration_template_spec.rb index 13edee6ec..4a50696b2 100644 --- a/spec/models/manageiq/providers/vmware/cloud_manager/orchestration_template_spec.rb +++ b/spec/models/manageiq/providers/vmware/cloud_manager/orchestration_template_spec.rb @@ -49,19 +49,138 @@ end context "orchestration template" do - it "creates parameter groups for the given template" do - parameter_groups = orchestration_template.parameter_groups + let(:parameter_groups) { orchestration_template.parameter_groups } - expect(parameter_groups.size).to eq(3) + it "creates vapp parameter group for given template" do + assert_vapp_parameter_group(parameter_groups) + end + + [ + { + :vm_name => 'VM1', + :vm_id => 'e9b55b85-640b-462c-9e7a-d18c47a7a5f3', + :hostname => 'vm-1', + :num_cores => 2, + :cores_per_socket => 2, + :memory_mb => 2048, + :disks => [ + { :disk_id => '2000', :disk_address => '0', :size => 16_384 }, + { :disk_id => '2001', :disk_address => '1', :size => 40_960 } + ], + :nics => [{ :idx => '0', :network => nil, :mode => 'DHCP', :ip_address => nil }] + }, + { + :vm_name => 'VM2', + :vm_id => '04f85cca-3f8d-43b4-8473-7aa099f95c1b', + :hostname => 'vm-2', + :num_cores => 2, + :cores_per_socket => 2, + :memory_mb => 4096, + :disks => [{ :disk_id => '2000', :disk_address => '0', :size => 40_960 }], + :nics => [ + { :idx => '0', :network => 'RedHat Private network 43', :mode => 'MANUAL', :ip_address => '192.168.43.100' }, + { :idx => '1', :network => nil, :mode => 'DHCP', :ip_address => nil } + ] + } + ].each_with_index do |args, vm_idx| + it "creates specific vm parameter group - #{args[:vm_name]} - for given template" do + # Group exists. + vm_group = parameter_groups.detect { |g| g.label == "VM Instance Parameters for '#{args[:vm_name]}'" } + expect(vm_group).not_to be_nil + # Group has expected parameters. + assert_parameter_group( + vm_group, + 'instance_name' => { + :name => "instance_name-#{vm_idx}", + :label => 'Instance name', + :data_type => 'string', + :required => true, + :default_value => args[:vm_name], + :constraints => [] + }, + 'hostname' => { + :name => "hostname-#{vm_idx}", + :label => 'Instance Hostname', + :data_type => 'string', + :required => true, + :default_value => args[:hostname] + }, + 'num_cores' => { + :name => "num_cores-#{vm_idx}", + :label => 'Number of virtual CPUs', + :data_type => 'integer', + :required => true, + :default_value => args[:num_cores] + }, + 'cores_per_socket' => { + :name => "cores_per_socket-#{vm_idx}", + :label => 'Cores per socket', + :data_type => 'integer', + :required => true, + :default_value => args[:cores_per_socket] + }, + 'memory_mb' => { + :name => "memory_mb-#{vm_idx}", + :label => 'Total memory (MB)', + :data_type => 'integer', + :required => true, + :default_value => args[:memory_mb] + } + ) + assert_vm_disks(vm_group, args[:disks], vm_idx) + assert_vm_nics(vm_group, args[:nics], vm_idx) + # Group has not extra parameters. + expect(vm_group.parameters.size).to eq(5 + args[:disks].count + 3 * args[:nics].count) + end + end - assert_vapp_parameter_group(parameter_groups[0]) - assert_vm_parameter_group(parameter_groups[1], "VM1", "e9b55b85-640b-462c-9e7a-d18c47a7a5f3") - assert_vm_parameter_group(parameter_groups[2], "VM2", "04f85cca-3f8d-43b4-8473-7aa099f95c1b") + [ + { + :vapp_net_name => 'VM Network', + :parent => nil, + :mode => 'isolated', + :subnets => [{ :gateway => '192.168.254.1', :netmask => '255.255.255.0', :dns1 => '', :dns2 => '' }] + }, + { + :vapp_net_name => 'RedHat Private network 43', + :parent => nil, + :mode => 'bridged', + :subnets => [{ :gateway => '192.168.43.1', :netmask => '255.255.255.0', :dns1 => '192.168.43.1', :dns2 => '' }] + } + ].each_with_index do |args, vapp_net_idx| + it "creates specific vapp network parameter group - #{args[:vapp_net_name]} - for given template" do + # Group exists. + vapp_net_group = parameter_groups.detect { |g| g.label == "vApp Network Parameters for '#{args[:vapp_net_name]}'" } + expect(vapp_net_group).not_to be_nil + # Group has expected parameters. + assert_parameter_group( + vapp_net_group, + 'parent' => { + :name => "parent-#{vapp_net_idx}", + :label => 'Parent Network', + :data_type => 'string', + :required => nil, + :default_value => args[:parent] + }, + 'fence_mode' => { + :name => "fence_mode-#{vapp_net_idx}", + :label => 'Fence Mode', + :data_type => 'string', + :required => true, + :default_value => args[:mode] + }, + ) + assert_vapp_net_subnets(vapp_net_group, vapp_net_idx, args[:subnets]) + # Group has not extra parameters. + expect(vapp_net_group.parameters.size).to eq(2 + 4 * args[:subnets].count) + end end end end - def assert_vapp_parameter_group(group) + def assert_vapp_parameter_group(groups) + group = groups.detect { |g| g.label == 'vApp Parameters' } + expect(group).not_to be_nil expect(group.parameters.size).to eq(2) expect(group.parameters[0]).to have_attributes( @@ -82,24 +201,72 @@ def assert_vapp_parameter_group(group) expect(group.parameters[1].constraints[0]).to be_instance_of(OrchestrationTemplate::OrchestrationParameterBoolean) end - def assert_vm_parameter_group(group, vm_name, vm_id) - expect(group.parameters.size).to eq(2) + def assert_parameter_group(group, params) + params.each do |key, attrs| + parameter = group.parameters.detect { |p| p.name.start_with?(key) } + expect(parameter).not_to be_nil + assert_parameter(parameter, attrs) + end + end - assert_parameter(group.parameters[0], - :name => "instance_name-#{vm_id}", - :label => "Instance name", - :data_type => "string", - :default_value => vm_name) - - assert_parameter(group.parameters[1], - :name => "vdc_network-#{vm_id}", - :label => "Network", - :data_type => "string", - :default_value => "(default)") - - network_parameter = group.parameters[1] - expect(network_parameter.constraints.count).to eq(1) - expect(network_parameter.constraints[0].fqname).to eq("/Cloud/Orchestration/Operations/Methods/Available_Vdc_Networks") + def assert_vm_disks(group, disks, vm_idx) + disks.each_with_index do |disk, disk_idx| + disk_name = "disk_mb-#{vm_idx}-#{disk_idx}" + parameter = group.parameters.detect { |p| p.name == disk_name } + assert_parameter( + parameter, + :name => disk_name, + :label => "Disk #{disk[:disk_address]} (MB)", + :data_type => 'integer', + :required => true, + :default_value => disk[:size] + ) + end + end + + def assert_vm_nics(vm_group, nics, vm_idx) + nics.each_with_index do |nic, nic_idx| + suffix = "#{vm_idx}-#{nic_idx}" + assert_parameter_group( + vm_group, + "nic_network-#{suffix}" => { + :name => "nic_network-#{suffix}", + :label => "NIC##{nic[:idx]} Network", + :data_type => 'string', + :required => nil, + :default_value => nic[:network] + }, + "nic_mode-#{suffix}" => { + :name => "nic_mode-#{suffix}", + :label => "NIC##{nic[:idx]} Mode", + :data_type => 'string', + :required => true, + :default_value => nic[:mode] + }, + "nic_ip_address-#{suffix}" => { + :name => "nic_ip_address-#{suffix}", + :label => "NIC##{nic[:idx]} IP Address", + :data_type => 'string', + :required => nil, + :default_value => nic[:ip_address] + }, + ) + end + end + + def assert_vapp_net_subnets(vapp_net_group, vapp_net_idx, subnets) + subnets.each_with_index do |subnet, subnet_idx| + assert_parameter_group( + vapp_net_group, + "gateway" => { + :name => "gateway-#{vapp_net_idx}-#{subnet_idx}", + :label => 'Gateway', + :data_type => 'string', + :required => nil, + :default_value => subnet[:gateway] + } + ) + end end def assert_parameter(field, attributes) diff --git a/spec/models/manageiq/providers/vmware/cloud_manager/ovf_template_spec.rb b/spec/models/manageiq/providers/vmware/cloud_manager/ovf_template_spec.rb new file mode 100644 index 000000000..490f937c5 --- /dev/null +++ b/spec/models/manageiq/providers/vmware/cloud_manager/ovf_template_spec.rb @@ -0,0 +1,85 @@ +describe ManageIQ::Providers::Vmware::CloudManager::OvfTemplate do + let(:ovf_string) { File.read(ManageIQ::Providers::Vmware::Engine.root.join(*%w(spec fixtures orchestration_templates vmware_parameters_ovf.xml))) } + let(:instance) { described_class.new(ovf_string) } + + describe '.parse' do + let(:vm1) { instance.vms[0] } + let(:vm2) { instance.vms[1] } + let(:vapp_net1) { instance.vapp_networks[0] } + let(:vapp_net2) { instance.vapp_networks[1] } + + it 'vms' do + expect(instance.vms.count).to eq(2) + expect(vm1).to have_attributes( + :id => 'e9b55b85-640b-462c-9e7a-d18c47a7a5f3', + :name => 'VM1', + :hostname => 'vm-1', + :num_cores => 2, + :cores_per_socket => 2, + :memory_mb => 2048, + ) + expect(vm2).to have_attributes( + :id => '04f85cca-3f8d-43b4-8473-7aa099f95c1b', + :name => 'VM2', + :hostname => 'vm-2', + :num_cores => 2, + :cores_per_socket => 2, + :memory_mb => 4096, + ) + end + + it 'vm disks' do + expect(vm1.disks.count).to eq(2) + expect(vm1.disks[0]).to have_attributes( + :id => '2000', + :address => '0', + :capacity_mb => 16_384 + ) + expect(vm1.disks[1]).to have_attributes( + :id => '2001', + :address => '1', + :capacity_mb => 40_960 + ) + expect(vm2.disks[0]).to have_attributes( + :id => '2000', + :address => '0', + :capacity_mb => 40_960 + ) + end + + it 'vm NICs' do + expect(vm1.nics.count).to eq(1) + expect(vm1.nics[0]).to have_attributes( + :idx => '0', + :network => nil, + :mode => 'DHCP', + :ip_address => nil + ) + expect(vm2.nics.count).to eq(2) + expect(vm2.nics[0]).to have_attributes( + :idx => '0', + :network => 'RedHat Private network 43', + :mode => 'MANUAL', + :ip_address => '192.168.43.100' + ) + expect(vm2.nics[1]).to have_attributes( + :idx => '1', + :network => nil, + :mode => 'DHCP', + :ip_address => nil + ) + end + + it 'vapp networks' do + expect(instance.vapp_networks.count).to eq(2) + expect(vapp_net1).to have_attributes( + :name => 'VM Network', + :mode => 'isolated' + ) + expect(vapp_net2).to have_attributes( + :name => 'RedHat Private network 43', + :mode => 'bridged' + ) + end + end +end