[DRE-commits] [vagrant-libvirt] 34/104: * Private network support. * Creating private networks if ip address is set and network is not available. * Guest network interfaces configuration.

Antonio Terceiro terceiro at moszumanska.debian.org
Sun Apr 24 13:55:42 UTC 2016


This is an automated email from the git hooks/post-receive script.

terceiro pushed a commit to annotated tag 0.0.11
in repository vagrant-libvirt.

commit 813a7c811e981a0b9e90ffcb1b989274bc7917d9
Author: pradels <les.pradels at gmail.com>
Date:   Thu May 9 20:32:06 2013 +0200

    * Private network support.
    * Creating private networks if ip address is set and network
      is not available.
    * Guest network interfaces configuration.
---
 lib/vagrant-libvirt/action.rb                      |   6 +-
 .../action/create_network_interfaces.rb            | 117 +++++++--
 lib/vagrant-libvirt/action/create_networks.rb      | 269 +++++++++++++++++++++
 lib/vagrant-libvirt/action/destroy_networks.rb     |  80 ++++++
 lib/vagrant-libvirt/errors.rb                      |  24 ++
 .../templates/private_network.xml.erb              |  21 ++
 lib/vagrant-libvirt/util.rb                        |   1 +
 lib/vagrant-libvirt/util/libvirt_util.rb           |  57 +++++
 locales/en.yml                                     |  14 ++
 9 files changed, 566 insertions(+), 23 deletions(-)

diff --git a/lib/vagrant-libvirt/action.rb b/lib/vagrant-libvirt/action.rb
index 5b0893f..259a28f 100644
--- a/lib/vagrant-libvirt/action.rb
+++ b/lib/vagrant-libvirt/action.rb
@@ -20,9 +20,10 @@ module VagrantPlugins
               b2.use HandleBoxImage
               b2.use CreateDomainVolume
               b2.use CreateDomain
-              b2.use CreateNetworkInterfaces
 
               b2.use TimedProvision
+              b2.use CreateNetworks
+              b2.use CreateNetworkInterfaces
               b2.use StartDomain
               b2.use WaitTillUp
               b2.use SyncFolders
@@ -101,6 +102,7 @@ module VagrantPlugins
 
             b2.use ConnectLibvirt
             b2.use DestroyDomain
+            b2.use DestroyNetworks
 
             # Cleanup running instance data. Now only IP address is stored.
             b2.use CleanupDataDir
@@ -262,8 +264,10 @@ module VagrantPlugins
       autoload :SetNameOfDomain, action_root.join("set_name_of_domain")
       autoload :CreateDomainVolume, action_root.join("create_domain_volume")
       autoload :CreateDomain, action_root.join("create_domain")
+      autoload :CreateNetworks, action_root.join("create_networks")
       autoload :CreateNetworkInterfaces, action_root.join("create_network_interfaces")
       autoload :DestroyDomain, action_root.join("destroy_domain")
+      autoload :DestroyNetworks, action_root.join("destroy_networks")
       autoload :StartDomain, action_root.join("start_domain")
       autoload :HaltDomain, action_root.join("halt_domain")
       autoload :SuspendDomain, action_root.join("suspend_domain")
diff --git a/lib/vagrant-libvirt/action/create_network_interfaces.rb b/lib/vagrant-libvirt/action/create_network_interfaces.rb
index d8b7f24..5eebb75 100644
--- a/lib/vagrant-libvirt/action/create_network_interfaces.rb
+++ b/lib/vagrant-libvirt/action/create_network_interfaces.rb
@@ -1,12 +1,18 @@
 require 'log4r'
+require 'vagrant/util/network_ip'
+require 'vagrant/util/scoped_hash_override'
 
 module VagrantPlugins
   module Libvirt
     module Action
 
       # Create network interfaces for domain, before domain is running.
+      # Networks for connecting those interfaces should be already prepared.
       class CreateNetworkInterfaces
         include VagrantPlugins::Libvirt::Util::ErbTemplate
+        include VagrantPlugins::Libvirt::Util::LibvirtUtil
+        include Vagrant::Util::NetworkIP
+        include Vagrant::Util::ScopedHashOverride
 
         def initialize(app, env)
           @logger = Log4r::Logger.new("vagrant_libvirt::action::create_network_interfaces")
@@ -23,41 +29,59 @@ module VagrantPlugins
               :error_message => e.message
           end
 
-          # Setup list of interfaces before creating them
+          # Setup list of interfaces before creating them.
           adapters = []
 
           # Assign main interface for provisioning to first slot.
           # Use network 'default' as network for ssh connecting and
-          # machine provisioning. This should be maybe configurable in
-          # Vagrantfile in future.
-          adapters[0] = 'default'
-
+          # machine provisioning.
+          #
+          # TODO Network name with DHCP for first interface should be
+          # configurable.
+          adapters[0] = {
+            :network_name => 'default'
+          }
+
+          # Assign interfaces to slots.
           env[:machine].config.vm.networks.each do |type, options|
-            # Other types than bridged are not supported for now.
-            next if type != :bridged
-
-            network_name = 'default'
-            network_name = options[:bridge] if options[:bridge]
-
+            # Only private network is supported now. Port forwarding and public
+            # network are not supported via libvirt API, so they are not
+            # implemented in this provider.
+            next if type != :private_network
+
+            # Get options for this interface. Options can be specified in
+            # Vagrantfile in short format (:ip => ...), or provider format
+            # (:libvirt__network_name => ...).
+            options = scoped_hash_override(options, :libvirt)
+            options = { :netmask => '255.255.255.0' }.merge(options)
+
+            # TODO fill first ifaces with adapter option specified.
             if options[:adapter]
               if adapters[options[:adapter]]
                 raise Errors::InterfaceSlotNotAvailable
               end
 
-              adapters[options[:adapter].to_i] = network_name
+              free_slot = options[:adapter].to_i
             else
-              empty_slot = find_empty(adapters, start=1)
-              raise Errors::InterfaceSlotNotAvailable if empty_slot == nil
+              free_slot = find_empty(adapters, start=1)
+              raise Errors::InterfaceSlotNotAvailable if free_slot == nil
+            end
 
-              adapters[empty_slot] = network_name
-            end           
+            # We have slot for interface, fill it with interface configuration.
+            adapters[free_slot] = options
+            adapters[free_slot][:network_name] = interface_network(
+              env[:libvirt_compute].client, adapters[free_slot])
           end
 
-          # Create each interface as new domain device
-          adapters.each_with_index do |network_name, slot_number|
+          # Create each interface as new domain device.
+          adapters.each_with_index do |iface_configuration, slot_number|
             @iface_number = slot_number
-            @network_name = network_name
-            @logger.info("Creating network interface eth#{@iface_number}")
+            @network_name = iface_configuration[:network_name]
+
+            message = "Creating network interface eth#{@iface_number}"
+            message << " connected to network #{@network_name}."
+            @logger.info(message)
+
             begin
               domain.attach_device(to_xml('interface'))
             rescue => e
@@ -66,7 +90,40 @@ module VagrantPlugins
             end
           end
 
+          # Continue the middleware chain. 
           @app.call(env)
+
+          # Configure interfaces that user requested. Machine should be up and
+          # running now.
+          networks_to_configure = []
+
+          adapters.each_with_index do |options, slot_number|
+            # Skip configuring first interface. It's used for provisioning and
+            # it has to be available during provisioning - ifdown command is
+            # not acceptable here.
+            next if slot_number == 0
+
+            network = {
+              :interface => slot_number,
+              #:mac => ...,
+            }
+
+            if options[:ip]
+              network = {
+                :type    => :static,
+                :ip      => options[:ip],
+                :netmask => options[:netmask],
+              }.merge(network)
+            else
+              network[:type] = :dhcp
+            end
+
+            networks_to_configure << network
+          end
+
+          env[:ui].info I18n.t("vagrant.actions.vm.network.configuring")
+          env[:machine].guest.capability(
+            :configure_networks, networks_to_configure) 
         end
 
         private
@@ -77,9 +134,25 @@ module VagrantPlugins
           end
           return nil
         end
-      end
 
+        # Return network name according to interface options.
+        def interface_network(libvirt_client, options)
+          return options[:network_name] if options[:network_name]
+
+          # Get list of all (active and inactive) libvirt networks.
+          available_networks = libvirt_networks(libvirt_client)
+
+          if options[:ip]
+            address = network_address(options[:ip], options[:netmask])
+            available_networks.each do |network|
+              return network[:name] if address == network[:network_address]
+            end
+          end
+
+          # TODO Network default can be missing or named different.
+          return 'default'
+        end
+      end
     end
   end
 end
-
diff --git a/lib/vagrant-libvirt/action/create_networks.rb b/lib/vagrant-libvirt/action/create_networks.rb
new file mode 100644
index 0000000..64b82ce
--- /dev/null
+++ b/lib/vagrant-libvirt/action/create_networks.rb
@@ -0,0 +1,269 @@
+require 'log4r'
+require 'vagrant/util/network_ip'
+require 'vagrant/util/scoped_hash_override'
+require 'ipaddr'
+
+module VagrantPlugins
+  module Libvirt
+    module Action
+      # Prepare all networks needed for domain connections.
+      class CreateNetworks
+        include Vagrant::Util::NetworkIP
+        include Vagrant::Util::ScopedHashOverride
+        include VagrantPlugins::Libvirt::Util::ErbTemplate
+        include VagrantPlugins::Libvirt::Util::LibvirtUtil
+
+        def initialize(app, env)
+          @logger = Log4r::Logger.new("vagrant_libvirt::action::create_networks")
+          @app = app
+
+          @available_networks = []
+          @options = {}
+          @libvirt_client = env[:libvirt_compute].client
+        end
+
+        def call(env)
+
+          # Iterate over networks requested from config. If some network is not
+          # available, create it if possible. Otherwise raise an error.
+          env[:machine].config.vm.networks.each do |type, options|
+
+            # Get a list of all (active and inactive) libvirt networks. This
+            # list is used throughout this class and should be easier to
+            # process than libvirt API calls.
+            @available_networks = libvirt_networks(env[:libvirt_compute].client)
+
+            # Now, we support private networks only. There are two other types
+            # public network and port forwarding, but there are problems with
+            # creating them via libvirt API, so this provider doesn't implement
+            # them.
+            next if type != :private_network
+
+            # Get options for this interface network. Options can be specified
+            # in Vagrantfile in short format (:ip => ...), or provider format
+            # (:libvirt__network_name => ...).
+            @options = scoped_hash_override(options, :libvirt)
+            @options = {
+              :netmask => '255.255.255.0',
+            }.merge(@options)
+
+            # Prepare a hash describing network for this specific interface.
+            @interface_network = {
+              :name            => nil,
+              :ip_address      => nil,
+              :netmask         => @options[:netmask],
+              :network_address => nil,
+              :bridge_name     => nil,
+              :created         => false,
+              :active          => false,
+              :autostart       => false,
+              :libvirt_network => nil,
+            }
+
+            if @options[:ip]
+              handle_ip_option(env)
+            elsif @options[:network_name]
+              handle_network_name_option
+            end
+
+            autostart_network if not @interface_network[:autostart]
+            activate_network if not @interface_network[:active]
+          end
+
+          @app.call(env)
+        end
+
+        private
+
+        # Return hash of network for specified name, or nil if not found.
+        def lookup_network_by_name(network_name)
+          @available_networks.each do |network|
+            return network if network[:name] == network_name
+          end
+          nil
+        end
+
+        # Return hash of network for specified bridge, or nil if not found.
+        def lookup_bridge_by_name(bridge_name)
+          @available_networks.each do |network|
+            return network if network[:bridge_name] == bridge_name
+          end
+          nil
+        end
+
+        # Handle only situations, when ip is specified. Variables @options and
+        # @available_networks should be filled before calling this function.
+        def handle_ip_option(env)
+          return if not @options[:ip]
+
+          net_address = network_address(@options[:ip], @options[:netmask])
+          @interface_network[:network_address] = net_address
+
+          # Set IP address of network (actually bridge). It will be used as
+          # gateway address for machines connected to this network.
+          net = IPAddr.new(net_address)
+          @interface_network[:ip_address] = net.to_range.begin.succ
+
+          # Is there an available network matching to configured ip
+          # address?
+          @available_networks.each do |available_network|
+            if available_network[:network_address] == \
+            @interface_network[:network_address]
+              @interface_network = available_network
+              break
+            end
+          end
+
+          if @options[:network_name]
+            if @interface_network[:created]
+              # Just check for mismatch error here - if name and ip from
+              # config match together.
+              if @options[:network_name] != @interface_network[:name]
+                raise Errors::NetworkNameAndAddressMismatch,
+                  :ip_address   => @options[:ip],
+                  :network_name => @options[:network_name]
+              end
+            else
+              # Network is not created, but name is set. We need to check,
+              # whether network name from config doesn't already exist.
+              if lookup_network_by_name @options[:network_name]
+                raise Errors::NetworkNameAndAddressMismatch,
+                  :ip_address   => @options[:ip],
+                  :network_name => @options[:network_name]
+              end
+
+              # Network with 'name' doesn't exist. Set it as name for new
+              # network.
+              @interface_network[:name] = @options[:network_name]
+            end 
+          end
+
+          # Do we need to create new network?
+          if not @interface_network[:created]
+
+            # TODO stop after some loops. Don't create infinite loops.
+
+            # Is name for new network set? If not, generate a unique one.
+            count = 0
+            while @interface_network[:name] == nil do
+
+              # Generate a network name.
+              network_name = env[:root_path].basename.to_s.dup
+              network_name << count.to_s
+              count += 1
+
+              # Check if network name is unique.
+              next if lookup_network_by_name(network_name)
+
+              @interface_network[:name] = network_name
+            end
+
+            # Generate a unique name for network bridge.
+            count = 0
+            while @interface_network[:bridge_name] == nil do
+              bridge_name = 'virbr'
+              bridge_name << count.to_s
+              count += 1
+
+              next if lookup_bridge_by_name(bridge_name)
+
+              @interface_network[:bridge_name] = bridge_name
+            end
+
+            # Create a private network.
+            create_private_network(env)
+          end
+        end
+
+        # Handle network_name option, if ip was not specified. Variables
+        # @options and @available_networks should be filled before calling this
+        # function.
+        def handle_network_name_option
+          return if @options[:ip] or not @options[:network_name]
+
+          @interface_network = lookup_network_by_name(@options[:network_name])
+          if not @interface_network
+            raise Errors::NetworkNotAvailableError,
+              :network_name => @options[:network_name]
+          end
+        end
+
+        def create_private_network(env)
+          @network_name = @interface_network[:name]
+          @network_bridge_name = @interface_network[:bridge_name]
+          @network_address = @interface_network[:ip_address]
+          @network_netmask = @interface_network[:netmask]
+
+          if @options[:isolated]
+            @network_forward_mode = false
+          else
+            @network_forward_mode = 'nat'
+
+            if @options[:nat_interface]
+              @network_nat_interface = @options[:nat_interface]
+            end
+          end
+
+          if @options[:dhcp_enabled]
+            # Find out DHCP addresses pool range.
+            network_address = "#{@interface_network[:network_address]}/"
+            network_address << "#{@interface_network[:netmask]}"
+            net = IPAddr.new(network_address)
+
+            # First is address of network, second is gateway. Start the range two
+            # addresses after network address.
+            start_address = net.to_range.begin.succ.succ
+
+            # Stop address must not be broadcast.
+            stop_address = net.to_range.end & IPAddr.new('255.255.255.254')
+
+            @network_dhcp_enabled = true
+            @network_range_start = start_address
+            @network_range_stop = stop_address
+          else
+            @network_dhcp_enabled = false
+          end
+
+          begin
+            @interface_network[:libvirt_network] = \
+              @libvirt_client.define_network_xml(to_xml('private_network'))
+          rescue => e
+            raise Errors::CreateNetworkError,
+              :error_message => e.message
+          end
+
+          created_networks_file = env[:machine].data_dir + 'created_networks'
+
+          message = "Saving information about created network "
+          message << "#{@interface_network[:name]}, "
+          message << "UUID=#{@interface_network[:libvirt_network].uuid} "
+          message << "to file #{created_networks_file}."
+          @logger.info(message)
+
+          File.open(created_networks_file, 'a') do |file|
+            file.puts @interface_network[:libvirt_network].uuid
+          end
+        end
+
+        def autostart_network
+          begin
+            @interface_network[:libvirt_network].autostart = true
+          rescue => e
+            raise Errors::AutostartNetworkError,
+              :error_message => e.message
+          end
+        end
+
+        def activate_network
+          begin
+            @interface_network[:libvirt_network].create
+          rescue => e
+            raise Errors::ActivateNetworkError,
+              :error_message => e.message
+          end
+        end
+
+      end
+    end
+  end
+end
diff --git a/lib/vagrant-libvirt/action/destroy_networks.rb b/lib/vagrant-libvirt/action/destroy_networks.rb
new file mode 100644
index 0000000..853be33
--- /dev/null
+++ b/lib/vagrant-libvirt/action/destroy_networks.rb
@@ -0,0 +1,80 @@
+require 'log4r'
+require 'nokogiri'
+
+module VagrantPlugins
+  module Libvirt
+    module Action
+      # Destroy all networks created for this specific domain. Skip
+      # removing if network has still active connections.
+      class DestroyNetworks
+
+        def initialize(app, env)
+          @logger = Log4r::Logger.new("vagrant_libvirt::action::destroy_networks")
+          @app = app
+        end
+
+        def call(env)
+          # If there were some networks created for this machine, in machines
+          # data directory, created_networks file holds UUIDs of each network.
+          created_networks_file = env[:machine].data_dir + 'created_networks'
+
+          # If created_networks file doesn't exist, there are no networks we
+          # need to remove.
+          return @app.call(env) if not File.exist?(created_networks_file)
+
+          # Iterate over each created network UUID and try to remove it.
+          created_networks = []
+          file = File.open(created_networks_file, 'r')
+          file.readlines.each do |network_uuid|
+            begin
+              libvirt_network = env[:libvirt_compute].client.lookup_network_by_uuid(
+                network_uuid)
+            rescue
+              next
+            end
+
+            # Maybe network doesn't exist anymore.
+            next if not libvirt_network
+
+            # Skip removing if network has still active connections.
+            xml = Nokogiri::XML(libvirt_network.xml_desc)
+            connections = xml.xpath('/network/@connections').first
+            if connections != nil
+              created_networks << network_uuid
+              next
+            end
+
+            # Shutdown network first.
+            begin
+              libvirt_network.destroy
+            rescue => e
+            end
+
+            # Undefine network.
+            begin
+              libvirt_network.undefine
+            rescue => e
+              raise Error::DestroyNetworkError,
+                :network_name  => libvirt_network.name,
+                :error_message => e.message
+            end
+          end
+          file.close
+
+          # Update status of created networks after removing some/all of them.
+          if created_networks.length > 0
+            File.open(created_networks_file, 'w') do |file|
+              created_networks.each do |network_uuid|
+                file.puts network_uuid
+              end
+            end
+          else
+            File.delete(created_networks_file)
+          end
+
+          @app.call(env)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/vagrant-libvirt/errors.rb b/lib/vagrant-libvirt/errors.rb
index 288874d..1aafa85 100644
--- a/lib/vagrant-libvirt/errors.rb
+++ b/lib/vagrant-libvirt/errors.rb
@@ -74,6 +74,30 @@ module VagrantPlugins
         error_key(:interface_slot_not_available)
       end
 
+      class NetworkNameAndAddressMismatch < VagrantLibvirtError
+        error_key(:network_name_and_address_mismatch)
+      end
+
+      class CreateNetworkError < VagrantLibvirtError
+        error_key(:create_network_error)
+      end
+
+      class DestroyNetworkError < VagrantLibvirtError
+        error_key(:destroy_network_error)
+      end
+
+      class NetworkNotAvailableError < VagrantLibvirtError
+        error_key(:network_not_available_error)
+      end
+
+      class AutostartNetworkError < VagrantLibvirtError
+        error_key(:autostart_network_error)
+      end
+
+      class ActivateNetworkError < VagrantLibvirtError
+        error_key(:activate_network_error)
+      end
+
       class RsyncError < VagrantLibvirtError
         error_key(:rsync_error)
       end
diff --git a/lib/vagrant-libvirt/templates/private_network.xml.erb b/lib/vagrant-libvirt/templates/private_network.xml.erb
new file mode 100644
index 0000000..8b96df7
--- /dev/null
+++ b/lib/vagrant-libvirt/templates/private_network.xml.erb
@@ -0,0 +1,21 @@
+<network ipv6='yes'>
+  <name><%= @network_name %></name>
+  <bridge name="<%= @network_bridge_name %>" />
+
+  <% if @network_forward_mode != false %>
+    <% if @network_nat_interface %>
+  <forward mode="<%= @network_forward_mode %>" dev="<%= @network_nat_interface %>" />
+    <% else %>
+  <forward mode="<%= @network_forward_mode %>" />
+    <% end %>
+  <% end %>
+
+  <ip address="<%= @network_address %>" netmask="<%= @network_netmask %>">
+  <% if @network_dhcp_enabled  %>
+    <dhcp>
+      <range start="<%= @network_range_start %>" end="<%= @network_range_stop %>" />
+    </dhcp>
+  <% end %>
+  </ip>
+
+</network>
diff --git a/lib/vagrant-libvirt/util.rb b/lib/vagrant-libvirt/util.rb
index 870fea4..3f4cc47 100644
--- a/lib/vagrant-libvirt/util.rb
+++ b/lib/vagrant-libvirt/util.rb
@@ -4,6 +4,7 @@ module VagrantPlugins
       autoload :ErbTemplate, 'vagrant-libvirt/util/erb_template'
       autoload :Collection,  'vagrant-libvirt/util/collection'
       autoload :Timer,  'vagrant-libvirt/util/timer'
+      autoload :LibvirtUtil,  'vagrant-libvirt/util/libvirt_util'
     end
   end
 end
diff --git a/lib/vagrant-libvirt/util/libvirt_util.rb b/lib/vagrant-libvirt/util/libvirt_util.rb
new file mode 100644
index 0000000..907b4e3
--- /dev/null
+++ b/lib/vagrant-libvirt/util/libvirt_util.rb
@@ -0,0 +1,57 @@
+require 'nokogiri'
+require 'vagrant/util/network_ip'
+
+module VagrantPlugins
+  module Libvirt
+    module Util
+      module LibvirtUtil
+        include Vagrant::Util::NetworkIP
+
+        # Return a list of all (active and inactive) libvirt networks as a list
+        # of hashes with their name, network address and status (active or not).
+        def libvirt_networks(libvirt_client)
+          libvirt_networks = []
+
+          active = libvirt_client.list_networks
+          inactive = libvirt_client.list_defined_networks
+
+          # Iterate over all (active and inactive) networks.
+          active.concat(inactive).each do |network_name|
+            libvirt_network = libvirt_client.lookup_network_by_name(
+              network_name)
+
+            # Parse ip address and netmask from the network xml description.
+            xml = Nokogiri::XML(libvirt_network.xml_desc)
+            ip = xml.xpath('/network/ip/@address').first
+            ip = ip.value if ip
+            netmask = xml.xpath('/network/ip/@netmask').first
+            netmask = netmask.value if netmask
+
+            # Calculate network address of network from ip address and
+            # netmask.
+            if ip and netmask
+              network_address = network_address(ip, netmask)
+            else
+              network_address = nil
+            end
+
+            libvirt_networks << {
+              :name            => network_name,
+              :ip_address      => ip,
+              :netmask         => netmask,
+              :network_address => network_address,
+              :bridge_name     => libvirt_network.bridge_name,
+              :created         => true,
+              :active          => libvirt_network.active?,
+              :autostart       => libvirt_network.autostart?,
+              :libvirt_network => libvirt_network,
+            }
+          end
+
+          libvirt_networks
+        end
+
+      end
+    end
+  end
+end
diff --git a/locales/en.yml b/locales/en.yml
index dba56ad..e19438c 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -101,6 +101,20 @@ en:
         Error while attaching new device to domain. %{error_message}
       no_ip_address_error: |-
         No IP address found.
+      network_name_and_address_mismatch: |-
+        Address %{ip_address} does not match with network name %{network_name}.
+        Please fix your configuration and run vagrant again.
+      create_network_error: |-
+        Error occured while creating new network: %{error_message}.
+      network_not_available_error: |-
+        Network %{network_name} is not available. Specify available network
+        name, or an ip address if you want to create a new network.
+      activate_network_error: |-
+        Error while activating network: %{error_message}.
+      autostart_network_error: |-
+        Error while setting up autostart on network: %{error_message}.
+      destroy_network_error: |-
+        Error while removing network %{network_name}. %{error_message}.
 
     states:
       short_paused: |-

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-ruby-extras/vagrant-libvirt.git



More information about the Pkg-ruby-extras-commits mailing list