#!/usr/bin/env ruby # # vmexec runs a list of commands inside a qemu based virtual machine. # # Copyright (c) 2011 Andreas Schneider # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # require 'net/telnet' require 'etc' require 'optparse' require 'ostruct' class VMExecOpts # # Return a structure describing the options. # def self.parse(argv) @args = {} # The options specified on the command line will be collected in *options*. # We set default values here. @options = OpenStruct.new @options.cmds = [] @options.disk = String.new # default values @options.port = 2222 @options.user = Etc.getlogin @options.pwd = String.new @options.mem = 512 @options.smp = 1 @options.cpu = "core2duo" @options.net = "virtio" @options.blk = "virtio" @options.snp = false @options.qemu = String.new @options.args = [] @options.wait = 20 @options.startup_timeout = 300 @options.shutdown_timeout = 120 @options.retires = 0 @options.log = String.new @options.base = String.new @options.opensolaris = false @options.freebsd = false @options.windows = false @options.verbose = false opts = OptionParser.new do |opts| opts.banner = "Usage: vmexec [options] disk.img command1 command2 ... commandN" opts.separator "" opts.separator "Boot the given virtual machine image and wait until it comes up. Run " opts.separator "the list of commands one at a time, aborting on receiving an error." opts.separator "When all commands are run (or one of them failed), shutdown the virtual" opts.separator "machine and exit." opts.separator "" opts.separator "Commands are by default run inside the virtual machine using ssh(1) on" opts.separator "UNIX and telnet(1) on Windows. By prefixing a command with an equals" opts.separator "sign '=', it will instead be run on the host system (for example to" opts.separator "copy files into or out of the virtual machine using scp(1))." opts.separator "" opts.separator "Some care is taken to ensure that the virtual machine is shutdown" opts.separator "gracefully and not left running even in case the controlling tty is" opts.separator "closed or the parent process killed. If a previous virtual machine is" opts.separator "already running on a conflicting port, an attempt is made to shut it" opts.separator "down first. For this purpose, a PID file is created in \$HOME/.vmexec/" opts.separator "" opts.separator "Specific options:" # Optional argument; multi-line description. opts.on("-p", "--port ", Integer, "Forward this port on the host side to the ssh port (port", "22) on the guest side. Must be different for each runvm", "instance running in parallel to avoid conflicts.", "Default is #{@options.port}.", "To copy files in/out of the guest use a command prefixed", "with '=' calling scp(1) with the -P option using the port", "specified here, like this:", " vmexec img.qcow2 \"= scp -P 2222 file.txt localhost:\"") do |port| @options.port = port end opts.on("-u", "--user ", String, "Name of the account to ssh into in the guest. Defaults is", "the name of the user (#{@options.user}) invoking vmexec on the host.") do |user| @options.user = user end opts.on("-p", "--password ", String, "The password of the account to log into in the guest. This is only", "for Windows!.") do |passwd| @options.pwd = passwd end opts.on("-m", "--memory N", Integer, "Amount of memory (in megabytes) to allocate to the guest.", "Default is #{@options.mem}") do |mem| @options.mem = mem end opts.on("--smp N", Integer, "Number of CPU cores to allocate to the guest.", "Default is #{@options.smp}") do |smp| @options.mem = smp end opts.on("-c", "--cpu=NAME", String, "Type of CPU to emulate for KVM, see qemu(1) for details.", " For example:", " --cpu=qemu64 For 64-bit amd64 emulation", " --cpu=qemu32 For 32-bit x86 emulation", " --cpu=qemu32,-nx 32-bit and disable 'no-execute'", " The default is #{@options.cpu}") do |cpu| @options.cpu = cpu end opts.on("--net=NAME", String, "Network device to emulate. The 'virtio' device has good", "performance but may not have driver support in all", "operating systems, if so another can be specified.", "The default is #{@options.net}.") do |net| @options.net = net end opts.on("--blk=NAME", String, "Block device to emulate. The default is #{@options.blk}.") do |blk| @options.blk = blk end opts.on("--snapshot", "Write to temporary files instead of disk image files. This", "is useful if want to test always from the same state") do |snapshot| @options.snp = snapshot end opts.on("--qemu-args=OPT", String, "Pass additional option OPT to qemu. Specify multiple times", "to pass more than one option. For example", "vmexec --qemu-args='-cdrom mycd.iso'") do |qargs| @options.args = qargs.split end opts.on("--qemu=BIN", String, "The qemu binary to use. The by default we search for a kvm", "binary: kvm, qemu-kvm") do |qemu| @options.qemu = qemu end opts.on("--wait=SECS", Integer, "How long should we wait before starting to poll the guest", "ssh or telnet port for it to be up.", "Default #{@options.wait}.") do |wait| @options.wait = wait end opts.on("--startup-timeout=SECS", Integer, "Wait at most this many seconds for the guest OS to respond", "to ssh. If this time is exceeded assume it has failed to", "boot correctly. Default #{@options.startup_timeout}.") do |wait| @options.startup_timeout = wait end opts.on("--shutdown-timeout=SECS", Integer, "Wait at most this many seconds for the guest OS to", "shutdown gracefully after sending a shutdown command. If", "this time is exceeded, assume the guest has failed to", "shutdown gracefully and kill it forcibly.", "Default #{@options.shutdown_timeout}.") do |wait| @options.shutdown_timeout = wait end opts.on("--retries=N", Integer, "If the guest fails to come up, retry the boot this many", "times before giving up. This helps if the virtual machine", "sometimes crashes during boot. Default #{@options.retries}.") do |ret| @options.retries = ret end opts.on("-l", "--logfile=FILE", String, "File to redirect the output from qemu into. This includes", "any (error) messages from qemu, and also includes anything", "the guest writes to the qemu emulated serial port (it can", "be useful to set the guest to send boot loader and kernel", "messages to the serial console and log them with this", "option). Default is to not log this output anywhere.") do |log| @options.log = log end opts.on("-b", "--base-img=IMG", String, "Instead of booting an existing image, create a new", "copy-on-write image based on IMG. This uses the -b option", "of qemu-img(1). IMG is not modified in any way. This way,", "the booted image can be discarded after use, so that each", "use of IMG is using the same reference image with no risk", "of 'polution' between different invocations.", "Note that this DELETES any existing image of the same", "name as the one specified on the command line to boot! It", "will be replaced with the image created as a copy of IMG,", "with any modifications done during the runvm session.") do |base| @options.base = base end # Boolean switches. opts.on("--linux", "This is a Linux VM (default)") do |b| @options.opensolaris = b end opts.on("--opensolaris", "This is a OpenSolaris VM") do |b| @options.opensolaris = b end opts.on("--freebsd", "This is a FreeBSD VM") do |b| @options.opensolaris = b end opts.on("--freebsd", "This is Windows VM") do |b| @options.opensolaris = b end opts.on("-v", "--verbose", "Run verbosely") do |v| @options.verbose = v end opts.separator "" opts.separator "Common options:" # No argument, shows at tail. This will print an options summary. # Try it and see! opts.on_tail("-h", "--help", "Show this message") do puts opts exit end # Another typical switch to print the version. opts.on_tail("--version", "Show version") do puts OptionParser::Version.join('.') exit end end # OptionParser.new begin opts.parse!(argv) rescue OptionParser::InvalidOption => e puts e exit end @args = opts.permute!(argv) if @args.size < 2 puts opts exit end @options.disk = @args.shift @options.cmds = @args @options end # parse() end # class VMExecOpts class VMExec VERSION = '0.1' attr_reader :options def initialize(args) @options = VMExecOpts.parse(args) end # init def run p @options p 'Running vmexec' end # run protected end # class VMExec vmexec = VMExec.new(ARGV) vmexec.run exit #host = Net::Telnet::new( # "Host" => "localhost", # default: "localhost" # "Port" => 2323, # default: 23 # "Binmode" => false, # default: false # "Output_log" => "output.log", # default: nil (no output) # "Dump_log" => "dump.log", # default: nil (no output) # "Prompt" => /C:.*>/, # default: /[$%#>] \z/n # "Telnetmode" => true, # default: true # "Timeout" => 10, # default: 10 # # if ignore timeout then set "Timeout" to false. # "Waittime" => 0 # default: 0 # ) #host.login("libssh", "libssh") { |c| print c } #host.cmd('echo %HOME%') { |c| print c } #host.cmd('set HOME=C:\\Users\\libssh') #host.cmd('echo %HOME%') { |c| print c } #host.cmd('ctest -S workspace/ctest/ctest-vs10-x86-default.cmake') { |c| print c } #host.cmd('ctest -S workspace/ctest/ctest-msys-x86-default.cmake') { |c| print c } #shutdown the machine #host.cmd('shutdown /s /f /m \\\\BESPIN') { |c| print c }