diff --git a/lib/fog/libvirt/compute.rb b/lib/fog/libvirt/compute.rb
index 14f6c20..bf7949c 100644
--- a/lib/fog/libvirt/compute.rb
+++ b/lib/fog/libvirt/compute.rb
@@ -38,6 +38,11 @@ class Compute < Fog::Service
request :upload_volume
request :clone_volume
request :list_networks
+ request :define_network
+ request :create_network
+ request :update_network
+ request :update_network_autostart
+ request :update_network_section
request :destroy_network
request :dhcp_leases
request :list_interfaces
diff --git a/lib/fog/libvirt/models/compute/attribute_model.rb b/lib/fog/libvirt/models/compute/attribute_model.rb
new file mode 100644
index 0000000..0a71fb4
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/attribute_model.rb
@@ -0,0 +1,38 @@
+require "fog/core/model"
+require_relative "clonable_model"
+
+module Fog
+ module Libvirt
+ class Compute
+ class AttributeModel < Fog::Model
+ include ClonableModel
+
+ def initialize(attributes = {}) # rubocop:disable Lint/MissingSuper
+ # don't call super because we want to allow regular :service attribute
+ merge_attributes(attributes)
+ end
+
+ def self.cast(item)
+ return item if item.is_a?(self)
+
+ new(item)
+ end
+
+ def ==(other)
+ return super unless other.is_a?(Fog::Model)
+ return false unless self.class == other.class
+
+ other.attributes.compact == attributes.compact
+ end
+
+ def hash
+ attributes.compact.hash
+ end
+
+ def eql?(other)
+ self == other
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/clonable_model.rb b/lib/fog/libvirt/models/compute/clonable_model.rb
new file mode 100644
index 0000000..d83e2da
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/clonable_model.rb
@@ -0,0 +1,22 @@
+require "fog/core/model"
+
+module Fog
+ module Libvirt
+ class Compute
+ module ClonableModel
+ def clone
+ copy = super
+ copy.instance_variable_set(:@attributes, Marshal.load(Marshal.dump(attributes)))
+ copy
+ end
+
+ def dup
+ copy = super
+ copy.instance_variable_set(:@attributes, Marshal.load(Marshal.dump(attributes)))
+ copy.identity = nil if copy.respond_to?(:identity=)
+ copy
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/network.rb b/lib/fog/libvirt/models/compute/network.rb
index b6282fc..005d662 100644
--- a/lib/fog/libvirt/models/compute/network.rb
+++ b/lib/fog/libvirt/models/compute/network.rb
@@ -1,42 +1,496 @@
require 'fog/core/model'
require 'fog/libvirt/models/compute/util/util'
+require 'fog/libvirt/models/compute/network/bandwidth'
+require 'fog/libvirt/models/compute/network/bridge'
+require 'fog/libvirt/models/compute/network/dhcp'
+require 'fog/libvirt/models/compute/network/dns'
+require 'fog/libvirt/models/compute/network/dnsmasq'
+require 'fog/libvirt/models/compute/network/domain'
+require 'fog/libvirt/models/compute/network/forward'
+require 'fog/libvirt/models/compute/network/ip'
+require 'fog/libvirt/models/compute/network/portgroup'
+require 'fog/libvirt/models/compute/network/route'
+require 'fog/libvirt/models/compute/network/virtualport'
+require 'fog/libvirt/models/compute/network/vlan'
+require 'fog/libvirt/models/compute/clonable_model'
+require 'nokogiri'
module Fog
module Libvirt
class Compute
class Network < Fog::Model
include Fog::Libvirt::Util
+ include ClonableModel
identity :uuid
+
+ attribute :persistent
+ attribute :active
+ attribute :autostart
+
+ attribute :ipv6
+ attribute :trust_guest_rx_filters
+
attribute :name
- attribute :bridge_name
- attribute :xml
+ attribute :metadata
+ attribute :title
+ attribute :description
+ attribute :bridge
+ attribute :mtu
+ attribute :domain
+ attribute :forward
+ attribute :bandwidth
+ attribute :virtualport
+ attribute :vlan
+ attribute :portgroups, :type => Array
+ attribute :isolated
+ attribute :mac
+ attribute :dns
+ attribute :ips, :type => Array
+ attribute :routes, :type => Array
+ attribute :dnsmasq
+
+ autocast_on_assign :bridge, Bridge
+ autocast_on_assign :domain, Domain
+ autocast_on_assign :forward, Forward
+ autocast_on_assign :bandwidth, Bandwidth
+ autocast_on_assign :virtualport, Virtualport
+ autocast_on_assign :vlan, Vlan
+ autocast_on_assign :portgroups, [Portgroup]
+ autocast_on_assign :dns, Dns
+ autocast_on_assign :ips, [Ip]
+ autocast_on_assign :routes, [Route]
+ autocast_on_assign :dnsmasq, Dnsmasq
+
+ attr_reader :xml
def initialize(attributes = {})
- super
+ @xml = attributes.delete(:xml)
+ preloaded = attributes.delete(:preloaded)
+ super(defaults.merge(attrs_underscore(attributes)))
+ @saved_attributes = preloaded ? Marshal.load(Marshal.dump(self.attributes)) : nil
end
- def dhcp_leases(mac, flags = 0)
- service.dhcp_leases(uuid, mac, flags)
+ def bridge_name
+ bridge&.name
+ end
+
+ def bridge_name=(name)
+ return if name.to_s.empty?
+
+ self.bridge = Bridge.new(:name => name) if bridge.nil?
+ bridge.name = name
+ end
+
+ def active?
+ !!(@saved_attributes.nil? ? active : @saved_attributes[:active])
+ end
+
+ def autostart?
+ !!(@saved_attributes.nil? ? autostart : @saved_attributes[:autostart])
+ end
+
+ def persistent?
+ !!(@saved_attributes.nil? ? persistent : @saved_attributes[:persistent])
end
def save
- raise Fog::Errors::Error.new('Creating a new network is not yet implemented. Contributions welcome!')
+ requires :name
+ update_network
+ reload
+ end
+
+ def reload
+ requires :identity
+
+ object = collection.get(identity)
+
+ return unless object
+
+ merge_attributes(object.all_associations_and_attributes)
+
+ @xml = object.xml
+ @saved_attributes = Marshal.load(Marshal.dump(attributes))
+ self
+ end
+
+ def start
+ return self if active?
+
+ service.client.lookup_network_by_uuid(uuid).create
+ reload
+ true
end
def shutdown
+ return unless active?
+
service.destroy_network(uuid)
+ if persistent?
+ reload
+ else
+ @saved_attributes = { :active => false, :persistent => false }
+ self.uuid = nil
+ self.active = true
+ self.persistent = false
+ end
+ true
+ end
+
+ alias stop shutdown
+
+ def destroy
+ shutdown
+ if persistent?
+ service.client.lookup_network_by_uuid(uuid).undefine if uuid
+ @saved_attributes = { :active => false, :persistent => false }
+ self.uuid = nil
+ self.active = false
+ self.persistent = true
+ end
+ true
+ end
+
+ def enable_autostart
+ service.update_network_autostart(uuid, true)
+ self.autostart = true
+ @saved_attributes[:autostart] = autostart if @saved_attributes
+ end
+
+ def disable_autostart
+ service.update_network_autostart(uuid, false)
+ self.autostart = false
+ @saved_attributes[:autostart] = autostart if @saved_attributes
+ end
+
+ def dhcp_leases(mac, flags = 0)
+ service.dhcp_leases(uuid, mac, flags)
+ end
+
+ def self.parse_xml(xml)
+ node = find_xml_network(xml)
+ return nil unless node
+
+ attrs = xml_attrs(node)
+ attrs[:xml] = node.to_xml
+
+ parse_xml_general(attrs, node)
+ parse_xml_models(attrs, node)
+ parse_xml_various(attrs, node)
+ parse_xml_lists(attrs, node)
+
+ attrs.compact
+ end
+
+ def self.find_xml_network(xml)
+ return nil unless xml
+
+ xml = Nokogiri::XML(xml) unless xml.is_a?(Nokogiri::XML::Node)
+ return xml unless xml.is_a?(Nokogiri::XML::Document)
+
+ xml.at_xpath("//network")
+ end
+
+ private_class_method def self.parse_xml_general(attrs, node)
+ attrs[:name] = node.at_xpath("name")&.content
+ attrs[:uuid] = node.at_xpath("uuid")&.content
+ attrs[:title] = node.at_xpath("title")&.content
+ attrs[:description] = node.at_xpath("description")&.content
+ attrs[:metadata] = node.at_xpath("metadata")&.to_xml
+ end
+
+ private_class_method def self.parse_xml_models(attrs, node)
+ attrs[:forward] = Forward.parse_xml(node.at_xpath("forward"))
+ attrs[:bridge] = Bridge.parse_xml(node.at_xpath("bridge"))
+ attrs[:domain] = Domain.parse_xml(node.at_xpath("domain"))
+ attrs[:dns] = Dns.parse_xml(node.at_xpath("dns"))
+ attrs[:vlan] = Vlan.parse_xml(node.at_xpath("vlan"))
+ attrs[:bandwidth] = Bandwidth.parse_xml(node.at_xpath("bandwidth"))
+ attrs[:virtualport] = Virtualport.parse_xml(node.at_xpath("virtualport"))
+ attrs[:dnsmasq] = Dnsmasq.parse_xml(node.at_xpath("dnsmasq:options", :dnsmasq => Dnsmasq::NAMESPACE))
+ attrs.delete(:dnsmasq) if attrs[:dnsmasq].nil?
+ end
+
+ private_class_method def self.parse_xml_various(attrs, node)
+ mtu_node = node.at_xpath("mtu")
+ attrs[:mtu] = mtu_node["size"]&.to_i if mtu_node
+
+ mac_node = node.at_xpath("mac")
+ attrs[:mac] = mac_node["address"] if mac_node
+
+ attrs[:isolated] = xml_attrs(node.at_xpath("port"))[:isolated]
+ attrs.delete(:isolated) if attrs[:isolated].nil?
+ end
+
+ private_class_method def self.parse_xml_lists(attrs, node)
+ attrs[:ips] = node.xpath("ip").map { |ip| Ip.parse_xml(ip) }
+ attrs.delete(:ips) if attrs[:ips].empty?
+
+ attrs[:routes] = node.xpath("route").map { |route| Route.parse_xml(route) }
+ attrs.delete(:routes) if attrs[:routes].empty?
+
+ attrs[:portgroups] = node.xpath("portgroup").map { |portgroup| Portgroup.parse_xml(portgroup) }
+ attrs.delete(:portgroups) if attrs[:portgroups].empty?
end
def to_xml
- builder = Nokogiri::XML::Builder.new do |xml|
- xml.network do
- xml.name(name)
- xml.bridge(:name => bridge_name, :stp => 'on', :delay => '0')
+ document, network = prepare_xml_document
+
+ attrs_xml(attributes.slice(:ipv6, :trust_guest_rx_filters).compact).each { |name, value| network[name] = value }
+
+ Nokogiri::XML::Builder.with(network) do |xml|
+ build_xml_general(xml)
+ build_xml_content1(xml)
+ build_xml_content2(xml)
+ build_xml_lists(xml)
+ end
+
+ document.to_xml
+ end
+
+ # Apply partial update using libvirt section updates for changed attributes
+ def save_fragment(new_items, old_items, parent_index: -1, enforce_order: false)
+ item_to_xml = ->(item) { Nokogiri::XML::Builder.new { |xml| item.build_xml(xml) }.to_xml }
+
+ updated = false
+ changes = changeset(new_items, old_items, :enforce_order => enforce_order)
+
+ changes[:modify].each do |item|
+ service.update_network_section(uuid, :modify, item.section, item_to_xml.call(item), { :parent_index => parent_index, :persist => persistent?, :live => active? })
+ updated = true
+ end
+
+ changes[:remove].each do |item|
+ service.update_network_section(uuid, :delete, item.section, item_to_xml.call(item), { :parent_index => parent_index, :persist => persistent?, :live => active? })
+ updated = true
+ end
+
+ changes[:add].each do |item|
+ service.update_network_section(uuid, :add_last, item.section, item_to_xml.call(item), { :parent_index => parent_index, :persist => persistent?, :live => active? })
+ updated = true
+ end
+
+ updated
+ end
+
+ def dup
+ copy = super
+ copy.name = "#{copy.name}-copy"
+ copy.instance_variable_set(:@saved_attributes, nil)
+ copy.active = false
+ copy
+ end
+
+ private
+
+ def defaults
+ {
+ :persistent => true,
+ :active => false,
+ :autostart => false
+ }
+ end
+
+ def prepare_xml_document
+ if @xml
+ document = Nokogiri::XML(@xml)
+ network = remove_managed_xml_items(document.at_xpath("//network"))
+ end
+
+ unless network
+ document = Nokogiri::XML::Document.new
+ network = Nokogiri::XML::Node.new("network", document)
+ document.add_child(network)
+ end
+
+ [document, network]
+ end
+
+ def remove_managed_xml_items(network)
+ return nil unless network
+
+ %w[ipv6 trust_guest_rx_filters connections].each { |attr| network.delete(attr_camelcase(attr)) }
+ hash_except(attributes, :ipv6, :trust_guest_rx_filters, :isolated, :persistent, :active, :autostart).each do |attr, value|
+ attr = attr.to_s.chomp("s") if value.is_a?(Array)
+ network.xpath(attr.to_s).each(&:remove)
+ end
+ network.xpath("port").each(&:remove)
+ network.xpath("dnsmasq:options", :dnsmasq => Dnsmasq::NAMESPACE).each(&:remove)
+ network.xpath("//text()").find_all { |text| text.content.strip.empty? }.map(&:remove)
+ network
+ end
+
+ def build_xml_general(xml)
+ xml.name(name)
+ xml.uuid(uuid) if uuid
+ xml.title(title) if title
+ xml.description(description) if description
+ xml << metadata unless metadata.to_s.empty?
+ end
+
+ def build_xml_content1(xml)
+ forward&.build_xml(xml)
+ bridge&.build_xml(xml)
+ xml.mtu(:size => mtu) if mtu
+ xml.mac(:address => mac) if mac
+ domain&.build_xml(xml)
+ end
+
+ def build_xml_content2(xml)
+ dns&.build_xml(xml)
+ vlan&.build_xml(xml)
+ bandwidth&.build_xml(xml)
+
+ xml.port(:isolated => value_xml(isolated)) unless isolated.nil?
+
+ virtualport&.build_xml(xml)
+ dnsmasq&.build_xml(xml)
+ end
+
+ def build_xml_lists(xml)
+ ips.each { |ip| model_cast(ip, Ip).build_xml(xml) }
+ routes.each { |route| model_cast(route, Route).build_xml(xml) }
+ portgroups.each { |pg| model_cast(pg, Portgroup).build_xml(xml) }
+ end
+
+ def update_network
+ updates = find_updates
+
+ if updates[:fragment]
+ perform_fragment_update
+ elsif updates[:full]
+ perform_full_update
+ end
+
+ if updates[:active]
+ if active
+ service.client.lookup_network_by_uuid(uuid).create
+ else
+ service.destroy_network(uuid)
end
end
+ service.update_network_autostart(uuid, autostart) if updates[:autostart]
- builder.to_xml
+ self
+ end
+
+ def find_updates
+ updates = {
+ :full => false,
+ :fragment => false,
+ :active => false,
+ :autostart => false
+ }
+
+ have_changes = find_partial_updates(updates)
+ updates[:full] = have_changes && !updates[:fragment]
+
+ if updates[:full]
+ # When performing full update then active and autostart will be updated anyway
+ updates[:active] = false
+ updates[:autostart] = false
+ end
+
+ updates
+ end
+
+ # Returns true if there have been any changes
+ # comparing with previously saved attributes
+ # if no attributes have been saved previously
+ # that is considered as having changes
+ def find_partial_updates(updates)
+ return true unless @saved_attributes
+
+ have_changes = false
+
+ attributes.each_key do |name|
+ next if [:active, :autostart].include?(name)
+ next if models_equal?(attributes[name], @saved_attributes[name])
+
+ have_changes = true
+ updates[:fragment] = fragment_only?(name)
+ break unless updates[:fragment]
+ end
+
+ # When both are false then we enforce active
+ self.active = true if !persistent && !active
+
+ updates[:active] = @saved_attributes[:active] != attributes[:active]
+ updates[:autostart] = persistent? && @saved_attributes[:autostart] != attributes[:autostart]
+
+ have_changes
+ end
+
+ def perform_fragment_update
+ Forward.save_fragment(forward, @saved_attributes[:forward], self)
+ Portgroup.save_fragment(portgroups, @saved_attributes[:portgroups], self)
+ Dns.save_fragment(dns, @saved_attributes[:dns], self)
+ Ip.save_fragment(ips, @saved_attributes[:ips], self)
+ end
+
+ def perform_full_update
+ current_uuid = @saved_attributes.to_h[:uuid] || uuid
+ network = nil
+ if current_uuid
+ begin
+ network = service.client.lookup_network_by_uuid(current_uuid)
+ rescue ::Libvirt::RetrieveError
+ # not present so will create it
+ end
+ end
+ self.uuid = service.update_network(network, to_xml, persistent, active, autostart).uuid
+ end
+
+ # Check if save_fragment could be used for all changes
+ def fragment_only?(name)
+ case name
+ when :forward
+ Forward.fragment_only?(forward, @saved_attributes[:forward])
+ when :portgroups
+ Portgroup.fragment_only?(portgroups, @saved_attributes[:portgroups])
+ when :dns
+ Dns.fragment_only?(dns, @saved_attributes[:dns])
+ when :ips
+ Ip.fragment_only?(ips, @saved_attributes[:ips])
+ else
+ false
+ end
+ end
+
+ def changeset(new_items, old_items, enforce_order: false)
+ changes = {
+ :modify => [],
+ :remove => old_items.dup,
+ :add => []
+ }
+
+ new_items.each_with_index do |new_item, new_index|
+ old_index = old_items.find_index { |old_item| old_item == new_item }
+ if old_index
+ wrong_order = enforce_order && new_index != old_index
+ changeset_modify(changes, new_item, old_items[old_index], wrong_order)
+ else
+ changes[:add] << new_item
+ end
+ end
+
+ changes
+ end
+
+ def changeset_modify(changes, new_item, old_item, wrong_order)
+ if models_equal?(new_item, old_item)
+ if wrong_order
+ changes[:add] << new_item
+ else
+ changes[:remove].delete(old_item)
+ end
+ elsif new_item.respond_to?(:update_modify?) && !new_item.update_modify?(service)
+ # libvirt doesn't support modify for some section updates so need to delete/add instead
+ changes[:add] << new_item
+ else
+ changes[:modify] << new_item
+ changes[:remove].delete(old_item)
+ end
end
end
end
diff --git a/lib/fog/libvirt/models/compute/network/bandwidth.rb b/lib/fog/libvirt/models/compute/network/bandwidth.rb
new file mode 100644
index 0000000..51f3e58
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/network/bandwidth.rb
@@ -0,0 +1,58 @@
+require "fog/core/model"
+require_relative "../attribute_model"
+require_relative "../util/util"
+
+module Fog
+ module Libvirt
+ class Compute
+ class Network < Fog::Model
+ class Bandwidth < Fog::Libvirt::Compute::AttributeModel
+ include Fog::Libvirt::Util
+
+ attribute :inbound
+ attribute :outbound
+
+ remove_method :inbound=
+
+ def inbound=(average)
+ attributes[:inbound] = if average.is_a?(String) || average.is_a?(Integer)
+ { :average => average.to_i }
+ else
+ average
+ end
+ end
+
+ remove_method :outbound=
+
+ def outbound=(average)
+ attributes[:outbound] = if average.is_a?(String) || average.is_a?(Integer)
+ { :average => average.to_i }
+ else
+ average
+ end
+ end
+
+ def self.parse_xml(node)
+ return nil unless node
+
+ attrs = {}
+ attrs[:inbound] = xml_attrs(node.at_xpath("inbound")).compact.transform_values(&:to_i)
+ attrs.delete(:inbound) if attrs[:inbound].empty?
+
+ attrs[:outbound] = xml_attrs(node.at_xpath("outbound")).compact.transform_values(&:to_i)
+ attrs.delete(:outbound) if attrs[:outbound].empty?
+
+ attrs
+ end
+
+ def build_xml(xml)
+ xml.bandwidth do
+ xml.inbound(inbound.compact) unless inbound.to_h.empty?
+ xml.outbound(outbound.compact) unless outbound.to_h.empty?
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/network/bridge.rb b/lib/fog/libvirt/models/compute/network/bridge.rb
new file mode 100644
index 0000000..ec523bf
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/network/bridge.rb
@@ -0,0 +1,45 @@
+require "fog/core/model"
+require_relative "../util/util"
+
+module Fog
+ module Libvirt
+ class Compute
+ class Network < Fog::Model
+ class Bridge < Fog::Model
+ include Fog::Libvirt::Util
+
+ identity :name
+ attribute :zone
+ attribute :stp
+ attribute :delay, :type => Integer
+ attribute :mac_table_manager
+
+ def initialize(attributes = {})
+ super(normalize_attrs(attributes))
+ end
+
+ def self.parse_xml(node)
+ return nil unless node
+
+ attrs = xml_attrs(node)
+ attrs[:delay] = attrs[:delay].to_i if attrs.key?(:delay)
+ attrs
+ end
+
+ def build_xml(xml)
+ attrs = attrs_xml(attributes)
+ attrs[:stp] = xml_switch(stp) # libvirt uses on/off for this rather than yes/no
+ xml.bridge(attrs.compact)
+ end
+
+ private
+
+ def normalize_attrs(attrs)
+ attrs = { :name => attrs } if attrs.is_a?(String)
+ attrs_underscore(attrs)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/network/dhcp.rb b/lib/fog/libvirt/models/compute/network/dhcp.rb
new file mode 100644
index 0000000..a8e4a33
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/network/dhcp.rb
@@ -0,0 +1,79 @@
+require "fog/core/model"
+require_relative "../attribute_model"
+require_relative "../util/util"
+require_relative "dhcp_range"
+require_relative "dhcp_host"
+
+module Fog
+ module Libvirt
+ class Compute
+ class Network < Fog::Model
+ class Dhcp < Fog::Libvirt::Compute::AttributeModel
+ include Fog::Libvirt::Util
+
+ attribute :ranges, :type => Array
+ attribute :hosts, :type => Array
+ attribute :bootp
+
+ autocast_on_assign :ranges, [DhcpRange]
+ autocast_on_assign :hosts, [DhcpHost]
+
+ remove_method :bootp=
+
+ def bootp=(bootp)
+ attributes[:bootp] = if bootp.nil? || bootp.is_a?(Hash)
+ bootp
+ else
+ { :file => bootp.to_s }
+ end
+ end
+
+ def self.parse_xml(node)
+ return nil unless node
+
+ attrs = {}
+ attrs[:ranges] = node.xpath("range").map { |range_node| DhcpRange.parse_xml(range_node) }
+ attrs.delete(:ranges) if attrs[:ranges].empty?
+
+ attrs[:hosts] = node.xpath("host").map { |host_node| DhcpHost.parse_xml(host_node) }
+ attrs.delete(:hosts) if attrs[:hosts].empty?
+
+ attrs[:bootp] = xml_attrs(node.at_xpath("bootp"))
+ attrs.delete(:bootp) if attrs[:bootp].empty?
+
+ attrs
+ end
+
+ def build_xml(xml)
+ xml.dhcp do
+ ranges.each { |range| DhcpRange.cast(range).build_xml(xml) }
+ hosts.each { |host| DhcpHost.cast(host).build_xml(xml) }
+ xml.bootp(bootp.compact) if bootp
+ end
+ end
+
+ def fragment_only?(other)
+ bootp == other.bootp
+ end
+
+ def self.fragment_only?(new, old)
+ old ||= self.new
+ new ||= self.new
+ new.fragment_only?(old)
+ end
+
+ def self.save_fragment(new, old, parent_index, network)
+ return false if old.nil? && new.nil?
+
+ old ||= self.new
+ new ||= self.new
+ updated = false
+ updated |= DhcpRange.save_fragment(new.ranges, old.ranges, parent_index, network)
+ updated |= DhcpHost.save_fragment(new.hosts, old.hosts, parent_index, network)
+ updated
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/network/dhcp_host.rb b/lib/fog/libvirt/models/compute/network/dhcp_host.rb
new file mode 100644
index 0000000..3d08dde
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/network/dhcp_host.rb
@@ -0,0 +1,55 @@
+require "fog/core/model"
+require_relative "../attribute_model"
+require_relative "../util/util"
+require_relative "dhcp_lease"
+
+module Fog
+ module Libvirt
+ class Compute
+ class Network < Fog::Model
+ class DhcpHost < Fog::Libvirt::Compute::AttributeModel
+ include Fog::Libvirt::Util
+
+ attribute :mac
+ attribute :ip
+ attribute :name
+ attribute :id
+
+ attribute :lease
+ autocast_on_assign :lease, DhcpLease
+
+ def ==(other)
+ return super unless other.is_a?(DhcpHost)
+
+ (!mac.nil? && mac == other.mac) ||
+ (!ip.nil? && ip.to_s == other.ip.to_s) ||
+ (!name.nil? && name == other.name)
+ end
+
+ def self.parse_xml(node)
+ return nil unless node
+
+ attrs = xml_attrs(node)
+ attrs[:lease] = DhcpLease.parse_xml(node.at_xpath("lease"))
+ attrs.delete(:lease) unless attrs[:lease]
+ attrs
+ end
+
+ def build_xml(xml)
+ xml.host(hash_except(attributes, :lease).compact.transform_values(&:to_s)) do
+ lease&.build_xml(xml)
+ end
+ end
+
+ def section
+ :dhcp_host
+ end
+
+ def self.save_fragment(new, old, parent_index, network)
+ network.save_fragment(models_cast(new, self), old, :parent_index => parent_index)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/network/dhcp_lease.rb b/lib/fog/libvirt/models/compute/network/dhcp_lease.rb
new file mode 100644
index 0000000..3498e5f
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/network/dhcp_lease.rb
@@ -0,0 +1,41 @@
+require "fog/core/model"
+require_relative "../attribute_model"
+require_relative "../util/util"
+
+module Fog
+ module Libvirt
+ class Compute
+ class Network < Fog::Model
+ class DhcpLease < Fog::Libvirt::Compute::AttributeModel
+ include Fog::Libvirt::Util
+
+ attribute :expiry, :type => Integer
+ attribute :unit
+
+ def initialize(attributes = {})
+ super(normalize_attrs(attributes))
+ end
+
+ def self.parse_xml(node)
+ return nil unless node
+
+ attrs = xml_attrs(node)
+ attrs[:expiry] = attrs[:expiry].to_i if attrs.key?(:expiry)
+ attrs
+ end
+
+ def build_xml(xml)
+ xml.lease(attributes.compact)
+ end
+
+ private
+
+ def normalize_attrs(attrs)
+ attrs = { :expiry => attrs.to_i } if attrs.is_a?(Integer) || attrs.is_a?(String)
+ attrs
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/network/dhcp_range.rb b/lib/fog/libvirt/models/compute/network/dhcp_range.rb
new file mode 100644
index 0000000..1efc62e
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/network/dhcp_range.rb
@@ -0,0 +1,68 @@
+require "fog/core/model"
+require_relative "../attribute_model"
+require_relative "../util/util"
+require_relative "dhcp_lease"
+
+module Fog
+ module Libvirt
+ class Compute
+ class Network < Fog::Model
+ class DhcpRange < Fog::Libvirt::Compute::AttributeModel
+ include Fog::Libvirt::Util
+
+ attribute :start
+ attribute :end
+
+ attribute :lease
+ autocast_on_assign :lease, DhcpLease
+
+ def initialize(attributes = {})
+ super(normalize_attrs(attributes))
+ end
+
+ def ==(other)
+ return super unless other.is_a?(DhcpRange)
+
+ start.to_s == other.start.to_s &&
+ self.end.to_s == other.end.to_s
+ end
+
+ def self.parse_xml(node)
+ return nil unless node
+
+ attrs = xml_attrs(node)
+ attrs[:lease] = DhcpLease.parse_xml(node.at_xpath("lease"))
+ attrs.delete(:lease) unless attrs[:lease]
+ attrs
+ end
+
+ def build_xml(xml)
+ xml.range(hash_except(attributes, :lease).compact.transform_values(&:to_s)) do
+ lease&.build_xml(xml)
+ end
+ end
+
+ def update_modify?(_service)
+ # Libvirt doesn't support MODIFY for IP_DHCP_RANGE
+ false
+ end
+
+ def section
+ :dhcp_range
+ end
+
+ def self.save_fragment(new, old, parent_index, network)
+ network.save_fragment(models_cast(new, self), old, :parent_index => parent_index)
+ end
+
+ private
+
+ def normalize_attrs(attrs)
+ attrs = normalize_ip_range(attrs) if attrs.is_a?(Range)
+ attrs
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/network/dns.rb b/lib/fog/libvirt/models/compute/network/dns.rb
new file mode 100644
index 0000000..b89496b
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/network/dns.rb
@@ -0,0 +1,88 @@
+require "fog/core/model"
+require_relative "../attribute_model"
+require_relative "../util/util"
+require_relative "dns_forwarder"
+require_relative "dns_txt"
+require_relative "dns_host"
+require_relative "dns_srv"
+
+module Fog
+ module Libvirt
+ class Compute
+ class Network < Fog::Model
+ class Dns < Fog::Libvirt::Compute::AttributeModel
+ include Fog::Libvirt::Util
+
+ attribute :enable
+ attribute :forward_plain_names
+ attribute :forwarders, :type => Array
+ attribute :hosts, :type => Array
+ attribute :txts, :type => Array
+ attribute :srvs, :type => Array
+
+ autocast_on_assign :forwarders, [DnsForwarder]
+ autocast_on_assign :hosts, [DnsHost]
+ autocast_on_assign :txts, [DnsTxt]
+ autocast_on_assign :srvs, [DnsSrv]
+
+ def initialize(attributes = {})
+ super(attrs_underscore(attributes))
+ end
+
+ def self.parse_xml(node)
+ return nil unless node
+
+ attrs = xml_attrs(node)
+
+ attrs[:forwarders] = node.xpath("forwarder").map { |forwarder_node| DnsForwarder.parse_xml(forwarder_node) }
+ attrs.delete(:forwarders) if attrs[:forwarders].empty?
+
+ attrs[:hosts] = node.xpath("host").map { |host_node| DnsHost.parse_xml(host_node) }
+ attrs.delete(:hosts) if attrs[:hosts].empty?
+
+ attrs[:txts] = node.xpath("txt").map { |txt_node| DnsTxt.parse_xml(txt_node) }
+ attrs.delete(:txts) if attrs[:txts].empty?
+
+ attrs[:srvs] = node.xpath("srv").map { |srv_node| DnsSrv.parse_xml(srv_node) }
+ attrs.delete(:srvs) if attrs[:srvs].empty?
+
+ attrs
+ end
+
+ def build_xml(xml)
+ attrs = attrs_xml(hash_except(attributes, :forwarders, :hosts, :txts, :srvs))
+ xml.dns(attrs.compact) do
+ forwarders.each { |forwarder| model_cast(forwarder, DnsForwarder).build_xml(xml) }
+ hosts.each { |host| model_cast(host, DnsHost).build_xml(xml) }
+ txts.each { |txt| model_cast(txt, DnsTxt).build_xml(xml) }
+ srvs.each { |srv| model_cast(srv, DnsSrv).build_xml(xml) }
+ end
+ end
+
+ def fragment_only?(other)
+ hash_except(attributes, :forwarders, :hosts, :txts, :srvs) == hash_except(other.attributes, :forwarders, :hosts, :txts, :srvs) &&
+ models_equal?(forwarders, other.forwarders)
+ end
+
+ def self.fragment_only?(new, old)
+ old ||= self.new
+ new ||= self.new
+ new.fragment_only?(old)
+ end
+
+ def self.save_fragment(new, old, network)
+ return false if old.nil? && new.nil?
+
+ old ||= self.new
+ new ||= self.new
+ updated = false
+ updated |= DnsHost.save_fragment(new.hosts, old.hosts, network)
+ updated |= DnsTxt.save_fragment(new.txts, old.txts, network)
+ updated |= DnsSrv.save_fragment(new.srvs, old.srvs, network)
+ updated
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/network/dns_forwarder.rb b/lib/fog/libvirt/models/compute/network/dns_forwarder.rb
new file mode 100644
index 0000000..83fdb81
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/network/dns_forwarder.rb
@@ -0,0 +1,44 @@
+require "fog/core/model"
+require_relative "../attribute_model"
+require_relative "../util/util"
+
+module Fog
+ module Libvirt
+ class Compute
+ class Network < Fog::Model
+ class DnsForwarder < Fog::Libvirt::Compute::AttributeModel
+ include Fog::Libvirt::Util
+
+ attribute :domain
+ attribute :addr
+ attribute :port
+
+ def initialize(attributes = {})
+ super(normalize_attrs(attributes))
+ end
+
+ def self.parse_xml(node)
+ return nil unless node
+
+ attrs = xml_attrs(node)
+ attrs[:port] = attrs[:port].to_i if attrs.key?(:port)
+ attrs
+ end
+
+ def build_xml(xml)
+ xml.forwarder(attributes.compact)
+ end
+
+ private
+
+ def normalize_attrs(attrs)
+ attrs = { :addr => attrs.to_s } unless attrs.is_a?(Hash)
+ attrs[:port] = attrs[:port].to_i unless attrs[:port].to_s.empty?
+ attrs[:port] = attrs.delete("port").to_i unless attrs["port"].to_s.empty?
+ attrs
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/network/dns_host.rb b/lib/fog/libvirt/models/compute/network/dns_host.rb
new file mode 100644
index 0000000..5699f7d
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/network/dns_host.rb
@@ -0,0 +1,44 @@
+require "fog/core/model"
+require_relative "../util/util"
+require_relative "../clonable_model"
+
+module Fog
+ module Libvirt
+ class Compute
+ class Network < Fog::Model
+ class DnsHost < Fog::Model
+ include Fog::Libvirt::Util
+ include ClonableModel
+
+ identity :ip
+ attribute :hostnames, :type => Array
+
+ def self.parse_xml(node)
+ return nil unless node
+
+ { :ip => node["ip"], :hostnames => node.xpath("hostname").map(&:content) }
+ end
+
+ def build_xml(xml)
+ xml.host(:ip => ip.to_s) do
+ hostnames.each { |hostname| xml.hostname(hostname) }
+ end
+ end
+
+ def section
+ :dns_host
+ end
+
+ def update_modify?(service)
+ # support since libvirt >= 10.6.0
+ service.client.libversion >= 10_006_000
+ end
+
+ def self.save_fragment(new, old, network)
+ network.save_fragment(models_cast(new, self), old)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/network/dns_srv.rb b/lib/fog/libvirt/models/compute/network/dns_srv.rb
new file mode 100644
index 0000000..d317ca0
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/network/dns_srv.rb
@@ -0,0 +1,59 @@
+require "fog/core/model"
+require_relative "../attribute_model"
+require_relative "../util/util"
+
+module Fog
+ module Libvirt
+ class Compute
+ class Network < Fog::Model
+ class DnsSrv < Fog::Libvirt::Compute::AttributeModel
+ include Fog::Libvirt::Util
+
+ attribute :service
+ attribute :protocol
+ attribute :domain
+ attribute :target
+ attribute :port, :type => Integer
+ attribute :priority, :type => Integer
+ attribute :weight, :type => Integer
+
+ def ==(other)
+ return super unless other.is_a?(DnsSrv)
+
+ service == other.service &&
+ protocol == other.protocol &&
+ domain == other.domain &&
+ target == other.target
+ end
+
+ def self.parse_xml(node)
+ return nil unless node
+
+ attrs = xml_attrs(node)
+ attrs[:port] = attrs[:port].to_i if attrs.key?(:port)
+ attrs[:priority] = attrs[:priority].to_i if attrs.key?(:priority)
+ attrs[:weight] = attrs[:weight].to_i if attrs.key?(:weight)
+ attrs
+ end
+
+ def build_xml(xml)
+ xml.srv(attributes.compact)
+ end
+
+ def section
+ :dns_srv
+ end
+
+ def update_modify?(_service)
+ # Libvirt doesn't support MODIFY for DNS_SRV
+ false
+ end
+
+ def self.save_fragment(new, old, network)
+ network.save_fragment(models_cast(new, self), old)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/network/dns_txt.rb b/lib/fog/libvirt/models/compute/network/dns_txt.rb
new file mode 100644
index 0000000..1a18abb
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/network/dns_txt.rb
@@ -0,0 +1,40 @@
+require "fog/core/model"
+require_relative "../util/util"
+
+module Fog
+ module Libvirt
+ class Compute
+ class Network < Fog::Model
+ class DnsTxt < Fog::Model
+ include Fog::Libvirt::Util
+
+ identity :name
+ attribute :value
+
+ def self.parse_xml(node)
+ return nil unless node
+
+ xml_attrs(node)
+ end
+
+ def build_xml(xml)
+ xml.txt(attributes.compact)
+ end
+
+ def section
+ :dns_txt
+ end
+
+ def update_modify?(service)
+ # support since libvirt >= 10.6.0
+ service.client.libversion >= 10_006_000
+ end
+
+ def self.save_fragment(new, old, network)
+ network.save_fragment(models_cast(new, self), old)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/network/dnsmasq.rb b/lib/fog/libvirt/models/compute/network/dnsmasq.rb
new file mode 100644
index 0000000..4689dde
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/network/dnsmasq.rb
@@ -0,0 +1,50 @@
+require "fog/core/model"
+require_relative "../attribute_model"
+
+module Fog
+ module Libvirt
+ class Compute
+ class Network < Fog::Model
+ class Dnsmasq < Fog::Libvirt::Compute::AttributeModel
+ NAMESPACE = "http://libvirt.org/schemas/network/dnsmasq/1.0".freeze
+
+ attribute :options, :type => Array
+
+ def initialize(attributes = {})
+ super(normalize_attrs(attributes))
+ end
+
+ def self.parse_xml(node)
+ return nil unless node
+
+ options = node.xpath("dnsmasq:option", "dnsmasq" => NAMESPACE).map { |option_node| option_node["value"] }
+ { :options => options }
+ end
+
+ def build_xml(xml)
+ return if options.empty?
+
+ xml.doc.root.add_namespace("dnsmasq", NAMESPACE)
+
+ xml["dnsmasq"].options do
+ options.each do |option|
+ xml["dnsmasq"].option({ :value => option })
+ end
+ end
+ end
+
+ private
+
+ def normalize_attrs(attrs)
+ if attrs.is_a?(String)
+ attrs = { :options => [attrs] }
+ elsif attrs.is_a?(Array)
+ attrs = { :options => attrs }
+ end
+ attrs
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/network/domain.rb b/lib/fog/libvirt/models/compute/network/domain.rb
new file mode 100644
index 0000000..2821102
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/network/domain.rb
@@ -0,0 +1,37 @@
+require "fog/core/model"
+require_relative "../util/util"
+
+module Fog
+ module Libvirt
+ class Compute
+ class Network < Fog::Model
+ class Domain < Fog::Model
+ include Fog::Libvirt::Util
+
+ identity :name
+ attribute :local_only
+ attribute :register
+
+ def initialize(attributes = {})
+ super(normalize_attrs(attributes))
+ end
+
+ def self.parse_xml(node)
+ return nil unless node
+
+ xml_attrs(node)
+ end
+
+ def build_xml(xml)
+ xml.domain(attrs_xml(attributes).compact)
+ end
+
+ def normalize_attrs(attrs)
+ attrs = { :name => attrs } if attrs.is_a?(String)
+ attrs_underscore(attrs)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/network/forward.rb b/lib/fog/libvirt/models/compute/network/forward.rb
new file mode 100644
index 0000000..7aa935e
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/network/forward.rb
@@ -0,0 +1,121 @@
+require "fog/core/model"
+require_relative "../attribute_model"
+require_relative "../util/util"
+require_relative "nat"
+require_relative "forward_interface"
+require_relative "pci_address"
+
+module Fog
+ module Libvirt
+ class Compute
+ class Network < Fog::Model
+ class Forward < Fog::Libvirt::Compute::AttributeModel
+ include Fog::Libvirt::Util
+
+ attribute :mode
+ attribute :managed
+
+ attribute :nat
+ attribute :interfaces, :type => Array
+ attribute :pf
+ attribute :driver
+ attribute :addresses, :type => Array
+
+ autocast_on_assign :nat, Nat
+ autocast_on_assign :interfaces, [ForwardInterface]
+ autocast_on_assign :addresses, [PciAddress]
+
+ def initialize(attributes = {})
+ attrs = normalize_attrs(attributes)
+ dev = attrs.delete(:dev)
+ super(attrs)
+ self.dev = dev if dev
+ end
+
+ def dev
+ interfaces&.first&.dev
+ end
+
+ def dev=(dev)
+ if dev.nil?
+ attributes[:interfaces] = []
+ else
+ attributes[:interfaces] ||= []
+ interface = attributes[:interfaces].find { |interface| interface.dev == dev }
+ if interface
+ attributes[:interfaces].unshift(attributes[:interfaces].delete(interface))
+ else
+ attributes[:interfaces] = [ForwardInterface.new(dev)]
+ end
+ end
+ end
+
+ def self.parse_xml(node)
+ return nil unless node
+
+ attrs = xml_attrs(node)
+ attrs[:mode] = attrs[:mode].to_sym if attrs.key?(:mode)
+
+ attrs[:nat] = Nat.parse_xml(node.at_xpath("nat"))
+ attrs.delete(:nat) unless attrs[:nat]
+
+ attrs[:pf] = node.at_xpath("pf").to_h["dev"]
+ attrs.delete(:pf) if attrs[:pf].nil?
+
+ attrs[:driver] = node.at_xpath("driver").to_h["model"]
+ attrs.delete(:driver) if attrs[:driver].nil?
+
+ parse_xml_lists(attrs, node)
+
+ attrs
+ end
+
+ private_class_method def self.parse_xml_lists(attrs, node)
+ attrs[:interfaces] = node.xpath("interface").map { |interface_node| ForwardInterface.parse_xml(interface_node) }
+ attrs.delete(:interfaces) if attrs[:interfaces].empty?
+
+ attrs[:addresses] = node.xpath("address").map { |address_node| PciAddress.parse_xml(address_node) }
+ attrs.delete(:addresses) if attrs[:addresses].empty?
+ end
+
+ def build_xml(xml)
+ attrs = attrs_xml(hash_except(attributes, :nat, :interfaces, :pf, :driver, :addresses))
+ attrs[:dev] = dev
+
+ xml.forward(attrs.compact) do
+ nat&.build_xml(xml)
+ interfaces.each { |interface| model_cast(interface, ForwardInterface).build_xml(xml) }
+ xml.pf(:dev => pf) if pf
+ xml.driver(:model => driver) if driver
+ addresses.each { |address| model_cast(address, PciAddress).build_xml(xml) }
+ end
+ end
+
+ def fragment_only?(other)
+ hash_except(attributes, :interfaces, :nat, :addresses) == hash_except(other.attributes, :interfaces, :nat, :addresses) &&
+ models_equal?(nat, other.nat) && models_equal?(addresses, other.addresses)
+ end
+
+ def self.fragment_only?(new, old)
+ old ||= self.new
+ new ||= self.new
+ new.fragment_only?(old)
+ end
+
+ def self.save_fragment(new, old, network)
+ old ||= self.new
+ new ||= self.new
+ ForwardInterface.save_fragment(new.interfaces, old.interfaces, network)
+ end
+
+ private
+
+ def normalize_attrs(attrs)
+ attrs = { :mode => attrs.to_sym } unless attrs.is_a?(Hash)
+ attrs
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/network/forward_interface.rb b/lib/fog/libvirt/models/compute/network/forward_interface.rb
new file mode 100644
index 0000000..fb2c9ee
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/network/forward_interface.rb
@@ -0,0 +1,50 @@
+require "fog/core/model"
+require_relative "../util/util"
+
+module Fog
+ module Libvirt
+ class Compute
+ class Network < Fog::Model
+ class ForwardInterface < Fog::Model
+ include Fog::Libvirt::Util
+
+ identity :dev
+
+ def initialize(attributes = {})
+ super(normalize_attrs(attributes))
+ end
+
+ def self.parse_xml(node)
+ return nil unless node
+
+ xml_attrs(node)
+ end
+
+ def build_xml(xml)
+ xml.interface(hash_except(attributes, :connections).compact)
+ end
+
+ def section
+ :forward_interface
+ end
+
+ def update_modify?(_service)
+ # Libvirt doesn't support MODIFY for FORWARD_INTERFACE
+ false
+ end
+
+ def self.save_fragment(new, old, network)
+ network.save_fragment(models_cast(new, self), old, :parent_index => -1, :enforce_order => true)
+ end
+
+ private
+
+ def normalize_attrs(attrs)
+ attrs = { :dev => attrs } if attrs.is_a?(String)
+ attrs
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/network/ip.rb b/lib/fog/libvirt/models/compute/network/ip.rb
new file mode 100644
index 0000000..9a3bef3
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/network/ip.rb
@@ -0,0 +1,102 @@
+require "fog/core/model"
+require_relative "../attribute_model"
+require_relative "../util/util"
+require_relative "dhcp"
+
+module Fog
+ module Libvirt
+ class Compute
+ class Network < Fog::Model
+ class Ip < Fog::Libvirt::Compute::AttributeModel
+ include Fog::Libvirt::Util
+
+ attribute :address
+ attribute :netmask
+ attribute :prefix
+ attribute :family
+ attribute :local_ptr
+
+ attribute :tftp
+ attribute :dhcp
+ autocast_on_assign :dhcp, Dhcp
+
+ def initialize(attributes = {})
+ super(normalize_attrs(attributes))
+ end
+
+ def identity
+ [address.to_s, netmask.to_s, prefix.to_s].join("/")
+ end
+
+ def self.parse_xml(node)
+ return nil unless node
+
+ attrs = xml_attrs(node)
+ attrs[:prefix] = attrs[:prefix].to_i if attrs.key?(:prefix)
+ attrs[:family] = attrs[:family].to_sym if attrs.key?(:family)
+
+ attrs[:tftp] = node.at_xpath("tftp").to_h["root"]
+ attrs.delete(:tftp) if attrs[:tftp].nil?
+
+ attrs[:dhcp] = Dhcp.parse_xml(node.at_xpath("dhcp"))
+ attrs.delete(:dhcp) unless attrs[:dhcp]
+
+ attrs
+ end
+
+ def build_xml(xml)
+ attrs = attrs_xml(hash_except(attributes, :dhcp, :tftp))
+ xml.ip(attrs.compact.transform_values(&:to_s)) do
+ xml.tftp(:root => tftp.to_s) if tftp
+ dhcp&.build_xml(xml)
+ end
+ end
+
+ def fragment_only?(other)
+ hash_except(attributes, :dhcp) == hash_except(other.attributes, :dhcp)
+ end
+
+ def self.fragment_only?(new_items, old_items)
+ return false unless new_items.to_a.length == old_items.to_a.length
+
+ old_by_id = old_items.to_h { |ip| [ip.identity, ip] }
+ new_by_id = new_items.to_h do |ip|
+ ip = cast(ip)
+ [ip.identity, ip]
+ end
+
+ return false unless old_by_id.keys == new_by_id.keys
+
+ new_by_id.all? do |id, new|
+ old = old_by_id[id]
+ new.fragment_only?(old) && Dhcp.fragment_only?(new.dhcp, old.dhcp)
+ end
+ end
+
+ def self.save_fragment(new_items, old_items, network)
+ old_by_id = old_items.each_with_index.to_h { |ip, index| [ip.identity, [ip, index]] }
+
+ updated = false
+ new_items.each do |new|
+ new = cast(new)
+ old = old_by_id[new.identity]
+ updated |= Dhcp.save_fragment(new.dhcp, old.first.dhcp, old.last, network)
+ end
+ updated
+ end
+
+ private
+
+ def normalize_attrs(attrs)
+ attrs = { :address => attrs.to_s } unless attrs.is_a?(Hash)
+ attrs[:prefix] = attrs[:prefix].to_i unless attrs[:prefix].to_s.empty?
+ attrs[:prefix] = attrs.delete("prefix").to_i unless attrs["prefix"].to_s.empty?
+ attrs[:family] = attrs[:family].to_sym unless attrs[:family].to_s.empty?
+ attrs[:family] = attrs.delete("family").to_sym unless attrs["family"].to_s.empty?
+ attrs_underscore(attrs)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/network/nat.rb b/lib/fog/libvirt/models/compute/network/nat.rb
new file mode 100644
index 0000000..50c59e4
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/network/nat.rb
@@ -0,0 +1,63 @@
+require "fog/core/model"
+require_relative "../attribute_model"
+require_relative "../util/util"
+
+module Fog
+ module Libvirt
+ class Compute
+ class Network < Fog::Model
+ class Nat < Fog::Libvirt::Compute::AttributeModel
+ include Fog::Libvirt::Util
+
+ attribute :ipv6
+
+ attribute :address
+ attribute :port
+
+ remove_method :address=
+
+ def address=(address)
+ attributes[:address] = if address.is_a?(Range)
+ normalize_ip_range(address)
+ else
+ address
+ end
+ end
+
+ remove_method :port=
+
+ def port=(port)
+ if port.is_a?(Range)
+ attributes[:port] = normalize_number_range(port)
+ else
+ attributes[:port] = port
+ attributes[:port][:start] = attributes[:port][:start].to_i unless attributes[:port].to_h[:start].to_s.empty?
+ attributes[:port][:end] = attributes[:port][:end].to_i unless attributes[:port].to_h[:end].to_s.empty?
+ end
+ end
+
+ def self.parse_xml(node)
+ return nil unless node
+
+ attrs = xml_attrs(node)
+
+ attrs[:address] = xml_attrs(node.at_xpath("address"))
+ attrs.delete(:address) if attrs[:address].empty?
+
+ attrs[:port] = xml_attrs(node.at_xpath("port"))
+ attrs.delete(:port) if attrs[:port].empty?
+
+ attrs
+ end
+
+ def build_xml(xml)
+ xml.nat(attrs_xml(hash_except(attributes, :address, :port)).compact) do
+ xml.address(address.compact) if address
+ xml.port(port.compact) if port
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/network/pci_address.rb b/lib/fog/libvirt/models/compute/network/pci_address.rb
new file mode 100644
index 0000000..c934e9a
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/network/pci_address.rb
@@ -0,0 +1,41 @@
+require "fog/core/model"
+require_relative "../attribute_model"
+require_relative "../util/util"
+
+module Fog
+ module Libvirt
+ class Compute
+ class Network < Fog::Model
+ class PciAddress < Fog::Libvirt::Compute::AttributeModel
+ include Fog::Libvirt::Util
+
+ attribute :type
+ attribute :domain
+ attribute :bus
+ attribute :slot
+ attribute :function
+
+ def initialize(attributes = {})
+ super(defaults.merge(attributes))
+ end
+
+ def self.parse_xml(node)
+ return nil unless node
+
+ xml_attrs(node)
+ end
+
+ def build_xml(xml)
+ xml.address(attributes.compact)
+ end
+
+ private
+
+ def defaults
+ { :type => "pci", :domain => 0 }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/network/portgroup.rb b/lib/fog/libvirt/models/compute/network/portgroup.rb
new file mode 100644
index 0000000..5120e18
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/network/portgroup.rb
@@ -0,0 +1,73 @@
+require "fog/core/model"
+require_relative "../util/util"
+require_relative "../clonable_model"
+require_relative "bandwidth"
+require_relative "virtualport"
+require_relative "vlan"
+
+module Fog
+ module Libvirt
+ class Compute
+ class Network < Fog::Model
+ class Portgroup < Fog::Model
+ include Fog::Libvirt::Util
+ include ClonableModel
+
+ identity :name
+ attribute :default
+ attribute :trust_guest_rx_filters
+
+ attribute :bandwidth
+ attribute :virtualport
+ attribute :vlan
+
+ autocast_on_assign :bandwidth, Bandwidth
+ autocast_on_assign :virtualport, Virtualport
+ autocast_on_assign :vlan, Vlan
+
+ def initialize(attributes = {})
+ super(attrs_underscore(attributes))
+ end
+
+ def self.parse_xml(node)
+ return nil unless node
+
+ attrs = xml_attrs(node)
+
+ attrs[:bandwidth] = Bandwidth.parse_xml(node.at_xpath("bandwidth"))
+ attrs.delete(:bandwidth) unless attrs[:bandwidth]
+
+ attrs[:virtualport] = Virtualport.parse_xml(node.at_xpath("virtualport"))
+ attrs.delete(:virtualport) unless attrs[:virtualport]
+
+ attrs[:vlan] = Vlan.parse_xml(node.at_xpath("vlan"))
+ attrs.delete(:vlan) unless attrs[:vlan]
+
+ attrs
+ end
+
+ def build_xml(xml)
+ attrs = attrs_xml(hash_except(attributes, :bandwidth, :virtualport, :vlan))
+ xml.portgroup(attrs.compact) do
+ bandwidth&.build_xml(xml)
+ virtualport&.build_xml(xml)
+ vlan&.build_xml(xml)
+ end
+ end
+
+ def section
+ :portgroup
+ end
+
+ def self.fragment_only?(new, old)
+ true
+ end
+
+ def self.save_fragment(new_items, old_items, network)
+ network.save_fragment(models_cast(new_items, self), old_items.to_a)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/network/route.rb b/lib/fog/libvirt/models/compute/network/route.rb
new file mode 100644
index 0000000..3a9c1d0
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/network/route.rb
@@ -0,0 +1,49 @@
+require "fog/core/model"
+require_relative "../util/util"
+
+module Fog
+ module Libvirt
+ class Compute
+ class Network < Fog::Model
+ class Route < Fog::Model
+ include Fog::Libvirt::Util
+
+ identity :gateway
+ attribute :address
+ attribute :netmask
+ attribute :prefix
+ attribute :family
+ attribute :metric
+
+ def initialize(attributes = {})
+ super(normalize_attrs(attributes))
+ end
+
+ def self.parse_xml(node)
+ return nil unless node
+
+ attrs = xml_attrs(node)
+ attrs[:prefix] = attrs[:prefix].to_i if attrs.key?(:prefix)
+ attrs[:metric] = attrs[:metric].to_i if attrs.key?(:metric)
+ attrs
+ end
+
+ def build_xml(xml)
+ xml.route(attributes.compact.transform_values(&:to_s))
+ end
+
+ private
+
+ def normalize_attrs(attrs)
+ attrs = { :gateway => attrs } if attrs.is_a?(String)
+ attrs[:prefix] = attrs[:prefix].to_i unless attrs[:prefix].to_s.empty?
+ attrs[:prefix] = attrs.delete("prefix").to_i unless attrs["prefix"].to_s.empty?
+ attrs[:metric] = attrs[:metric].to_i unless attrs[:metric].to_s.empty?
+ attrs[:metric] = attrs.delete("metric").to_i unless attrs["metric"].to_s.empty?
+ attrs
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/network/virtualport.rb b/lib/fog/libvirt/models/compute/network/virtualport.rb
new file mode 100644
index 0000000..f5ec027
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/network/virtualport.rb
@@ -0,0 +1,45 @@
+require "fog/core/model"
+require_relative "../attribute_model"
+require_relative "../util/util"
+
+module Fog
+ module Libvirt
+ class Compute
+ class Network < Fog::Model
+ class Virtualport < Fog::Libvirt::Compute::AttributeModel
+ include Fog::Libvirt::Util
+
+ attribute :type
+ attribute :interfaceid
+ attribute :profileid
+ attribute :managerid
+ attribute :typeid
+ attribute :typeidversion
+ attribute :instanceid
+
+ def self.parse_xml(node)
+ return nil unless node
+
+ attrs = {}
+ attrs[:type] = node["type"] if node["type"]
+ attrs.merge!(xml_attrs(node.at_xpath("parameters")))
+
+ attrs[:managerid] = attrs[:managerid].to_i if attrs.key?(:managerid)
+ attrs[:typeid] = attrs[:typeid].to_i if attrs.key?(:typeid)
+ attrs[:typeidversion] = attrs[:typeidversion].to_i if attrs.key?(:typeidversion)
+
+ attrs
+ end
+
+ def build_xml(xml)
+ attrs = attributes.slice(:type).compact
+ parameters = hash_except(attributes, :type).compact
+ xml.virtualport(attrs) do
+ xml.parameters(parameters) unless parameters.empty?
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/network/vlan.rb b/lib/fog/libvirt/models/compute/network/vlan.rb
new file mode 100644
index 0000000..7c51c09
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/network/vlan.rb
@@ -0,0 +1,36 @@
+require "fog/core/model"
+require_relative "../attribute_model"
+require_relative "../util/util"
+require_relative "vlan_tag"
+
+module Fog
+ module Libvirt
+ class Compute
+ class Network < Fog::Model
+ class Vlan < Fog::Libvirt::Compute::AttributeModel
+ include Fog::Libvirt::Util
+
+ attribute :trunk
+ attribute :tags, :type => Array
+
+ autocast_on_assign :tags, [VlanTag]
+
+ def self.parse_xml(node)
+ return nil unless node
+
+ attrs = xml_attrs(node)
+ attrs[:tags] = node.xpath("tag").map { |tag_node| VlanTag.parse_xml(tag_node) }
+ attrs.delete(:tags) if attrs[:tags].empty?
+ attrs
+ end
+
+ def build_xml(xml)
+ xml.vlan(attrs_xml(hash_except(attributes, :tags)).compact) do
+ tags.each { |tag| model_cast(tag, VlanTag).build_xml(xml) }
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/network/vlan_tag.rb b/lib/fog/libvirt/models/compute/network/vlan_tag.rb
new file mode 100644
index 0000000..9b89a6d
--- /dev/null
+++ b/lib/fog/libvirt/models/compute/network/vlan_tag.rb
@@ -0,0 +1,39 @@
+require "fog/core/model"
+require_relative "../util/util"
+
+module Fog
+ module Libvirt
+ class Compute
+ class Network < Fog::Model
+ class VlanTag < Fog::Model
+ include Fog::Libvirt::Util
+
+ identity :id, :type => Integer
+
+ attribute :native_mode
+
+ def initialize(attributes = {})
+ super(normalize_attrs(attributes))
+ end
+
+ def self.parse_xml(node)
+ return nil unless node
+
+ xml_attrs(node)
+ end
+
+ def build_xml(xml)
+ xml.tag(attrs_xml(attributes).compact)
+ end
+
+ private
+
+ def normalize_attrs(attrs)
+ attrs = { :id => attrs } if attrs.is_a?(Integer) || attrs.is_a?(String)
+ attrs_underscore(attrs)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/models/compute/util/util.rb b/lib/fog/libvirt/models/compute/util/util.rb
index e0eee24..f32dc7e 100644
--- a/lib/fog/libvirt/models/compute/util/util.rb
+++ b/lib/fog/libvirt/models/compute/util/util.rb
@@ -17,6 +17,177 @@ def xml_elements(xml, path, attribute=nil)
def randomized_name
"fog-#{(SecureRandom.random_number*10E14).to_i.round}"
end
+
+ def self.hash_except(hash, *attrs)
+ hash.respond_to?(:except) ? hash.except(*attrs) : hash.reject { |key, _| attrs.include?(key) }
+ end
+
+ def hash_except(*attrs)
+ Util.hash_except(*attrs)
+ end
+
+ module ClassMethods
+ def xml_value(value)
+ return nil if value.nil?
+ return ["yes", "on"].include?(value) if ["yes", "no", "on", "off"].include?(value)
+
+ value
+ end
+
+ # Copied from fog-core/lib/fog/core/provider.rb
+ def xml_underscore(name)
+ name.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
+ .tr("-", "_")
+ .downcase
+ end
+
+ def xml_attrs(node)
+ return {} unless node
+
+ attrs = node.to_h.transform_keys { |name| xml_underscore(name).to_sym }
+ attrs.transform_values! { |value| xml_value(value) }
+ attrs
+ end
+
+ def autocast_on_assign(name, type)
+ assign_name = "#{name}=".to_sym
+ remove_method(assign_name) if method_defined?(assign_name)
+ if type.is_a?(Array)
+ type = type.first
+ raise "Missing type for Array" if type.nil?
+
+ create_array_assigner(assign_name, name, type)
+ else
+ define_method(assign_name) do |value|
+ attributes[name] = value.nil? || value.is_a?(type) ? value : type.new(value)
+ end
+ end
+ end
+
+ def model_cast(value, type)
+ return value if value.is_a?(type)
+
+ type.new(value)
+ end
+
+ def models_cast(models, type)
+ models.map { |model| model_cast(model, type) }
+ end
+
+ private
+
+ def create_array_assigner(assign_name, attr_name, type)
+ define_method(assign_name) do |values|
+ attributes[attr_name] = if values.nil?
+ []
+ elsif !values.is_a?(Array)
+ [values.is_a?(type) ? values : type.new(values)]
+ else
+ values.map { |value| value.is_a?(type) ? value : type.new(value) }
+ end
+ end
+ end
+ end
+
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ def attr_camelcase(name)
+ first, *rest = name.to_s.split("_")
+ return name if rest.empty?
+
+ first + rest.map(&:capitalize).join
+ end
+
+ def value_xml(value)
+ return nil if value.nil?
+ return value ? "yes" : "no" if [true, false].include?(value)
+
+ value
+ end
+
+ def xml_switch(value)
+ return nil if value.nil?
+
+ value ? "on" : "off"
+ end
+
+ def xml_underscore(attrs)
+ self.class.xml_underscore(attrs)
+ end
+
+ def attrs_underscore(attrs)
+ attrs.to_h.transform_keys { |name| xml_underscore(name.to_s).to_sym }
+ end
+
+ def attrs_xml(attrs)
+ attrs = attrs.to_h.transform_keys { |name| attr_camelcase(name).to_sym }
+ attrs.transform_values! { |value| value_xml(value) }
+ attrs
+ end
+
+ def normalize_ip_range(range)
+ end_val = range.end.to_s
+ if range.exclude_end?
+ last_ip = IPAddr.new(end_val)
+ end_val = IPAddr.new(last_ip.to_i - 1, last_ip.family).to_s
+ end
+ { :start => range.begin.to_s, :end => end_val }
+ end
+
+ def normalize_number_range(range)
+ end_val = range.end.to_i
+ end_val -= 1 if range.exclude_end?
+ { :start => range.begin.to_i, :end => end_val }
+ end
+
+ def model_cast(value, type)
+ self.class.model_cast(value, type)
+ end
+
+ def models_cast(models, type)
+ self.class.models_cast(models, type)
+ end
+
+ def model_empty?(model)
+ return true if model.nil?
+
+ if model.respond_to?(:attributes)
+ model.attributes.values.all? { |value| model_empty?(value) }
+ elsif model.respond_to?(:empty?)
+ model.empty?
+ else
+ false
+ end
+ end
+
+ def models_equal?(left, right)
+ return true if left.nil? && model_empty?(right)
+ return true if right.nil? && model_empty?(left)
+ return false unless left.instance_of?(right.class)
+
+ if left.is_a?(Array)
+ model_arrays_equal?(left, right)
+ elsif left.is_a?(Hash)
+ model_hashes_equal?(left, right)
+ elsif left.respond_to?(:attributes)
+ models_equal?(left.attributes, right.attributes)
+ else
+ left == right
+ end
+ end
+
+ private
+
+ def model_arrays_equal?(left, right)
+ left.length == right.length && left.zip(right).all? { |x, y| models_equal?(x, y) }
+ end
+
+ def model_hashes_equal?(left, right)
+ left.length == right.length && left.all? { |key, value| right.key?(key) && models_equal?(value, right[key]) }
+ end
end
end
end
diff --git a/lib/fog/libvirt/requests/compute/create_network.rb b/lib/fog/libvirt/requests/compute/create_network.rb
new file mode 100644
index 0000000..8885a4e
--- /dev/null
+++ b/lib/fog/libvirt/requests/compute/create_network.rb
@@ -0,0 +1,19 @@
+module Fog
+ module Libvirt
+ class Compute
+ module Shared
+ def create_network(xml)
+ client.create_network_xml(xml)
+ end
+ end
+
+ class Real
+ include Shared
+ end
+
+ class Mock
+ include Shared
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/requests/compute/define_network.rb b/lib/fog/libvirt/requests/compute/define_network.rb
new file mode 100644
index 0000000..2fe0b49
--- /dev/null
+++ b/lib/fog/libvirt/requests/compute/define_network.rb
@@ -0,0 +1,19 @@
+module Fog
+ module Libvirt
+ class Compute
+ module Shared
+ def define_network(xml)
+ client.define_network_xml(xml)
+ end
+ end
+
+ class Real
+ include Shared
+ end
+
+ class Mock
+ include Shared
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/requests/compute/list_networks.rb b/lib/fog/libvirt/requests/compute/list_networks.rb
index d063687..47dd379 100644
--- a/lib/fog/libvirt/requests/compute/list_networks.rb
+++ b/lib/fog/libvirt/requests/compute/list_networks.rb
@@ -1,3 +1,5 @@
+require "nokogiri"
+
module Fog
module Libvirt
class Compute
@@ -37,11 +39,21 @@ def network_to_attributes(net)
bridge_name = ''
end
- {
+ attrs = {
:uuid => net.uuid,
:name => net.name,
+ :persistent => net.persistent?,
+ :active => net.active?,
+ :autostart => net.autostart?,
:bridge_name => bridge_name
}
+
+ network_attrs = Fog::Libvirt::Compute::Network.parse_xml(net.xml_desc)
+ if network_attrs
+ attrs = network_attrs.merge(attrs)
+ attrs[:preloaded] = true
+ end
+ attrs
end
end
diff --git a/lib/fog/libvirt/requests/compute/update_network.rb b/lib/fog/libvirt/requests/compute/update_network.rb
new file mode 100644
index 0000000..9c59f94
--- /dev/null
+++ b/lib/fog/libvirt/requests/compute/update_network.rb
@@ -0,0 +1,31 @@
+module Fog
+ module Libvirt
+ class Compute
+ module Shared
+ def update_network(network, xml, persistent, active, autostart)
+ currently_persistent = network&.persistent?
+ network.destroy if network&.active?
+ network.undefine if currently_persistent
+
+ if persistent || persistent.nil?
+ new_network = define_network(xml)
+ new_network.create if active
+ new_network.autostart = true if autostart
+ else
+ new_network = create_network(xml)
+ end
+
+ new_network
+ end
+ end
+
+ class Real
+ include Shared
+ end
+
+ class Mock
+ include Shared
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/requests/compute/update_network_autostart.rb b/lib/fog/libvirt/requests/compute/update_network_autostart.rb
new file mode 100644
index 0000000..1c551de
--- /dev/null
+++ b/lib/fog/libvirt/requests/compute/update_network_autostart.rb
@@ -0,0 +1,22 @@
+module Fog
+ module Libvirt
+ class Compute
+ module Shared
+ def update_network_autostart(uuid, value)
+ network = client.lookup_network_by_uuid(uuid)
+ previous = network.autostart
+ network.autostart = value
+ previous
+ end
+ end
+
+ class Real
+ include Shared
+ end
+
+ class Mock
+ include Shared
+ end
+ end
+ end
+end
diff --git a/lib/fog/libvirt/requests/compute/update_network_section.rb b/lib/fog/libvirt/requests/compute/update_network_section.rb
new file mode 100644
index 0000000..e54e66c
--- /dev/null
+++ b/lib/fog/libvirt/requests/compute/update_network_section.rb
@@ -0,0 +1,55 @@
+module Fog
+ module Libvirt
+ class Compute
+ module Shared
+ def update_network_section(uuid, command, section, xml, options = {})
+ network = client.lookup_network_by_uuid(uuid)
+ parent_index = options.fetch(:parent_index, -1)
+ flags = network_update_flags(options)
+ network.update(network_update_command(command), network_section_id(section), parent_index, xml, flags)
+ true
+ end
+
+ private
+
+ def network_update_command(command)
+ case command
+ when :modify then ::Libvirt::Network::UPDATE_COMMAND_MODIFY
+ when :add_last then ::Libvirt::Network::UPDATE_COMMAND_ADD_LAST
+ when :add_first then ::Libvirt::Network::UPDATE_COMMAND_ADD_FIRST
+ when :delete then ::Libvirt::Network::UPDATE_COMMAND_DELETE
+ else raise ArgumentError, "Unknown update command: #{command}"
+ end
+ end
+
+ def network_section_id(section)
+ case section
+ when :dhcp_range then ::Libvirt::Network::SECTION_IP_DHCP_RANGE
+ when :dhcp_host then ::Libvirt::Network::SECTION_IP_DHCP_HOST
+ when :forward_interface then ::Libvirt::Network::SECTION_FORWARD_INTERFACE
+ when :portgroup then ::Libvirt::Network::SECTION_PORTGROUP
+ when :dns_host then ::Libvirt::Network::SECTION_DNS_HOST
+ when :dns_txt then ::Libvirt::Network::SECTION_DNS_TXT
+ when :dns_srv then ::Libvirt::Network::SECTION_DNS_SRV
+ else raise ArgumentError, "Unknown or unsupported section: #{section}"
+ end
+ end
+
+ def network_update_flags(options = {})
+ flags = 0
+ flags |= ::Libvirt::Network::UPDATE_AFFECT_LIVE if options.fetch(:live, false)
+ flags |= ::Libvirt::Network::UPDATE_AFFECT_CONFIG if options.fetch(:persist, false)
+ flags
+ end
+ end
+
+ class Real
+ include Shared
+ end
+
+ class Mock
+ include Shared
+ end
+ end
+ end
+end
diff --git a/minitests/network/network_test.rb b/minitests/network/network_test.rb
index 8ea819c..5fe57c3 100644
--- a/minitests/network/network_test.rb
+++ b/minitests/network/network_test.rb
@@ -1,33 +1,956 @@
require_relative "../test_helper"
+require "nokogiri"
+require "json"
+require "ipaddr"
class NetworkTest < Minitest::Test
def setup
- @network = Fog::Compute[:libvirt].networks.new(:name => "default", :uuid => "dd8fe884-6c02-601e-7551-cca97df1c5df", :bridge_name => "virbr0")
+ @created_networks = []
+ @compute = Fog::Compute[:libvirt]
+ end
+
+ def teardown
+ @created_networks.each do |name|
+ @compute.networks.all(:name => name).each(&:destroy)
+ end
end
def test_model
- assert_kind_of Fog::Libvirt::Compute::Network, @network
+ network = @compute.networks.new(:name => "default", :uuid => "dd8fe884-6c02-601e-7551-cca97df1c5df", :bridge_name => "virbr0")
- assert @network.respond_to? "reload"
- assert @network.respond_to? "dhcp_leases"
+ assert_kind_of Fog::Libvirt::Compute::Network, network
- assert_kind_of Array, @network.dhcp_leases("aa:bb:cc:dd:ee:ff", 0) if Fog.mock?
+ assert network.respond_to? "reload"
+ assert network.respond_to? "dhcp_leases"
attributes = [:name, :uuid, :bridge_name]
attributes.each do |attribute|
- assert @network.respond_to? attribute
- assert @network.attributes.key? attribute
+ assert network.respond_to? attribute
+ assert network.attributes.key? attribute unless attribute == :bridge_name
end
- end
- def test_to_xml
expected = <<~NETWORK
default
-
+ dd8fe884-6c02-601e-7551-cca97df1c5df
+
NETWORK
- assert_equal expected, @network.to_xml
+ assert_equal expected, network.to_xml
+ end
+
+ def test_dhcp_leases
+ network = create_network("test-network-model", :uuid => "dd8fe884-6c02-601e-7551-cca97df1c5df")
+
+ if Fog.mock?
+ # From models/compute/network/dhcp_lease.rb
+ dhcp_leases_mock_data = [{ "type" => 2, "ipaddr" => "1.2.3.4", "prefix" => 24, "expirytime" => 5000 },
+ { "type" => 2, "ipaddr" => "1.2.5.6", "prefix" => 24, "expirytime" => 5005 }]
+
+ assert_equal dhcp_leases_mock_data, network.dhcp_leases("aa:bb:cc:dd:ee:ff", 0)
+ elsif real_libvirt?
+ assert_kind_of Array, network.dhcp_leases("aa:bb:cc:dd:ee:ff", 0)
+ else
+ skip("libvirt test driver doesn't support dhcp_leases so can't test without mocking or real libvirt")
+ end
+ end
+
+ def network_all_attrs
+ {
+ :uuid => "106c3ca9-04ca-4120-a9a5-c153e41289d9",
+ :ipv6 => true,
+ :trust_guest_rx_filters => true,
+ :name => "fog-test-xml",
+ :metadata => %(\n \n 123\n),
+ :title => "Title",
+ :description => "Description",
+ :bridge => { :name => "virbr100", :zone => "dmz", :stp => true, :delay => 5, :mac_table_manager => "libvirt" },
+ :mtu => 3000,
+ :domain => { :name => "example.local", :local_only => true, :register => false },
+ :forward => network_all_forward,
+ :bandwidth => network_all_bandwidth,
+ :virtualport => network_all_virtualport,
+ :vlan => { :trunk => true, :tags => [{ :id => 100, :native_mode => "tagged" }, { :id => 200 }] },
+ :portgroups => network_all_portgroups,
+ :isolated => true,
+ :mac => "f8:ee:dd:cc:bb:aa",
+ :dns => network_all_dns,
+ :ips => network_all_ips,
+ :routes => [{ :gateway => "10.0.0.1", :address => "10.0.0.100", :netmask => "255.255.128.0", :prefix => 17, :family => "ipv4", :metric => 100 }],
+ :dnsmasq => { :options => ["foo=bar", "cname=*.foo.example.com,master.example.com"] }
+ }
+ end
+
+ def network_all_forward
+ {
+ :mode => :nat, :managed => true,
+ :nat => {
+ :ipv6 => true,
+ :address => { :start => "172.16.24.100", :end => "172.16.27.200" },
+ :port => { :start => 1000, :end => 5000 }
+ },
+ :interfaces => [{ :dev => "eth1" }],
+ :pf => "eth0",
+ :driver => "vfio",
+ :addresses => [{ :type => "pci", :domain => "0x0000", :bus => "0x04",
+ :slot => "0x02", :function => "0x1" }]
+ }
+ end
+
+ def network_all_bandwidth
+ {
+ :inbound => { :average => 1000, :peak => 2000, :burst => 256, :floor => 300 },
+ :outbound => { :average => 500, :peak => 1000, :burst => 128 }
+ }
+ end
+
+ def network_all_virtualport
+ { :type => "802.1Qbg",
+ :interfaceid => "aaaaaaaa-094c-4267-9de0-0f0b40a61389",
+ :profileid => "profile",
+ :managerid => 11,
+ :typeid => 345,
+ :typeidversion => 2,
+ :instanceid => "bbbbbbbb-a905-474d-bbc0-1ef1920dcd8d" }
+ end
+
+ def network_all_portgroups
+ [
+ { :name => "portgroup",
+ :default => true,
+ :trust_guest_rx_filters => true,
+ :bandwidth => {},
+ :virtualport => { :type => "openvswitch", :interfaceid => "cccccccc-ac59-4b03-9a61-bc28e5427a35" },
+ :vlan => { :trunk => false } }
+ ]
+ end
+
+ def network_all_dns
+ {
+ :enable => true, :forward_plain_names => false,
+ :forwarders => [{ :addr => "192.168.20.30", :port => 53 },
+ { :addr => "192.168.20.50" },
+ { :domain => "example.com" }],
+ :hosts => [{ :ip => "192.168.20.70", :hostnames => ["ns1.example.com", "ns1"] }],
+ :txts => [{ :name => "example", :value => "text" }],
+ :srvs => [{ :service => "xmpp", :protocol => "tcp", :domain => "example.com",
+ :target => "xmpp.example.com", :port => 5269, :priority => 10, :weight => 100 }]
+ }
+ end
+
+ def network_all_ips
+ [
+ {
+ :address => "172.16.200.1", :netmask => "255.255.252.0", :local_ptr => true,
+ :tftp => "/var/lib/tftpboot",
+ :dhcp => {
+ :ranges => [{ :start => "172.16.201.50", :end => "172.16.202.100" },
+ { :start => "172.16.202.200", :end => "172.16.202.250" }],
+ :hosts => [{ :mac => "44:11:bb:33:dd:22", :name => "hostnm", :ip => "172.16.200.60", :lease => { :expiry => 24, :unit => "hours" } }],
+ :bootp => { :file => "pxelinux.0", :server => "172.16.200.10" }
+ }
+ },
+ {
+ :address => "2001:db8::1", :prefix => 100, :family => "ipv6",
+ :dhcp => { :hosts => [{ :id => "00:02:00:00:ab:11:b0:42:16:54:60:57:e8:65", :name => "ipv6host", :ip => "2001:db8::0100" }] }
+ }
+ ]
+ end
+
+ def unmanaged_elements_xml
+ <<~XML
+
+
+
+
+
+ stuff
+ fog-test-xml
+ 106c3ca9-04ca-4120-a9a5-c153e41289d9
+
+
+
+
+
+
+ XML
+ end
+
+ def test_xml
+ attrs = network_all_attrs
+ network = @compute.networks.new(attrs)
+ expected_attrs = JSON.parse(network.send(:defaults).merge(attrs).to_json)
+
+ assert_equal expected_attrs, JSON.parse(network.to_json)
+
+ network_xml = network.to_xml
+ parsed_network = Fog::Libvirt::Compute::Network.new(Fog::Libvirt::Compute::Network.parse_xml(network_xml))
+
+ assert_equal network_xml.strip, "\n#{parsed_network.xml}"
+ assert_equal expected_attrs, JSON.parse(parsed_network.to_json)
+
+ assert_equal unmanaged_elements_xml, unmanaged_elements_network(parsed_network.xml).to_xml
+ end
+
+ def unmanaged_elements_network(xml)
+ network_element = Nokogiri::XML(xml).at_xpath("//network")
+ network_element.add_namespace("something", "http://something.example.org")
+ network_element.add_child('')
+ network_element.add_child('stuff')
+
+ attrs = network_all_attrs
+ custom_attrs = attrs.merge(Fog::Libvirt::Util.hash_except(attrs, :uuid, :name, :ips).transform_values { nil })
+ custom_attrs[:ips] = [attrs[:ips].last]
+ custom_attrs[:xml] = network_element.to_xml
+ @compute.networks.new(custom_attrs)
+ end
+
+ def test_defaults
+ network = @compute.networks.new(:name => __method__.to_s)
+ refute network.active?
+ refute network.autostart?
+ assert network.persistent?
+ end
+
+ def test_clone_dup
+ attrs = network_all_attrs
+ attrs[:active] = true
+ attrs[:xml] = unmanaged_elements_xml
+ attrs[:preloaded] = true
+ network = @compute.networks.new(attrs)
+ refute_nil network.instance_variable_get(:@saved_attributes)
+
+ network_clone = network.clone
+ network_check_clone(network_clone, network, attrs)
+ network_check_deep_clone(network_clone, network, attrs)
+
+ network_dup = network_clone.dup
+ network_check_dup(network_dup, network_clone)
+ network_check_deep_dup(network_dup, network_clone)
+
+ assert_equal unmanaged_elements_xml, network_dup.xml
+ end
+
+ def network_check_clone(network_clone, network, attrs)
+ refute_same network, network_clone
+ assert_kind_of Fog::Libvirt::Compute::Network, network_clone
+
+ assert_equal network.uuid, network_clone.uuid
+ assert_equal network.name, network_clone.name
+ assert network_clone.active?
+ end
+
+ def network_check_deep_clone(network_clone, network, attrs)
+ network_clone.ips[0].dhcp.hosts[0].name = "new name for clone"
+ assert_equal attrs[:ips][0][:dhcp][:hosts][0][:name], network.ips[0].dhcp.hosts[0].name
+
+ network_clone.dns.forwarders.pop
+ assert network.dns.forwarders.length > network_clone.dns.forwarders.length
+ end
+
+ def network_check_dup(network_dup, network_clone)
+ refute_same network_clone, network_dup
+ assert_kind_of Fog::Libvirt::Compute::Network, network_dup
+
+ assert_nil network_dup.uuid
+ refute_equal network_clone.name, network_dup.name
+ assert_equal network_clone.persistent, network_dup.persistent
+ assert_nil network_dup.instance_variable_get(:@saved_attributes)
+ refute network_dup.active?
+ end
+
+ def network_check_deep_dup(network_dup, network_clone)
+ assert_equal network_clone.ips[0].dhcp.hosts[0].name, network_dup.ips[0].dhcp.hosts[0].name
+ network_dup.ips[0].dhcp.hosts[0].name = "name for dup"
+
+ refute_equal network_clone.ips[0].dhcp.hosts[0].name, network_dup.ips[0].dhcp.hosts[0].name
+ end
+
+ def test_lifecycle
+ network = create_network(__method__, :forward => { :mode => :bridge }, :persistent => false)
+
+ lifecycle_transient_shutdown(network)
+ lifecycle_make_persistent(network)
+ lifecycle_autostart(network)
+ lifecycle_recreate_persistent(network)
+ lifecycle_persistent_shutdown(network)
+ end
+
+ def lifecycle_transient_shutdown(network)
+ assert network.active?
+ refute network.persistent?
+ refute network.autostart?
+ network.shutdown
+ assert_nil network.uuid
+ refute network.active?
+ refute network.persistent?
+ end
+
+ def lifecycle_make_persistent(network)
+ network.active = false
+ network.persistent = true
+
+ assert_save network, 1, 0, :active, :persistent, :autostart
+
+ refute network.active?
+ assert network.persistent?
+ end
+
+ def lifecycle_autostart(network)
+ network.start
+ assert network.active?
+ assert network.persistent?
+
+ network.active = false
+ network.autostart = true
+
+ assert_save network, 0, 0, :active, :persistent, :autostart
+
+ refute network.active?
+ assert network.persistent?
+ assert network.autostart?
+
+ network.active = true
+ network.persistent = false
+
+ assert_save network, 1, 0, :persistent
+
+ assert network.active?
+ refute network.persistent?
+ refute network.autostart?
+
+ network.active = false
+
+ assert_save network, 0, 0, :persistent
+
+ assert network.active?
+ refute network.persistent?
+ refute network.autostart?
+ end
+
+ def lifecycle_recreate_persistent(network)
+ network.start
+ network.destroy
+ refute network.active?
+ refute network.persistent?
+ refute network.autostart?
+ assert_nil network.uuid
+
+ network.active = false
+ network.persistent = true
+
+ assert_save network, 1, 0, :active, :persistent, :autostart
+
+ refute network.active?
+ assert network.persistent?
+ end
+
+ def lifecycle_persistent_shutdown(network)
+ network.active = true
+
+ assert_save network, 0, 0, :active, :persistent, :autostart
+
+ assert network.active?
+ assert network.persistent?
+
+ network.shutdown
+ refute_nil network.uuid
+ end
+
+ def test_various
+ network = create_network(__method__)
+
+ various_initial(network)
+ various_other(network)
+ various_empty(network)
+ various_dnsmasq(network)
+ end
+
+ def various_initial(network)
+ refute network.autostart?
+
+ network.enable_autostart
+ network.domain = "example.com"
+
+ network.bridge = nil
+ network.bridge_name = "bridge-test1"
+ network.routes = "192.168.70.20"
+ network.routes[0].address = "192.168.50.0"
+ network.routes[0].netmask = "255.255.255.0"
+ network.forward.nat = Fog::Libvirt::Compute::Network::Nat.new
+ network.forward.nat.address = IPAddr.new("192.168.100.1")..IPAddr.new("192.168.100.10")
+ network.forward.nat.port = 5000..6000
+
+ assert network.autostart?
+
+ assert_save network, 1, 0, :domain, :routes, :forward
+ assert_equal "bridge-test1", network.bridge_name
+
+ network.disable_autostart
+ refute network.autostart?
+ end
+
+ def various_other(network)
+ network.bridge = "bridge-test2"
+ network.forward.nat.address = IPAddr.new("10.50.70.1")...IPAddr.new("10.50.70.255")
+ network.forward.nat.port = 1000...2000
+
+ assert_save network, 1, 0, :domain, :routes, :forward
+ assert_equal "bridge-test2", network.bridge_name
+
+ network.forward = "route"
+ assert_save network, 1, 0, :routes, :forward
+ end
+
+ def various_empty(network)
+ network.dns = Fog::Libvirt::Compute::Network::Dns.new
+ network.bandwidth = Fog::Libvirt::Compute::Network::Bandwidth.new
+ assert_save network, 0, 0, :domain, :routes, :forward, :dns, :bandwidth
+ end
+
+ def various_dnsmasq(network)
+ # libvirt test driver doesn't support dnsmasq
+ # so we can only test this against actual libvirt
+ real_libvirt = !Fog.mock? && real_libvirt?
+
+ network.dnsmasq = "custom=option"
+ assert_save network, 1, 0, real_libvirt ? :dnsmasq : nil
+
+ network.dnsmasq = ["more=options", "another=123"]
+ assert_save network, 1, 0, real_libvirt ? :dnsmasq : nil
+ end
+
+ def forward_bridge_final_xml
+ <<~XML
+
+
+ fog-test-forward-bridge
+
+
+ XML
+ end
+
+ def test_forward_bridge
+ network = create_network(__method__, :forward => { :mode => :bridge, :interfaces => "eth0" })
+ network.forward.dev = "eth1"
+ network.forward.interfaces += ["eth0", "eth2"]
+
+ assert_save network, 0, 4, :forward, &method(:forward_bridge_assert_initial_updates)
+ assert network.xml.include?('\n\n)
+ assert_update_section_call calls, 1, :add_last, :forward_interface, %(\n\n)
+ assert_update_section_call calls, 2, :add_last, :forward_interface, %(\n\n)
+ assert_update_section_call calls, 3, :add_last, :forward_interface, %(\n\n)
+ end
+
+ def forward_bridge_assert_modify_updates(calls)
+ assert_update_section_call calls, 0, :delete, :forward_interface, %(\n\n)
+ assert_update_section_call calls, 1, :add_last, :forward_interface, %(\n\n)
+ end
+
+ def forward_bridge_assert_clear_updates(calls)
+ assert_update_section_call calls, 0, :delete, :forward_interface, %(\n\n)
+ assert_update_section_call calls, 1, :delete, :forward_interface, %(\n\n)
+ assert_update_section_call calls, 2, :delete, :forward_interface, %(\n\n)
+ end
+
+ def test_portgroups
+ network = create_network(:test_portgroups,
+ :forward => { :mode => :bridge },
+ :portgroups => [
+ { :name => "portgroup1",
+ :trust_guest_rx_filters => true,
+ :virtualport => {
+ :interfaceid => "aabbccdd-ffa7-40c8-83ad-b8d47ba270f3"
+ } },
+ { :name => "portgroup2" }
+ ],
+ :persistent => false)
+
+ portgroups_live(network)
+ portgroups_persistent_live(network)
+ portgroups_persistent(network)
+ portgroups_final(network)
+ end
+
+ def portgroups_live(network)
+ assert network.active?
+ refute network.persistent?
+
+ network.portgroups = network.portgroups.drop(1)
+ network.portgroups[0].vlan = { :trunk => true, :tags => 20 }
+ network.portgroups << { :name => "portgroup3", :default => true, :virtualport => { :type => "802.1Qbh", :profileid => "port-profile" } }
+
+ assert_save network, 0, 3, :portgroups do |calls|
+ assert_update_section_call calls, 0, :modify, :portgroup, %(\n\n \n \n \n\n), :persist => false, :live => true
+ assert_update_section_call calls, 1, :delete, :portgroup, %(\n\n \n \n \n\n), :persist => false, :live => true
+ assert_update_section_call calls, 2, :add_last, :portgroup, %(\n\n \n \n \n\n), :persist => false, :live => true
+ end
+ end
+
+ def portgroups_first_xml
+ <<~XML
+
+
+ fog-test-portgroups
+
+
+
+
+
+
+
+ XML
+ end
+
+ def portgroups_persistent_live(network)
+ network.persistent = true
+ network.save
+ assert network.active?
+ assert network.persistent?
+
+ network.portgroups.pop
+ network.portgroups[0].vlan.trunk = nil
+ assert_save network, 0, 2, :portgroups do |calls|
+ assert_update_section_call calls, 0, :modify, :portgroup, %(\n\n \n \n \n\n), :persist => true, :live => true
+ assert_update_section_call calls, 1, :delete, :portgroup, %(\n\n \n \n \n\n), :persist => true, :live => true
+ end
+
+ assert_equal portgroups_first_xml, remove_uuid(@compute.networks.get(network.uuid).to_xml)
+ end
+
+ def portgroups_persistent(network)
+ network.shutdown
+ refute network.active?
+ assert network.persistent?
+ assert_equal portgroups_first_xml, remove_uuid(@compute.networks.get(network.uuid).to_xml)
+
+ network.portgroups[0].vlan.trunk = true
+ network.portgroups[0].vlan.tags << 50
+
+ assert_save network, 0, 1, :portgroups do |calls|
+ assert_update_section_call calls, 0, :modify, :portgroup, %(\n\n \n \n \n \n\n), :persist => true, :live => false
+ end
+ end
+
+ def portgroups_final_xml
+ <<~XML
+
+
+ fog-test-portgroups
+
+
+
+
+
+
+
+
+
+ XML
+ end
+
+ def portgroups_final(network)
+ network.forward = nil
+ network.bridge_name = "virbr-fog-test-portgroups"
+ network.mac = "e4:bd:de:c5:aa:cc"
+ network.portgroups = { :name => "final-portgroup", :bandwidth => { :inbound => 500, :outbound => "300" } }
+
+ assert_save network, 1, 0, :portgroups
+ assert_equal portgroups_final_xml, remove_uuid(@compute.networks.get(network.uuid).to_xml)
+ end
+
+ def dns_attrs
+ {
+ :hosts => [{ :ip => "172.16.160.25", :hostnames => ["hostname1.local"] },
+ { :ip => "172.16.170.85", :hostnames => ["hostname2.test", "hostname22.local"] }],
+ :txts => [{ :name => "txt1", :value => "value1" },
+ { :name => "txt2", :value => "value2" }],
+ :srvs => [{ :service => "sip", :protocol => "tcp", :domain => "example.com",
+ :target => "sip.example.com", :port => 5060, :priority => 10, :weight => 100 },
+ { :service => "smtp", :protocol => "tcp", :domain => "example.com",
+ :target => "smtp.example.com", :port => 25, :priority => 20, :weight => 200 }]
+ }
+ end
+
+ def test_dns
+ network = create_network(__method__, :dns => dns_attrs)
+
+ dns_initial_updates(network)
+
+ network.dns = nil
+ assert_save network, 0, 6, &method(:dns_assert_clear_updates)
+
+ network.dns = { :txts => { :name => "fresh", :value => "456" } }
+ assert_save network, 0, 1, :dns do |calls|
+ assert_update_section_call calls, 0, :add_last, :dns_txt, %(\n\n)
+ end
+
+ network.dns = { :forwarders => ["192.168.120.130"] }
+ assert_save network, 1, 0, :dns
+ end
+
+ def dns_initial_updates(network)
+ dns_initial_remove(network)
+ dns_initial_add(network)
+ dns_initial_modify(network)
+
+ expected_section_updates = [10, 12]
+ expected_asserts = [:dns_assert_modify_updates, :dns_assert_nomodify_updates]
+
+ # DNS host/txt :modify supported since libvirt >= 10.6.0
+ if @compute.client.libversion >= 10_006_000
+ mocked_version = 10_005_000
+ else
+ expected_section_updates.reverse!
+ expected_asserts.reverse!
+ mocked_version = 10_006_000
+ end
+
+ network_copy = network.clone
+ assert_save network, 0, expected_section_updates.first, :dns, &method(expected_asserts.first)
+
+ # Now test other case by mocking it
+ @compute.client.expects(:libversion).returns(mocked_version).twice
+ assert_save network_copy, 0, expected_section_updates.last, :dns, :mock_calls => true, &method(expected_asserts.last)
+
+ assert network.models_equal?(network_copy, network)
+ ensure
+ @compute.client.unstub(:libversion)
+ end
+
+ def dns_initial_remove(network)
+ network.dns.hosts = network.dns.hosts.drop(1)
+ network.dns.txts = network.dns.txts.drop(1)
+ network.dns.srvs = network.dns.srvs.drop(1)
+ end
+
+ def dns_initial_add(network)
+ network.dns.hosts << { :ip => "172.16.164.51", :hostnames => ["added.example.org"] }
+ network.dns.txts << { :name => "added", :value => "added value" }
+ network.dns.srvs << { :service => "pop3", :protocol => "tcp", :target => "pop3.example.org" }
+ end
+
+ def dns_initial_modify(network)
+ network.dns.hosts[0].hostnames = ["hostname2-replaced.test"]
+ network.dns.txts[0].value = "new value"
+ network.dns.srvs[0].priority = 50
+ end
+
+ # When libvirt supports DNS host/txt :modify (>= 10.6.0)
+ def dns_assert_modify_updates(calls)
+ assert_update_section_call calls, 0, :modify, :dns_host, %(\n\n hostname2-replaced.test\n\n)
+ assert_update_section_call calls, 1, :delete, :dns_host, %(\n\n hostname1.local\n\n)
+ assert_update_section_call calls, 2, :add_last, :dns_host, %(\n\n added.example.org\n\n)
+ assert_update_section_call calls, 3, :modify, :dns_txt, %(\n\n)
+ assert_update_section_call calls, 4, :delete, :dns_txt, %(\n\n)
+ assert_update_section_call calls, 5, :add_last, :dns_txt, %(\n\n)
+ assert_update_section_call calls, 6, :delete, :dns_srv, %(\n\n)
+ assert_update_section_call calls, 7, :delete, :dns_srv, %(\n\n)
+ assert_update_section_call calls, 8, :add_last, :dns_srv, %(\n\n)
+ assert_update_section_call calls, 9, :add_last, :dns_srv, %(\n\n)
+ end
+
+ # When libvirt doesn't support DNS host/txt :modify (< 10.6.0)
+ def dns_assert_nomodify_updates(calls)
+ assert_update_section_call calls, 0, :delete, :dns_host, %(\n\n hostname1.local\n\n)
+ assert_update_section_call calls, 1, :delete, :dns_host, %(\n\n hostname2.test\n hostname22.local\n\n)
+ assert_update_section_call calls, 2, :add_last, :dns_host, %(\n\n hostname2-replaced.test\n\n)
+ assert_update_section_call calls, 3, :add_last, :dns_host, %(\n\n added.example.org\n\n)
+ assert_update_section_call calls, 4, :delete, :dns_txt, %(\n\n)
+ assert_update_section_call calls, 5, :delete, :dns_txt, %(\n\n)
+ assert_update_section_call calls, 6, :add_last, :dns_txt, %(\n\n)
+ assert_update_section_call calls, 7, :add_last, :dns_txt, %(\n\n)
+ assert_update_section_call calls, 8, :delete, :dns_srv, %(\n\n)
+ assert_update_section_call calls, 9, :delete, :dns_srv, %(\n\n)
+ assert_update_section_call calls, 10, :add_last, :dns_srv, %(\n\n)
+ assert_update_section_call calls, 11, :add_last, :dns_srv, %(\n\n)
+ end
+
+ def dns_assert_clear_updates(calls)
+ assert_update_section_call calls, 0, :delete, :dns_host, %(\n\n hostname2-replaced.test\n\n)
+ assert_update_section_call calls, 1, :delete, :dns_host, %(\n\n added.example.org\n\n)
+ assert_update_section_call calls, 2, :delete, :dns_txt, %(\n\n)
+ assert_update_section_call calls, 3, :delete, :dns_txt, %(\n\n)
+ assert_update_section_call calls, 4, :delete, :dns_srv, %(\n\n)
+ assert_update_section_call calls, 5, :delete, :dns_srv, %(\n\n)
+ end
+
+ def ips_expected_xml
+ <<~XML
+
+
+ fog-test-ips
+
+
+
+
+
+
+
+
+ XML
+ end
+
+ def test_ips
+ network = create_network(__method__, :ips => [
+ "172.16.140.1",
+ { :address => "172.16.160.2", :prefix => 22, :local_ptr => true },
+ { :address => "172.16.220.3", :netmask => "255.255.252.0" }
+ ])
+
+ ips_initial_updates(network)
+ ips_next_updates(network)
+
+ network.ips = Fog::Libvirt::Compute::Network::Ip.new(:address => "172.16.150.12", :netmask => "255.255.224.0")
+ assert_save network, 1, 0, :ips
+ end
+
+ def ips_initial_updates(network)
+ network.ips = network.ips.drop(1)
+ network.ips[0].address = "172.16.160.27"
+ network.ips << { :address => "172.16.248.1", :netmask => "255.255.252.0" }
+ assert_save network, 1, 0, :ips
+ end
+
+ def ips_next_updates(network)
+ network.ips[1].address = "2001:db8::a300"
+ network.ips[1].netmask = nil
+ network.ips[1].prefix = 100
+ network.ips[1].family = :ipv6
+ network.ips << { :address => "2001:db8::eeee:7000", :prefix => 112, :family => :ipv6 }
+
+ assert_save network, 1, 0, :ips
+ assert_equal ips_expected_xml, remove_uuid(network.to_xml)
+ end
+
+ def dhcp_network_ips
+ [
+ {
+ :address => "192.168.96.1", :netmask => "255.255.224.0",
+ :dhcp => {
+ :ranges => [{ :start => "192.168.96.40", :end => "192.168.96.80" },
+ "192.168.96.150".."192.168.96.200"],
+ :hosts => [{ :mac => "a2:bb:cc:dd:aa:01", :name => "host1", :ip => "192.168.96.17" },
+ { :mac => "b4:aa:77:cc:ee:02", :name => "host2", :ip => "192.168.96.24" }]
+ }
+ },
+ {
+ :address => "2001:db8::dcea:1000", :prefix => 97, :family => :ipv6,
+ :dhcp => {
+ :ranges => [{ :start => "2001:db8::dcea:3000", :end => "2001:db8::dcea:5000" },
+ IPAddr.new("2001:db8::dcea:8000")...IPAddr.new("2001:db8::dcea:9000")],
+ :hosts => [{ :id => "00:02:00:00:bb:cc:dd:ee:16:54:60:57:aa:21", :ip => "2001:db8::dcea:2100" },
+ { :id => "00:02:00:00:ee:ff:aa:bb:21:23:24:25:26:32", :ip => "2001:db8::dcea:6200" }]
+ }
+ }
+ ]
+ end
+
+ def dhcp_final_xml
+ <<~XML
+
+
+ fog-test-dhcp
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ XML
+ end
+
+ def test_dhcp
+ network = create_network(__method__, :ips => dhcp_network_ips)
+
+ dhcp_initial_updates(network)
+
+ network.ips[1].dhcp = nil
+ assert_save network, 0, 4, :ips, &method(:dhcp_assert_clear_updates)
+
+ network.ips[1].dhcp = { :ranges => [{ :start => "2001:db8::dcea:1200", :end => "2001:db8::dcea:1400" }] }
+ assert_save network, 0, 1, :ips do |calls|
+ assert_update_section_call calls, 0, :add_last, :dhcp_range, %(\n\n), :parent_index => 1
+ end
+
+ network.ips[0].dhcp.bootp = "grub.efi"
+ assert_save network, 1, 0, :ips
+
+ assert_equal dhcp_final_xml, remove_uuid(network.to_xml)
+ end
+
+ def dhcp_initial_updates(network)
+ dhcp_change_ipv4(network.ips[0].dhcp)
+ dhcp_change_ipv6(network.ips[1].dhcp)
+
+ assert_save network, 0, 14, :ips, &method(:dhcp_assert_updates)
+ end
+
+ def dhcp_change_ipv4(dhcp)
+ dhcp.ranges = dhcp.ranges.drop(1)
+ dhcp.hosts = dhcp.hosts.drop(1)
+ dhcp.ranges << { :start => IPAddr.new("192.168.96.230"), :end => IPAddr.new("192.168.96.250") }
+ dhcp.hosts << { :mac => "cc:aa:ee:ff:bb:03", :name => "host3", :ip => "192.168.96.36" }
+ dhcp.ranges[0].lease = { :expiry => 9600, "unit" => :seconds }
+ dhcp.hosts[0].name = "updated_host"
+ end
+
+ def dhcp_change_ipv6(dhcp)
+ dhcp.ranges = dhcp.ranges.drop(1)
+ dhcp.hosts = dhcp.hosts.drop(1)
+ dhcp.ranges << { :start => "2001:db8::dcea:a000", :end => "2001:db8::dcea:b000" }
+ dhcp.hosts << { :id => "00:02:00:00:cc:dd:ee:ff:32:34:36:38:40:43", :ip => "2001:db8::dcea:1400" }
+ dhcp.ranges[0].lease = 20
+ dhcp.ranges[0].lease.unit = "hours"
+ dhcp.hosts[0].id = "00:02:00:00:dd:ee:cc:ff:66:77:88:99:00:55"
+ end
+
+ def dhcp_assert_updates(calls)
+ assert_update_section_call calls, 0, :delete, :dhcp_range, %(\n\n), :parent_index => 0
+ assert_update_section_call calls, 1, :delete, :dhcp_range, %(\n\n), :parent_index => 0
+ assert_update_section_call calls, 2, :add_last, :dhcp_range, %(\n\n \n\n), :parent_index => 0
+ assert_update_section_call calls, 3, :add_last, :dhcp_range, %(\n\n), :parent_index => 0
+ assert_update_section_call calls, 4, :modify, :dhcp_host, %(\n\n), :parent_index => 0
+ assert_update_section_call calls, 5, :delete, :dhcp_host, %(\n\n), :parent_index => 0
+ assert_update_section_call calls, 6, :add_last, :dhcp_host, %(\n\n), :parent_index => 0
+
+ assert_update_section_call calls, 7, :delete, :dhcp_range, %(\n\n), :parent_index => 1
+ assert_update_section_call calls, 8, :delete, :dhcp_range, %(\n\n), :parent_index => 1
+ assert_update_section_call calls, 9, :add_last, :dhcp_range, %(\n\n \n\n), :parent_index => 1
+ assert_update_section_call calls, 10, :add_last, :dhcp_range, %(\n\n), :parent_index => 1
+ assert_update_section_call calls, 11, :modify, :dhcp_host, %(\n\n), :parent_index => 1
+ assert_update_section_call calls, 12, :delete, :dhcp_host, %(\n\n), :parent_index => 1
+ assert_update_section_call calls, 13, :add_last, :dhcp_host, %(\n\n), :parent_index => 1
+ end
+
+ def dhcp_assert_clear_updates(calls)
+ assert_update_section_call calls, 0, :delete, :dhcp_range, %(\n\n \n\n), :parent_index => 1
+ assert_update_section_call calls, 1, :delete, :dhcp_range, %(\n\n), :parent_index => 1
+ assert_update_section_call calls, 2, :delete, :dhcp_host, %(\n\n), :parent_index => 1
+ assert_update_section_call calls, 3, :delete, :dhcp_host, %(\n\n), :parent_index => 1
+ end
+
+ def test_rename
+ network = create_network(__method__)
+
+ old_name = network.name
+ network.name = "fog-test-rename-new-name"
+ assert_save network, 1, 0, :uuid, :name
+ @created_networks.delete(old_name)
+ @created_networks << network.name
+
+ old_uuid = network.uuid
+ network.uuid = "22222222-3333-4444-5555-666666666666"
+ assert_save network, 1, 0, :uuid, :name
+ assert_equal "22222222-3333-4444-5555-666666666666", network.uuid
+ assert_raises(Libvirt::RetrieveError) { @compute.client.lookup_network_by_uuid(old_uuid) }
+ end
+
+ private
+
+ def remove_uuid(xml)
+ xml.gsub(/^\s*[^\n]*\n/, "")
+ end
+
+ def assert_save(network, expected_full_updates, expected_section_updates, *check_attrs, mock_calls: false)
+ full_update_calls = []
+ section_update_calls = []
+
+ real_update_network = network.service.method(:update_network)
+ real_update_network_section = network.service.method(:update_network_section)
+
+ network.service.define_singleton_method(:update_network) do |*args|
+ full_update_calls << args
+ real_update_network.call(*args) unless mock_calls
+ end
+ network.service.define_singleton_method(:update_network_section) do |*args|
+ section_update_calls << args
+ real_update_network_section.call(*args) unless mock_calls
+ end
+
+ before_save = JSON.parse(network.clone.to_json)
+
+ network.save
+
+ assert_equal expected_full_updates, full_update_calls.length, "full update count"
+ assert_equal expected_section_updates, section_update_calls.length, "section update count"
+
+ after_save = JSON.parse(@compute.networks.get(network.uuid).to_json)
+ assert_saved_attrs(before_save, after_save, check_attrs)
+
+ yield section_update_calls if block_given?
+ ensure
+ network.service.singleton_class.remove_method(:update_network)
+ network.service.singleton_class.remove_method(:update_network_section)
+ end
+
+ def assert_update_section_call(section_update_calls, index, command, section, xml, parent_index: -1, persist: true, live: false) # rubocop:disable Metrics/ParameterLists
+ assert_equal [command, section, xml, { :parent_index => parent_index, :persist => persist, :live => live }], section_update_calls[index].drop(1)
+ end
+
+ def create_network(name, options = {})
+ options[:name] = "fog-#{name.to_s.gsub('_', '-')}"
+ options[:forward] = { :mode => :nat } unless options[:forward]
+ if options[:forward][:mode] == :nat
+ options[:mac] = "cc:ee:f0:d7:b1:54" unless options.key?(:mac)
+ options[:bridge] = { :name => "virbr-fog-test", :stp => true, :delay => 0 } if !options[:bridge] && !options[:bridge_name]
+ options[:ips] = [{ :address => "192.168.70.1", :netmask => "255.255.255.0" }] unless options[:ips]
+ end
+ network = @compute.networks.new(options)
+ network.save
+ @created_networks << network.name
+ network
+ end
+
+ def assert_saved_attrs(before_save, after_save, check_attrs)
+ check_attrs.each do |attr|
+ expected = remove_empty(before_save[attr.to_s])
+ if expected.nil?
+ assert_nil after_save[attr.to_s]
+ else
+ assert_equal expected, after_save[attr.to_s]
+ end
+ end
+ end
+
+ def remove_empty(attrs)
+ return attrs unless attrs
+
+ if attrs.is_a?(Array)
+ attrs = attrs.map { |attr| remove_empty(attr) }.compact
+ attrs = nil if attrs.empty?
+ elsif attrs.is_a?(Hash)
+ attrs = attrs.transform_values { |value| remove_empty(value) }.compact
+ attrs = nil if attrs.empty?
+ end
+ attrs
end
end