Mohammed Morsi
2009-Jul-24 21:01 UTC
[Ovirt-devel] permit many-to-many vms / networks relationship redux
redux patchset permitting a vm to be associated with multiple networks and vice-versa. updated patchset so as to be applicable against current oVirt server HEAD these patches may be applied in any order, they all need to be pushed together
Mohammed Morsi
2009-Jul-24 21:01 UTC
[Ovirt-devel] [PATCH server] permit many-to-many vms / networks relationship redux (backend changes)
implements changes to the controllers, models, services, and taskomatic to permit vms to be associated with multiple networks. additional various fixes / cleanup to get things working --- src/app/controllers/pool_controller.rb | 2 +- src/app/controllers/vm_controller.rb | 55 +++++++++++++- src/app/models/network.rb | 2 - src/app/models/nic.rb | 60 +++++++++++---- src/app/models/vlan.rb | 14 +++- src/app/models/vm.rb | 15 ++-- src/app/services/network_service.rb | 12 ++-- src/app/services/nic_service.rb | 2 +- src/app/services/vm_service.rb | 39 +++++----- src/db/migrate/042_associate_vms_with_nics.rb | 101 +++++++++++++++++++++++++ src/task-omatic/task_vm.rb | 14 ++-- src/task-omatic/taskomatic.rb | 43 ++++++----- 12 files changed, 277 insertions(+), 82 deletions(-) create mode 100644 src/db/migrate/042_associate_vms_with_nics.rb diff --git a/src/app/controllers/pool_controller.rb b/src/app/controllers/pool_controller.rb index 44cb780..e86b064 100644 --- a/src/app/controllers/pool_controller.rb +++ b/src/app/controllers/pool_controller.rb @@ -96,7 +96,7 @@ class PoolController < ApplicationController def vms_json(args) attr_list = [:id, :description, :uuid, :num_vcpus_allocated, :memory_allocated_in_mb, - :vnic_mac_addr, :state, :calc_uptime, :id] + :state, :calc_uptime, :id] if (@pool.is_a? VmResourcePool) and @pool.get_hardware_pool.can_view(@user) attr_list.insert(3, [:host, :hostname]) end diff --git a/src/app/controllers/vm_controller.rb b/src/app/controllers/vm_controller.rb index 197241d..54c39cf 100644 --- a/src/app/controllers/vm_controller.rb +++ b/src/app/controllers/vm_controller.rb @@ -55,29 +55,31 @@ class VmController < ApplicationController def new alert = svc_new(params[:vm_resource_pool_id]) _setup_provisioning_options + _setup_network_options @storage_tree = VmResourcePool.find(params[:vm_resource_pool_id]).get_hardware_pool.storage_tree.to_json - @networks = Network.find(:all).collect{ |net| [net.name, net.id] } render :layout => 'popup' end def create params[:vm][:forward_vnc] = params[:forward_vnc] - alert = svc_create(params[:vm], params[:start_now]) + _parse_network_params(params) + alert = svc_create(params[:vm], params[:start_now], params[:nics]) render :json => { :object => "vm", :success => true, :alert => alert } end def edit svc_modify(params[:id]) _setup_provisioning_options - @networks = Network.find(:all).collect{ |net| [net.name, net.id] } + _setup_network_options @storage_tree = @vm.vm_resource_pool.get_hardware_pool.storage_tree(:vm_to_include => @vm).to_json render :layout => 'popup' end def update params[:vm][:forward_vnc] = params[:forward_vnc] + _parse_network_params(params) alert = svc_update(params[:id], params[:vm], params[:start_now], - params[:restart_now]) + params[:restart_now], params[:nics]) render :json => { :object => "vm", :success => true, :alert => alert } end @@ -164,4 +166,49 @@ class VmController < ApplicationController #if cobbler doesn't respond/is misconfigured/etc just don't add profiles end end + + # sets up a list of nics for the vm form + def _setup_network_options + net_conditions = "" + @nics = [] + + unless @vm.nil? + @vm.nics.each { |nic| + nnic = Nic.new(:mac => nic.mac, + :vm_id => @vm.id, + :network => nic.network) + if(nic.network.boot_type.proto == 'static') + nnic.ip_addresses << IpAddress.new(:address => nic.ip_address) + end + @nics.push nnic + + net_conditions += (net_conditions == "" ? "" : " AND ") + + "id != " + nic.network_id.to_s + } + end + + networks = Network.find(:all, :conditions => net_conditions) + + networks.each{ |net| + nnic = Nic.new(:mac => Nic::gen_mac, :network => net) + if(net.boot_type.proto == 'static') + nnic.ip_addresses << IpAddress.new(:address => '127.0.0.1') # FIXME + end + @nics.push nnic + } + + end + + # merges vm / network parameters as submitted on the vm form + def _parse_network_params(params) + params[:nics] = [] + unless params[:networks].nil? + params[:networks].each { |network, id| + params[:nics].push({ :mac => params[("nic_"+network.to_s).intern], + :network_id => network, + :bandwidth => 0}) + } + end + end + end diff --git a/src/app/models/network.rb b/src/app/models/network.rb index 0e2aa8c..a4b1b8b 100644 --- a/src/app/models/network.rb +++ b/src/app/models/network.rb @@ -22,8 +22,6 @@ class Network < ActiveRecord::Base has_and_belongs_to_many :usages, :join_table => 'networks_usages' - has_many :vms - validates_presence_of :type, :message => 'A type must be specified.' validates_presence_of :name, diff --git a/src/app/models/nic.rb b/src/app/models/nic.rb index e8b7768..51425bc 100644 --- a/src/app/models/nic.rb +++ b/src/app/models/nic.rb @@ -19,7 +19,10 @@ class Nic < ActiveRecord::Base belongs_to :host - belongs_to :physical_network + belongs_to :vm + + belongs_to :network + has_many :ip_addresses, :dependent => :destroy # FIXME bondings_nics table should just be replaced with @@ -33,24 +36,31 @@ class Nic < ActiveRecord::Base validates_format_of :mac, :with => %r{^([0-9a-fA-F]{2}([:-]|$)){6}$} - validates_presence_of :host_id, - :message => 'A host must be specified.' + # nic must be assigned to network if associated w/ a vm + validates_presence_of :network_id, + :unless => Proc.new { |nic| nic.vm.nil? } + + # nic must be associated w/ a vm if assigned to a vlan + validates_presence_of :vm_id, + :message => 'A vm must be specified.', + :if => Proc.new { |nic| !nic.network.nil? && nic.network.class == Vlan } + + # a vm / host can't have more than one nic on a network + validates_uniqueness_of :network_id, + :scope => [:host_id, :vm_id], + :unless => Proc.new { |nic| nic.network.nil? } validates_numericality_of :bandwidth, :greater_than_or_equal_to => 0 - validates_uniqueness_of :physical_network_id, - :scope => :host_id, - :unless => Proc.new { |nic| nic.physical_network_id.nil? } - # Returns whether the nic has networking defined. def networking? - (physical_network != nil) + (network != nil) end # Returns the boot protocol for the nic if networking is defined. def boot_protocol - return physical_network.boot_type.proto if networking? + return network.boot_type.proto if networking? end # Returns whether the nic is enslaved by a bonded interface. @@ -66,30 +76,50 @@ class Nic < ActiveRecord::Base # Returns the netmask for the nic if networking is defined. def netmask - return physical_network.ip_addresses.first.netmask if networking? && !physical_network.ip_addresses.empty? + return network.ip_addresses.first.netmask if networking? && !network.ip_addresses.empty? return nil end # Returns the broadcast address for the nic if networking is defined. def broadcast - return physical_network.ip_addresses.first.broadcast if networking? && !physical_network.ip_addresses.empty? + return network.ip_addresses.first.broadcast if networking? && !network.ip_addresses.empty? return nil end # Returns the gateway address fo rthe nic if networking is defined. def gateway - return physical_network.ip_addresses.first.gateway if networking? && !physical_network.ip_addresses.empty? + return network.ip_addresses.first.gateway if networking? && !network.ip_addresses.empty? + return nil + end + + def parent + return host if !host.nil? && vm.nil? + return vm if !vm.nil? && host.nil? + return nil + end + + def parent_pool + return host.hardware_pool if !host.nil? && vm.nil? + return vm.vm_resource_pool if !vm.nil? && host.nil? return nil end + def self.gen_mac + [ 0x00, 0x16, 0x3e, rand(0x7f), rand(0xff), + rand(0xff) ].collect {|x| "%02x" % x}.join(":") + end + # validate 'bridge' or 'usage_type' attribute ? protected def validate - if ! physical_network.nil? and - physical_network.boot_type.proto == 'static' and + # nic must be associated with a host or vm, but not both + errors.add("one host or one vm must be specified") unless host.nil? ^ vm.nil? + + if ! network.nil? and + network.boot_type.proto == 'static' and ip_addresses.size == 0 - errors.add("physical_network_id", + errors.add("network_id", "is static. Must create at least one static ip") end end diff --git a/src/app/models/vlan.rb b/src/app/models/vlan.rb index 2f6acba..217a89c 100644 --- a/src/app/models/vlan.rb +++ b/src/app/models/vlan.rb @@ -19,11 +19,23 @@ class Vlan < Network has_many :bondings + has_many :nics + validates_presence_of :number, :message => 'A number must be specified.' def is_destroyable? - bondings.empty? + bondings.empty? && nics.empty? end + protected + def validate + # ensure that any assigned nics only belong to vms, not hosts + nics.each{ |nic| + if nic.parent.class == Host + errors.add("nics", "must only be assigned to vms") + break + end + } + end end diff --git a/src/app/models/vm.rb b/src/app/models/vm.rb index 6d0f864..3e0a6fa 100644 --- a/src/app/models/vm.rb +++ b/src/app/models/vm.rb @@ -29,7 +29,7 @@ class Vm < ActiveRecord::Base end has_and_belongs_to_many :storage_volumes - belongs_to :network + has_many :nics, :dependent => :destroy has_many :smart_pool_tags, :as => :tagged, :dependent => :destroy has_many :smart_pools, :through => :smart_pool_tags @@ -53,8 +53,7 @@ class Vm < ActiveRecord::Base validates_presence_of :uuid, :description, :num_vcpus_allocated, :boot_device, :memory_allocated_in_mb, - :memory_allocated, :vnic_mac_addr, - :vm_resource_pool_id + :memory_allocated, :vm_resource_pool_id validates_format_of :uuid, :with => %r([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}) @@ -96,7 +95,7 @@ class Vm < ActiveRecord::Base :greater_than_or_equal_to => 0, :unless => Proc.new{ |vm| vm.memory_allocated.nil? } - acts_as_xapian :texts => [ :uuid, :description, :vnic_mac_addr, :state ], + acts_as_xapian :texts => [ :uuid, :description, :state ], :terms => [ [ :search_users, 'U', "search_users" ] ], :eager_load => :smart_pools @@ -119,8 +118,7 @@ class Vm < ActiveRecord::Base NEEDS_RESTART_FIELDS = [:uuid, :num_vcpus_allocated, - :memory_allocated, - :vnic_mac_addr] + :memory_allocated] STATE_PENDING = "pending" STATE_CREATING = "creating" @@ -420,6 +418,11 @@ class Vm < ActiveRecord::Base return i end + def self.gen_uuid + ["%02x"*4, "%02x"*2, "%02x"*2, "%02x"*2, "%02x"*6].join("-") % + Array.new(16) {|x| rand(0xff) } + end + def self.calc_uptime "vms.*, case when state='running' then (cast(total_uptime || ' sec' as interval) + diff --git a/src/app/services/network_service.rb b/src/app/services/network_service.rb index 3e7c997..221472d 100644 --- a/src/app/services/network_service.rb +++ b/src/app/services/network_service.rb @@ -205,13 +205,13 @@ module NetworkService def svc_modify_nic(id) authorize @nic = Nic.find(id) - @network = @nic.physical_network + @network = @nic.network network_options # filter out networks already assigned to nics on host network_conditions = [] - @nic.host.nics.each { |nic| - unless nic.physical_network.nil? || nic.id == @nic.id - network_conditions.push(" id != " + nic.physical_network.id.to_s) + @nic.parent.nics.each { |nic| + unless nic.network.nil? || nic.id == @nic.id + network_conditions.push(" id != " + nic.network.id.to_s) end } network_conditions = network_conditions.join(" AND ") @@ -228,8 +228,8 @@ module NetworkService def svc_update_nic(id, nic_hash, ip_hash) authorize network_options - unless nic_hash[:physical_network_id].to_i == 0 - @network = Network.find(nic_hash[:physical_network_id]) + unless nic_hash[:network_id].to_i == 0 + @network = Network.find(nic_hash[:network_id]) if @network.boot_type.id == @static_boot_type.id if ip_hash[:id] == "New" svc_create_ip_address(ip_hash) diff --git a/src/app/services/nic_service.rb b/src/app/services/nic_service.rb index 12314a4..231194a 100644 --- a/src/app/services/nic_service.rb +++ b/src/app/services/nic_service.rb @@ -33,7 +33,7 @@ module NicService private def lookup(id, priv) @nic = Nic.find(id) - authorized!(priv, at nic.host.hardware_pool) + authorized!(priv, at nic.parent_pool) end end diff --git a/src/app/services/vm_service.rb b/src/app/services/vm_service.rb index 073b819..76e4158 100644 --- a/src/app/services/vm_service.rb +++ b/src/app/services/vm_service.rb @@ -82,8 +82,8 @@ module VmService def svc_new(vm_resource_pool_id) raise ActionError.new("VM Resource Pool is required.") unless vm_resource_pool_id - new_vm_hash = {:vm_resource_pool_id => vm_resource_pool_id} - default_mac_and_uuid(new_vm_hash) + new_vm_hash = {:vm_resource_pool_id => vm_resource_pool_id, + :uuid => Vm::gen_uuid} @vm = Vm.new(new_vm_hash) authorized!(Privilege::MODIFY, @vm.vm_resource_pool) end @@ -94,8 +94,8 @@ module VmService # [<tt>@vm</tt>] the newly saved Vm # === Required permissions # [<tt>Privilege::MODIFY</tt>] for the vm's VmResourcePool - def svc_create(vm_hash, start_now) - default_mac_and_uuid(vm_hash) + def svc_create(vm_hash, start_now, nics) + vm_hash[:uuid] = Vm::gen_uuid unless vm_hash[:uuid] vm_hash[:state] = Vm::STATE_PENDING @vm = Vm.new(vm_hash) authorized!(Privilege::MODIFY, at vm.vm_resource_pool) @@ -103,6 +103,10 @@ module VmService alert = "VM was successfully created." Vm.transaction do @vm.save! + nics.each{ |nic| + nic[:vm_id] = @vm.id + Nic.create(nic) + } vm_provision @task = VmTask.new({ :user => @user, :task_target => @vm, @@ -129,7 +133,7 @@ module VmService # [<tt>@vm</tt>] stores the Vm with +id+ # === Required permissions # [<tt>Privilege::MODIFY</tt>] for the Vm's VmResourcePool - def svc_update(id, vm_hash, start_now, restart_now) + def svc_update(id, vm_hash, start_now, restart_now, nics) lookup(id,Privilege::MODIFY) #needs restart if certain fields are changed @@ -153,6 +157,9 @@ module VmService alert = "VM was successfully updated." Vm.transaction do + @vm.nics.clear + nics.each{ |nic| @vm.nics.push Nic.new(nic) } + @vm.update_attributes!(vm_hash) vm_provision if start_now @@ -285,10 +292,15 @@ module VmService unless system system = Cobbler::System.new("name" => name, @vm.cobbler_type => @vm.cobbler_name) - system.interfaces = [Cobbler::NetworkInterface.new( - {'mac_address' => @vm.vnic_mac_addr})] + system.interfaces = [] + # do we need to do anything if vm.nics is empty ? + @vm.nics.each { |nic| + system.interfaces.push Cobbler::NetworkInterface.new( + {'mac_address' => nic.mac}) + } system.save end + # TODO update system if found end end @@ -305,17 +317,4 @@ module VmService @vm = Vm.find(id) authorized!(priv, at vm.vm_resource_pool) end - - def default_mac_and_uuid(vm_hash) - unless vm_hash[:uuid] - vm_hash[:uuid] = ["%02x"*4, "%02x"*2, "%02x"*2, - "%02x"*2, "%02x"*6].join("-") % - Array.new(16) {|x| rand(0xff) } - end - unless vm_hash[:vnic_mac_addr] - vm_hash[:vnic_mac_addr] = [ 0x00, 0x16, 0x3e, rand(0x7f), rand(0xff), - rand(0xff) ].collect {|x| "%02x" % x}.join(":") - end - end - end diff --git a/src/db/migrate/042_associate_vms_with_nics.rb b/src/db/migrate/042_associate_vms_with_nics.rb new file mode 100644 index 0000000..0e457c0 --- /dev/null +++ b/src/db/migrate/042_associate_vms_with_nics.rb @@ -0,0 +1,101 @@ +# Copyright (C) 2009 Red Hat, Inc. +# Written by Mohammed Morsi <mmorsi at redhat.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. A copy of the GNU General Public License is +# also available at http://www.gnu.org/copyleft/gpl.html. + +class AssociateVmsWithNics < ActiveRecord::Migration + + def self.up + # assocate nics w/ vms + add_column :nics, :vm_id, :integer + execute "alter table nics add constraint + fk_nics_vm_id + foreign key (vm_id) references + vms(id)" + execute "alter table nics alter column host_id DROP NOT NULL" + + # change physical_network_id column in nic table to network_id + execute 'alter table nics drop constraint fk_nic_networks' + execute 'alter table nics rename column physical_network_id to network_id' + execute 'alter table nics add constraint fk_nic_networks + foreign key (network_id) references networks(id)' + + + # create a nic for each vm / network + Vm.find(:all, :conditions => "network_id IS NOT NULL").each{ |vm| + nic = Nic.new(:mac => vm.vnic_mac_addr, + :network_id => vm.network_id, + :vm_id => vm.id, + :bandwidth => 0) + nic.vm = vm + vm.nics.push nic + + vm.save! + nic.save! + } + + # removes 1toM networks/vms relationship + # remove network_id column from vms table + # remove vnic_mac_addr column from vms table + execute 'alter table vms drop constraint fk_vm_network_id' + remove_column :vms, :network_id + remove_column :vms, :vnic_mac_addr + + # add to the db some validations + # host_id is set xor vm_id is set + execute 'alter table nics add constraint host_xor_vm + check (host_id IS NOT NULL and vm_id IS NULL or + vm_id IS NOT NULL and host_id IS NULL)' + # network_id is set if vm_id is + execute 'alter table nics add constraint vm_nic_has_network + check (vm_id IS NULL or network_id IS NOT NULL)' + # vm_id is set if network is vlan (TBD) + end + + def self.down + # drop constraints added to nics table + execute 'alter table nics drop constraint host_xor_vm' + execute 'alter table nics drop constraint vm_nic_has_network' + + # add network_id, vnic_mac_addr column back to vm table + add_column :vms, :network_id, :integer + add_column :vms, :vnic_mac_addr, :string + execute 'alter table vms add constraint fk_vm_network_id + foreign key (network_id) references networks(id)' + + # copy network id over + # NOTE since we're going from a MtoM relationship to a 1toM + # we're just gonna associate the last network found + # w/ the vm, so this operation is lossy + Nic.find(:all, :conditions => 'vm_id IS NOT NULL').each{ |nic| + vm = Vm.find(nic.vm_id) + vm.vnic_mac_addr = nic.mac + vm.network_id = nic.network_id + vm.save! + nic.destroy + } + + # unassociate nics / vms + remove_column :nics, :vm_id + execute "alter table nics alter column host_id SET NOT NULL" + + # change nic::network_id back to nic::physical_network_id + execute 'alter table nics drop constraint fk_nic_networks' + execute 'alter table nics rename column network_id to physical_network_id' + execute 'alter table nics add constraint fk_nic_networks + foreign key (physical_network_id) references networks(id)' + end +end diff --git a/src/task-omatic/task_vm.rb b/src/task-omatic/task_vm.rb index a448d30..dd71747 100644 --- a/src/task-omatic/task_vm.rb +++ b/src/task-omatic/task_vm.rb @@ -35,7 +35,7 @@ end def create_vm_xml(name, uuid, memAllocated, memUsed, vcpus, bootDevice, - macAddr, bridge, diskDevices) + net_interfaces, diskDevices) doc = Document.new doc.add_element("domain", {"type" => "kvm"}) @@ -87,11 +87,13 @@ def create_vm_xml(name, uuid, memAllocated, memUsed, vcpus, bootDevice, which_device += 1 end - unless macAddr.nil? || bridge.nil? || macAddr == "" || bridge == "" - doc.root.elements["devices"].add_element("interface", {"type" => "bridge"}) - doc.root.elements["devices"].elements["interface"].add_element("mac", {"address" => macAddr}) - doc.root.elements["devices"].elements["interface"].add_element("source", {"bridge" => bridge}) - end + net_interfaces.each { |nic| + interface = Element.new("interface") + interface.add_attribute("type", "bridge") + interface.add_element("mac", {"address" => nic[:mac]}) + interface.add_element("source", {"bridge" => nic[:interface]}) + doc.root.elements["devices"] << interface + } doc.root.elements["devices"].add_element("input", {"type" => "mouse", "bus" => "ps2"}) doc.root.elements["devices"].add_element("graphics", {"type" => "vnc", "port" => "-1", "listen" => "0.0.0.0"}) diff --git a/src/task-omatic/taskomatic.rb b/src/task-omatic/taskomatic.rb index 8088769..96b2ec4 100755 --- a/src/task-omatic/taskomatic.rb +++ b/src/task-omatic/taskomatic.rb @@ -168,6 +168,8 @@ class TaskOmatic and node.memory >= db_vm.memory_allocated \ and not curr.is_disabled.nil? and curr.is_disabled == 0 \ and ((!vm or vm.active == 'false') or vm.node != node.object_id) + # FIXME ensure host is on all networks a vm's assigned to + # db_vm.nics.each { |nic| ignore if nic.network ! in host } possible_hosts.push(curr) end end @@ -360,31 +362,32 @@ class TaskOmatic @logger.debug("Connecting volumes: #{volumes}") storagedevs = connect_storage_pools(node, volumes) - # determine if vm has been assigned to physical or - # virtual network and assign nic / bonding accordingly - # FIXME instead of trying to find a nic or bonding here, given - # a specified host and network, we should try earlier on to find a host - # that has a nic / bonding on the specified network - - net_device = "breth0" # FIXME remove this default value at some point, tho net_device can't be nil - unless db_vm.network.nil? - if db_vm.network.class == PhysicalNetwork + # loop through each nic/network assigned to vm, + # finding necessary host devices to bridge + net_interfaces = [] + db_vm.nics.each { |nic| + device = net_device = nil + if nic.network.class == PhysicalNetwork device = Nic.find(:first, - :conditions => ["host_id = ? AND physical_network_id = ?", - db_host.id, db_vm.network_id ]) - net_device = "br" + device.interface_name unless device.nil? - - else + :conditions => ["host_id = ? AND network_id = ?", + db_host.id, nic.network_id ]) + else device = Bonding.find(:first, - :conditions => ["host_id = ? AND vlan_id = ?", - db_host.id, db_vm.network_id]) - net_device = "br" + device.interface_name unless device.nil? - end - end + :conditions => ["host_id = ? AND vlan_id = ?", + db_host.id, nic.network_id ]) + end + + unless device.nil? + net_device = "br" + device.interface_name + else + net_device = "breth0" # FIXME remove this default at some point + end + net_interfaces.push({ :mac => nic.mac, :interface => net_device }) + } xml = create_vm_xml(db_vm.description, db_vm.uuid, db_vm.memory_allocated, db_vm.memory_used, db_vm.num_vcpus_allocated, db_vm.boot_device, - db_vm.vnic_mac_addr, net_device, storagedevs) + net_interfaces, storagedevs) @logger.debug("XML Domain definition: #{xml}") -- 1.6.0.6
Seemingly Similar Threads
- permit many-to-many vms / networks relationship
- [PATCH server] permit many-to-many vms / networks relationship
- [PATCH server] provide default uuid and mac if not provided for create.
- [PATCH] Virtio support
- [PATCH 1/1] Introduce an option to always pxe-boot a vm.