Skip to content
Browse files

[BGBUILD-211] Alpha edition libvirt registration support, remote and …

…local

Make SFTP plugin more conducive to reuse

Change to non-greedy operator to prevent chomping of uris
  • Loading branch information...
1 parent b954e9a commit 4155989dd8e4b855f20155f20c4a60a6c71e228a @msavy msavy committed Sep 17, 2011
View
2 bin/boxgrinder-build
@@ -39,7 +39,7 @@ ARGV_DUP = ARGV.clone
def validate_hash_option(options, name, value)
value.each do |entry|
- if entry =~ /^(\S+):(\S+)$/
+ if entry =~ /^(\S+?):(\S+)$/
k = $1.strip
v = $2.strip
View
1 lib/boxgrinder-build/helpers/plugin-helper.rb
@@ -26,6 +26,7 @@
require 'boxgrinder-build/plugins/delivery/local/local-plugin'
require 'boxgrinder-build/plugins/delivery/elastichosts/elastichosts-plugin'
require 'boxgrinder-build/plugins/delivery/openstack/openstack-plugin'
+require 'boxgrinder-build/plugins/delivery/libvirt/libvirt-plugin'
require 'boxgrinder-build/plugins/platform/vmware/vmware-plugin'
require 'boxgrinder-build/plugins/platform/ec2/ec2-plugin'
View
124 lib/boxgrinder-build/helpers/sftp-helper.rb
@@ -0,0 +1,124 @@
+require 'boxgrinder-core/helpers/log-helper'
+require 'net/ssh'
+require 'net/sftp'
+
+module BoxGrinder
+ class SFTPHelper
+ def initialize(options={})
+ @log = options[:log] || LogHelper.new
+ end
+
+ def connect(host, username, options={})
+ @log.info "Connecting to #{host}..."
+ @ssh = Net::SSH.start(host, username, options)
+ end
+
+ def connected?
+ return true if !@ssh.nil? and !@ssh.closed?
+ false
+ end
+
+ def disconnect
+ @log.info "Disconnecting from host..."
+ @ssh.close if connected?
+ @ssh = nil
+ end
+
+ def upload_files(path, default_permissions, overwrite, files = {})
+ return if files.size == 0
+
+ raise "You're not connected to server" unless connected?
+
+ @log.debug "Files to upload:"
+
+ files.each do |remote, local|
+ @log.debug "#{File.basename(local)} => #{path}/#{remote}"
+ end
+
+ global_size = 0
+
+ files.each_value do |file|
+ global_size += File.size(file)
+ end
+
+ global_size_kb = global_size / 1024
+ global_size_mb = global_size_kb / 1024
+
+ @log.info "#{files.size} files to upload (#{global_size_mb > 0 ? global_size_mb.to_s + "MB" : global_size_kb > 0 ? global_size_kb.to_s + "kB" : global_size.to_s})"
+
+ @ssh.sftp.connect do |sftp|
+ begin
+ sftp.stat!(path)
+ rescue Net::SFTP::StatusException => e
+ raise unless e.code == 2
+ @ssh.exec!("mkdir -p #{path}")
+ end
+
+ nb = 0
+
+ files.each do |key, local|
+ name = File.basename(local)
+ remote = "#{path}/#{key}"
+ size_b = File.size(local)
+ size_kb = size_b / 1024
+ nb_of = "#{nb += 1}/#{files.size}"
+
+ begin
+ sftp.stat!(remote)
+
+ unless overwrite
+
+ local_md5_sum = `md5sum #{local} | awk '{ print $1 }'`.strip
+ remote_md5_sum = @ssh.exec!("md5sum #{remote} | awk '{ print $1 }'").strip
+
+ if (local_md5_sum.eql?(remote_md5_sum))
+ @log.info "#{nb_of} #{name}: files are identical (md5sum: #{local_md5_sum}), skipping..."
+ next
+ end
+ end
+
+ rescue Net::SFTP::StatusException => e
+ raise unless e.code == 2
+ end
+
+ @ssh.exec!("mkdir -p #{File.dirname(remote) }")
+
+ pbar = ProgressBar.new("#{nb_of} #{name}", size_b)
+ pbar.file_transfer_mode
+
+ sftp.upload!(local, remote) do |event, uploader, * args|
+ case event
+ when :open then
+ when :put then
+ pbar.set(args[1])
+ when :close then
+ when :mkdir then
+ when :finish then
+ pbar.finish
+ end
+ end
+
+ sftp.setstat(remote, :permissions => default_permissions)
+ end
+ end
+ end
+
+ ## Extend the default baked paths in net-ssh/net-sftp to include SUDO_USER
+ ## and/or LOGNAME key directories too.
+ #def generate_keypaths
+ # keys = %w(id_rsa id_dsa)
+ # dirs = %w(.ssh .ssh2)
+ # paths = %w(~/.ssh/id_rsa ~/.ssh/id_dsa ~/.ssh2/id_rsa ~/.ssh2/id_dsa)
+ # ['SUDO_USER','LOGNAME'].inject(paths) do |accum, var|
+ # if user = ENV[var]
+ # accum << dirs.collect do |d|
+ # keys.collect { |k| File.expand_path("~#{user}/#{d}/#{k}") if File.exist?("~#{user}/#{d}/#{k}") }
+ # end.flatten!
+ # end
+ # accum
+ # end.flatten!
+ # paths
+ #end
+
+ end
+end
View
164 lib/boxgrinder-build/plugins/delivery/libvirt/libvirt-capabilities.rb
@@ -0,0 +1,164 @@
+#
+# Copyright 2010 Red Hat, Inc.
+#
+# This is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of
+# the License, or (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this software; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+# 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+
+require 'libvirt'
+require 'enumerator'
+require 'nokogiri'
+require 'ostruct'
+
+module BoxGrinder
+ class LibvirtCapabilities
+
+ class Domain
+ include Comparable
+ attr_accessor :name, :bus, :virt_rank, :virt_map
+ def initialize(name, bus, virt_rank)
+ @name = name
+ @bus = bus
+ @virt_rank = virt_rank.freeze
+ @virt_map = virt_rank.enum_for(:each_with_index).inject({}) do |accum, (virt, rank)|
+ accum.merge(virt => rank)
+ end
+ end
+
+ def <=>(other)
+ self.name <=> other.name
+ end
+ end
+
+ class Plugin
+ include Comparable
+ attr_accessor :name, :domain_rank, :domain_map
+ def initialize(name, domain_rank)
+ @name = name
+ @domain_map = domain_rank.enum_for(:each_with_index).inject({}) do |accum, (domain, rank)|
+ accum.merge(domain.name => {:domain => domain, :rank => rank})
+ end
+ @domain_map.freeze
+ @domain_rank = domain_rank.freeze
+ end
+
+ def <=>(other)
+ self.name <=> other.name
+ end
+ end
+
+ # Arrays are populated in order of precedence. Best first.
+ DEFAULT_DOMAIN_MAPPINGS = {
+ :xen => { :bus => :xen, :virt_rank => [:xen, :linux, :hvm] },
+ :kqemu => { :bus => :virtio, :virt_rank => [:hvm] },
+ :kvm => { :bus => :virtio, :virt_rank => [:xen, :linux, :hvm] },
+ :qemu => { :bus => :ide, :virt_rank => [:xen, :linux, :hvm] },
+ :vbox => { :bus => :virtio, :virt_rank => [:hvm] },
+ :vmware => { :bus => :ide, :virt_rank => [:hvm] }
+ }
+
+ PLUGIN_MAPPINGS = {
+ :default => { :domain_rank => [:kvm, :xen, :kqemu, :qemu] },
+ :virtualbox => { :domain_rank => [:vbox] },
+ :xen => { :domain_rank => [:xen] },
+ :citrix => { :domain_rank => [:xen] },
+ :kvm => { :domain_rank => [:kvm] },
+ :vmware => { :domain_rank => [:vmware] },
+ :ec2 => { :domain_rank => [:xen, :qemu] }
+ }
+
+ DOMAINS = DEFAULT_DOMAIN_MAPPINGS.inject({}) do |accum, mapping|
+ accum.merge(mapping.first => Domain.new(mapping.first, mapping.last[:bus], mapping.last[:virt_rank]))
+ end
+
+ PLUGINS = PLUGIN_MAPPINGS.inject({}) do |accum, mapping|
+ d_refs = mapping.last[:domain_rank].collect{|d| DOMAINS[d]}
+ accum.merge(mapping.first => Plugin.new(mapping.first, d_refs))
+ end
+
+ def initialize(opts={})
+ @log = opts[:log] || LogHelper.new
+ end
+
+ # Connect to the remote machine and determine the best available settings
+ def determine_capabilities(conn, previous_plugin_info)
+ plugin = get_plugin(previous_plugin_info)
+ root = Nokogiri::XML.parse(conn.capabilities)
+ guests = root.xpath("//guest/arch[@name='x86_64']/..")
+
+ guests = guests.sort do |a, b|
+ dom_maps = [a,b].map { |x| plugin.domain_map[xpath_first_intern(x, './/domain/@type')] }
+
+ # Handle unknown mappings
+ next resolve_unknowns(dom_maps) if dom_maps.include?(nil)
+
+ # Compare according to domain ranking
+ dom_rank = dom_maps.map { |m| m[:rank]}.reduce(:<=>)
+
+ # Compare according to virtualisation ranking
+ virt_rank = [a,b].enum_for(:each_with_index).map do |x, i|
+ dom_maps[i][:domain].virt_map[xpath_first_intern(x, './/os_type')]
+ end
+
+ # Handle unknown mappings
+ next resolve_unknowns(virt_rank) if virt_rank.include?(nil)
+
+ # Domain rank first
+ next dom_rank unless dom_rank == 0
+
+ # OS type rank second
+ virt_rank.reduce(:<=>)
+ end
+ # Favourite!
+ build_guest(guests.first)
+ end
+
+ def resolve_unknowns(pair)
+ return 0 if pair.first.nil? and pair.last.nil?
+ return 1 if pair.first.nil?
+ -1 if pair.last.nil?
+ end
+
+ def build_guest(xml)
+ dom = DOMAINS[xpath_first_intern(xml, ".//domain/@type")]
+ bus = 'ide'
+ bus = dom.bus if dom
+
+ OpenStruct.new({
+ :domain_type => xpath_first_intern(xml, ".//domain/@type"),
+ :os_type => xpath_first_intern(xml, './/os_type'),
+ :bus => bus
+ })
+ end
+
+ def xpath_first_intern(xml, path)
+ xml.xpath(path).first.text.intern
+ end
+
+ # At present we don't have enough meta-data to work with to easily generalise,
+ # so we have to assume defaults often. This is something to improve later.
+ def get_plugin(previous_plugin_info)
+ if previous_plugin_info[:type] == :platform
+ if PLUGINS.has_key?(previous_plugin_info[:name])
+ @log.debug("Using #{previous_plugin_info[:name]} mapping")
+ return PLUGINS[previous_plugin_info[:name]]
+ else
+ @log.debug("This plugin does not know what mappings to choose, so will assume default values where user values are not provided.")
+ end
+ end
+ @log.debug("Using default domain mappings.")
+ PLUGINS[:default]
+ end
+ end
+end
View
313 lib/boxgrinder-build/plugins/delivery/libvirt/libvirt-plugin.rb
@@ -0,0 +1,313 @@
+#
+# Copyright 2010 Red Hat, Inc.
+#
+# This is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of
+# the License, or (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this software; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+# 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+
+require 'boxgrinder-build/plugins/base-plugin'
+require 'boxgrinder-build/plugins/delivery/libvirt/libvirt-capabilities'
+require 'boxgrinder-build/helpers/sftp-helper'
+
+require 'libvirt'
+require 'net/sftp'
+require 'fileutils'
+require 'uri'
+require 'etc'
+require 'builder'
+require 'ostruct'
+
+module BoxGrinder
+
+ # @plugin_config [String] connection_uri Libvirt endpoint address. If you are
+ # using authenticated transport such as +ssh+ you should register your keys with
+ # an ssh agent. See: {http://libvirt.org/uri.html Libvirt Connection URIs}.
+ # * Default: +empty string+
+ # * Examples: <tt>qemu+ssh://user@example.com/system</tt>
+ # * +qemu:///system+
+ #
+ # @plugin_config [String] image_delivery_uri Where to deliver the image to. This must be a
+ # local path or an SFTP address. The local ssh-agent is used for keys if available.
+ # * Default: +/var/lib/libvirt/images+
+ # * Examples: +sftp\://user@example.com/some/path+
+ # * +sftp\://user:pass@example.com/some/path+ It is advisable to use keys with ssh-agent.
+ #
+ # @plugin_config [String] libvirt_image_uri Where the image will be on the Libvirt machine.
+ # * Default: +image_delivery_uri+ _path_ element.
+ # * Example: +/var/lib/libvirt/images+
+ #
+ # @plugin_config [Int] default_permissions Permissions of delivered image. Examples:
+ # * Default: +0770+
+ # * Examples: +0755+, +0775+
+ #
+ # @plugin_config [Int] overwrite Overwrite any identically named file at the delivery path.
+ # Also undefines any existing domain of the same name.
+ # * Default: +false+
+ #
+ # @plugin_config [String] script Path to user provided script to modify XML before registration
+ # with Libvirt. Plugin passes the raw XML, and consumes stdout to use as revised XML document.
+ #
+ # @plugin_config [Bool] remote_no_verify Disable certificate verification procedures
+ # * Default: +true+
+ #
+ # @plugin_config [Bool] xml_only Do not connect to the Libvirt hypervisor, just assume sensible
+ # defaults where no user values are provided, and produce the XML domain.
+ # * Default: +false+
+ #
+ # @plugin_config [String] appliance_name Name for the appliance to be registered as in Libvirt.
+ # At present the user can only specify literal strings.
+ # * Default: +name-version-release-os_name-os_version-arch-platform+
+ # * Example: +boxgrinder-f16-rocks+
+ #
+ # @plugin_config [String] domain_type Libvirt domain type.
+ # * Default is a calculated value. Unless you are using +xml_only+ the remote instance will
+ # be contacted and an attempt to determine the best value will be made. If +xml_only+
+ # is set then a safe pre-determined default is used. User-set values take precedence.
+ # See _type_: {http://libvirt.org/formatdomain.html#elements Domain format}
+ # * Examples: +qemu+, +kvm+, +xen+
+ #
+ # @plugin_config [String] virt_type Libvirt virt type.
+ # * Default is a calculated value. Where available paravirtual is preferred.
+ # See _type_: {http://libvirt.org/formatdomain.html#elementsOSBIOS BIOS bootloader}.
+ # * Examples: +hvm+, +xen+, +linux+
+ #
+ # @plugin_config [String] bus Disk bus.
+ # * Default is a pre-determined value depending on the domain type. User-set values take
+ # precedence
+ # * Examples: +virtio+, +ide+
+ #
+ # @plugin_config [String] network Network name. If you require a more complex setup
+ # than a simple network name, then you should create and set a +script+.
+ # * Default: +default+
+ class LibvirtPlugin < BasePlugin
+
+ plugin :type => :delivery, :name => :libvirt, :full_name => "libvirt Virtualisation API"
+
+ def set_defaults
+ set_default_config_value('connection_uri', '')
+ set_default_config_value('script', false)
+ set_default_config_value('image_delivery_uri', '/var/lib/libvirt/images')
+ set_default_config_value('libvirt_image_uri', false)
+ set_default_config_value('remote_no_verify', true)
+ set_default_config_value('overwrite', false)
+ set_default_config_value('default_permissions', 0770)
+ set_default_config_value('xml_only', false)
+ # Manual overrides
+ set_default_config_value('appliance_name', [@appliance_config.name, @appliance_config.version, @appliance_config.release,
+ @appliance_config.os.name, @appliance_config.os.version, @appliance_config.hardware.arch,
+ current_platform].join("-"))
+ set_default_config_value('domain_type', false)
+ set_default_config_value('virt_type', false)
+ set_default_config_value('bus', false)
+ set_default_config_value('network', 'default')
+ set_default_config_value('mac', false)
+ set_default_config_value('noautoconsole', false)
+
+ libvirt_code_patch
+ end
+
+ def validate
+ set_defaults
+
+ ['connection_uri', 'xml_only', 'network', 'domain_type', 'virt_type', 'script',
+ 'bus', 'appliance_name', 'default_permissions', 'overwrite', 'noautoconsole',
+ 'mac'].each do |v|
+ self.instance_variable_set(:"@#{v}", @plugin_config[v])
+ end
+
+ @libvirt_capabilities = LibvirtCapabilities.new(:log => @log)
+ @image_delivery_uri = URI.parse(@plugin_config['image_delivery_uri'])
+ @libvirt_image_uri = (@plugin_config['libvirt_image_uri'] || @image_delivery_uri.path)
+
+ @remote_no_verify = @plugin_config['remote_no_verify'] ? 1 : 0
+
+ (@connection_uri.include?('?') ? '&' : '?') + "no_verify=#{@remote_no_verify}"
+ @connection_uri = URI.parse(@plugin_config['connection_uri'])
+ end
+
+ def execute
+ if @image_delivery_uri.scheme =~ /sftp/
+ @log.info("Transferring file via SFTP...")
+ upload_image
+ else
+ @log.info("Copying disk #{@previous_deliverables.disk} to: #{@image_delivery_uri.path}...")
+ FileUtils.cp(@previous_deliverables.disk, @image_delivery_uri.path)
+ end
+
+ if @xml_only
+ @log.info("Determining locally only.")
+ xml = determine_locally
+ else
+ @log.info("Determining remotely.")
+ xml = determine_remotely
+ end
+ write_xml(xml)
+ end
+
+ # Interact with a libvirtd, attempt to determine optimal settings where possible.
+ # Register the appliance as a new domain.
+ def determine_remotely
+ # Remove password field from URI, as libvirt doesn't support it directly. We can use it for passphrase if needed.
+ lv_uri = URI::Generic.build(:scheme => @connection_uri.scheme, :userinfo => @connection_uri.user,
+ :host => @connection_uri.host, :path => @connection_uri.path,
+ :query => @connection_uri.query)
+
+ # The authentication only pertains to libvirtd itself and _not_ the transport (e.g. SSH).
+ conn = Libvirt::open_auth(lv_uri.to_s, [Libvirt::CRED_AUTHNAME, Libvirt::CRED_PASSPHRASE]) do |cred|
+ case cred["type"]
+ when Libvirt::CRED_AUTHNAME
+ @connection_uri.user
+ when Libvirt::CRED_PASSPHRASE
+ @connection_uri.password
+ end
+ end
+
+ if dom = get_existing_domain(conn, @appliance_name)
+ unless @overwrite
+ @log.fatal("A domain already exists with the name #{@appliance_name}. Set overwrite:true to automatically destroy and undefine it.")
+ raise RuntimeError, "Domain '#{@appliance_name}' already exists" #Make better specific exception
+ end
+ @log.info("Undefining existing domain #{@appliance_name}")
+ undefine_domain(dom)
+ end
+
+ guest = @libvirt_capabilities.determine_capabilities(conn, @previous_plugin_info)
+
+ raise "Remote libvirt machine offered no viable guests!" if guest.nil?
+
+ xml = generate_xml(guest)
+ @log.info("Defining domain #{@appliance_name}")
+ conn.define_domain_xml(xml)
+ xml
+ ensure
+ if conn
+ conn.close unless conn.closed?
+ end
+ end
+
+ # Make no external connections, just dump a basic XML skeleton and provide sensible defaults
+ # where user provided values are not given.
+ def determine_locally
+ domain = @libvirt_capabilities.get_plugin(@previous_plugin_info).domain_rank.last
+ generate_xml(OpenStruct.new({
+ :domain_type => domain.name,
+ :os_type => domain.virt_rank.last,
+ :bus => domain.bus
+ }))
+ end
+
+ # Upload an image via SFTP
+ def upload_image
+ uploader = SFTPHelper.new(:log => @log)
+
+ #SFTP library automagically uses keys registered with the OS first before trying a password.
+ uploader.connect(@image_delivery_uri.host,
+ (@image_delivery_uri.user || Etc.getlogin),
+ :password => @image_delivery_uri.password)
+
+ uploader.upload_files(@image_delivery_uri.path,
+ @default_permissions,
+ @overwrite,
+ File.basename(@previous_deliverables.disk) => @previous_deliverables.disk)
+ ensure
+ uploader.disconnect if uploader.connected?
+ end
+
+ # Preferentially choose user settings
+ def generate_xml(guest)
+ build_xml(:domain_type => (@domain_type || guest.domain_type),
+ :os_type => (@virt_type || guest.os_type),
+ :bus => (@bus || guest.bus))
+ end
+
+ # Build the XML domain definition. If the user provides a script, it will be called after
+ # the basic definition has been constructed with the XML as the sole parameter. The output
+ # from stdout of the script will be used as the new domain definition.
+ def build_xml(opts = {})
+ opts = {:bus => @bus, :os_type => :hvm}.merge!(opts)
+
+ builder = Builder::XmlMarkup.new(:indent => 2)
+
+ xml = builder.domain(:type => opts[:domain_type].to_s) do |domain|
+ domain.name(@appliance_name)
+ domain.description(@appliance_config.summary)
+ domain.memory(@appliance_config.hardware.memory * 1024) #KB
+ domain.vcpu(@appliance_config.hardware.cpus)
+ domain.os do |os|
+ os.type(opts[:os_type].to_s, :arch => @appliance_config.hardware.arch)
+ os.boot(:dev => 'hd')
+ end
+ domain.devices do |devices|
+ devices.disk(:type => 'file', :device => 'disk') do |disk|
+ disk.source(:file => "#{@libvirt_image_uri}/#{File.basename(@previous_deliverables.disk)}")
+ disk.target(:dev => 'hda', :bus => opts[:bus].to_s)
+ end
+ devices.interface(:type => 'network') do |interface|
+ interface.source(:network => @network)
+ interface.mac(:address => @mac) if @mac
+ end
+ devices.console(:type => 'pty') unless @noautoconsole
+ devices.graphics(:type => 'vnc', :port => -1) unless @novnc
+ end
+ domain.features do |features|
+ features.pae if @appliance_config.os.pae
+ end
+ end
+ @log.debug xml
+
+ # Let the user modify the XML specification to their requirements
+ if @script
+ @log.info "Attempting to run user provided script for modifying libVirt XML..."
+ xml = IO::popen("#{@script} --domain '#{xml}'").read
+ @log.debug "Response was: #{xml}"
+ end
+ xml
+ end
+
+ private
+
+ # Look up a domain by name
+ def get_existing_domain(conn, name)
+ return conn.lookup_domain_by_name(name)
+ rescue Libvirt::Error => e
+ return nil if e.libvirt_code == 42 # If domain not defined
+ raise # Otherwise reraise
+ end
+
+ # Undefine a domain. The domain will be destroyed first if required.
+ def undefine_domain(dom)
+ case dom.info.state
+ when Libvirt::Domain::RUNNING, Libvirt::Domain::PAUSED, Libvirt::Domain::BLOCKED
+ dom.destroy
+ end
+ dom.undefine
+ end
+
+ # Libvirt library in older version of Fedora provides no way of getting the
+ # libvirt_code for errors, this patches it in.
+ def libvirt_code_patch
+ return if Libvirt::Error.respond_to?(:libvirt_code, false)
+ Libvirt::Error.module_eval do
+ def libvirt_code; @libvirt_code end
+ end
+ end
+
+ # Write domain XML to file
+ def write_xml(xml)
+ fname = "#{@appliance_name}.xml"
+ File.open("#{@dir.tmp}/#{fname}", 'w'){|f| f.write(xml)}
+ register_deliverable(:xml => fname)
+ end
+ end
+end
View
120 lib/boxgrinder-build/plugins/delivery/sftp/sftp-plugin.rb
@@ -22,6 +22,7 @@
require 'progressbar'
require 'boxgrinder-build/plugins/base-plugin'
require 'boxgrinder-build/helpers/package-helper'
+require 'boxgrinder-build/helpers/sftp-helper'
module BoxGrinder
class SFTPPlugin < BasePlugin
@@ -30,8 +31,12 @@ class SFTPPlugin < BasePlugin
def validate
set_default_config_value('overwrite', false)
set_default_config_value('default_permissions', 0644)
+ set_default_config_value('identity', false)
validate_plugin_config(['path', 'username', 'host'], 'http://boxgrinder.org/tutorials/boxgrinder-build-plugins/#SFTP_Delivery_Plugin')
+
+ @identity = (@plugin_config['identity'] || @plugin_config['i'])
+ @sftp_helper = SFTPHelper.new(:log => @log)
end
def after_init
@@ -43,113 +48,22 @@ def execute
@log.info "Uploading #{@appliance_config.name} appliance via SSH..."
- begin
- #TODO move to a block
- connect
- upload_files(@plugin_config['path'], File.basename(@deliverables[:package]) => @deliverables[:package])
- disconnect
-
- @log.info "Appliance #{@appliance_config.name} uploaded."
- rescue => e
- @log.error e
- @log.error "An error occurred while uploading files."
- end
- end
+ sftp_opts={}
+ sftp_opts.merge!(:password => @plugin_config['password']) if @plugin_config['password']
+ sftp_opts.merge!(:keys => @identity.to_a) if @identity
- def connect
- @log.info "Connecting to #{@plugin_config['host']}..."
- @ssh = Net::SSH.start(@plugin_config['host'], @plugin_config['username'], {:password => @plugin_config['password']})
- end
+ @sftp_helper.connect(@plugin_config['host'], @plugin_config['username'], sftp_opts)
+ @sftp_helper.upload_files(@plugin_config['path'], @plugin_config['default_permissions'], @plugin_config['overwrite'], File.basename(@deliverables[:package]) => @deliverables[:package])
- def connected?
- return true if !@ssh.nil? and !@ssh.closed?
- false
+ @log.info "Appliance #{@appliance_config.name} uploaded."
+ rescue => e
+ @log.error e
+ @log.error "An error occurred while uploading files."
+ raise
+ ensure
+ @sftp_helper.disconnect
end
- def disconnect
- @log.info "Disconnecting from #{@plugin_config['host']}..."
- @ssh.close if connected?
- @ssh = nil
- end
-
- def upload_files(path, files = {})
- return if files.size == 0
-
- raise "You're not connected to server" unless connected?
-
- @log.debug "Files to upload:"
-
- files.each do |remote, local|
- @log.debug "#{File.basename(local)} => #{path}/#{remote}"
- end
-
- global_size = 0
-
- files.each_value do |file|
- global_size += File.size(file)
- end
-
- global_size_kb = global_size / 1024
- global_size_mb = global_size_kb / 1024
-
- @log.info "#{files.size} files to upload (#{global_size_mb > 0 ? global_size_mb.to_s + "MB" : global_size_kb > 0 ? global_size_kb.to_s + "kB" : global_size.to_s})"
-
- @ssh.sftp.connect do |sftp|
- begin
- sftp.stat!(path)
- rescue Net::SFTP::StatusException => e
- raise unless e.code == 2
- @ssh.exec!("mkdir -p #{path}")
- end
-
- nb = 0
-
- files.each do |key, local|
- name = File.basename(local)
- remote = "#{path}/#{key}"
- size_b = File.size(local)
- size_kb = size_b / 1024
- nb_of = "#{nb += 1}/#{files.size}"
-
- begin
- sftp.stat!(remote)
-
- unless @plugin_config['overwrite']
-
- local_md5_sum = `md5sum #{local} | awk '{ print $1 }'`.strip
- remote_md5_sum = @ssh.exec!("md5sum #{remote} | awk '{ print $1 }'").strip
-
- if (local_md5_sum.eql?(remote_md5_sum))
- @log.info "#{nb_of} #{name}: files are identical (md5sum: #{local_md5_sum}), skipping..."
- next
- end
- end
-
- rescue Net::SFTP::StatusException => e
- raise unless e.code == 2
- end
-
- @ssh.exec!("mkdir -p #{File.dirname(remote) }")
-
- pbar = ProgressBar.new("#{nb_of} #{name}", size_b)
- pbar.file_transfer_mode
-
- sftp.upload!(local, remote) do |event, uploader, * args|
- case event
- when :open then
- when :put then
- pbar.set(args[1])
- when :close then
- when :mkdir then
- when :finish then
- pbar.finish
- end
- end
-
- sftp.setstat(remote, :permissions => @plugin_config['default_permissions'])
- end
- end
- end
end
end

0 comments on commit 4155989

Please sign in to comment.
Something went wrong with that request. Please try again.