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