qemu-toolkit 0.2.18

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+
2
+ Copyright (c) 2012 Kaspar Schiess
3
+
4
+ Permission is hereby granted, free of charge, to any person
5
+ obtaining a copy of this software and associated documentation
6
+ files (the "Software"), to deal in the Software without
7
+ restriction, including without limitation the rights to use,
8
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the
10
+ Software is furnished to do so, subject to the following
11
+ conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23
+ OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,7 @@
1
+ SYNOPSIS
2
+
3
+ qemu-toolkit is a small set of scripts to control QEMU kvm virtualised
4
+ machines on a set of illumos hosts.
5
+
6
+ Have a look at 'storadm' and 'vmadm' - inline help will show you the
7
+ possibilities.
data/bin/storadm ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ def root(*args)
4
+ File.expand_path(
5
+ File.join(File.dirname(__FILE__), '..',
6
+ *args))
7
+ end
8
+
9
+ $:.unshift root('lib')
10
+
11
+ require 'qemu-toolkit'
12
+
13
+ require 'qemu-toolkit/storadm'
14
+ QemuToolkit::Storadm.run
data/bin/vmadm ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ def root(*args)
4
+ File.expand_path(
5
+ File.join(File.dirname(__FILE__), '..',
6
+ *args))
7
+ end
8
+
9
+ $:.unshift root('lib')
10
+
11
+ require 'qemu-toolkit'
12
+
13
+ require 'qemu-toolkit/vmadm'
14
+ QemuToolkit::Vmadm.run
data/bin/vmconnect ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # A small helper script that allows to make a VNC connection through ssh
4
+ # securely. Note that on the VM host, you need 'socat' installed for this to
5
+ # work. Uses ssvnc (vncviewer).
6
+
7
+ host, machine = ARGV
8
+ exec "ssvnc exec='/usr/bin/ssh #{host} /usr/local/bin/socat "+
9
+ "STDIO UNIX-CONNECT:/var/run/qemu-toolkit/#{machine}/vm.vnc'"
@@ -0,0 +1,46 @@
1
+ module QemuToolkit
2
+ class Backend::Illumos
3
+ attr_accessor :verbose
4
+
5
+ def zfs *args
6
+ run_cmd :zfs, *args
7
+ end
8
+ def itadm(*args)
9
+ run_cmd :itadm, *args
10
+ end
11
+ def stmfadm *args
12
+ run_cmd :stmfadm, *args
13
+ end
14
+ def qemu name, args
15
+ exec_with_arg0 '/usr/bin/qemu-system-x86_64', name, *args
16
+ end
17
+ def iscsiadm *args
18
+ run_cmd :iscsiadm, *args
19
+ end
20
+ def dladm *args
21
+ run_cmd :dladm, *args
22
+ end
23
+
24
+ def disks(path)
25
+ output = zfs :list, '-H -o name -t volume', '-r', path
26
+ output.split("\n")
27
+ end
28
+
29
+ def run_cmd(*args)
30
+ cmd = args.join(' ')
31
+ puts cmd if verbose
32
+ ret = %x(#{cmd} 2>&1)
33
+
34
+ raise "Execution error: #{cmd}." unless $?.success?
35
+
36
+ ret
37
+ end
38
+ def exec_with_arg0(command, name, *args)
39
+ # exec in this form only wants to be given raw arguments that contain
40
+ # no spaces. This is why we split and flatten args.
41
+ exec [command, name], *args.map { |a| a.split }.flatten
42
+
43
+ fail "exec failed."
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,48 @@
1
+ module QemuToolkit
2
+
3
+ # A configuration class that acts as a singleton, but really isn't. This
4
+ # allows for a mixed style of coding, either using QemuToolkit::Config as
5
+ # an instance or as a class.
6
+ #
7
+ class Config
8
+ class << self # CLASS METHODS
9
+ def method_missing(sym, *args, &block)
10
+ return current.send(sym, *args, &block) if current.respond_to?(sym)
11
+ super
12
+ end
13
+ def respond_to?(sym)
14
+ current.respond_to?(sym) || super
15
+ end
16
+
17
+ def current
18
+ @current ||= new
19
+ end
20
+ def reset
21
+ @current = nil
22
+ end
23
+ end
24
+
25
+ attr_writer :etc
26
+ attr_writer :var_run
27
+
28
+ def etc(*args)
29
+ expand_path(@etc, *args)
30
+ end
31
+ def var_run(*args)
32
+ expand_path(@var_run, *args)
33
+ end
34
+
35
+ # The command runner backend for all commands. Actual execution happens
36
+ # through this instance. This has advantages for testing and for
37
+ # customizing behaviour.
38
+ #
39
+ def backend
40
+ @backend ||= Backend::Illumos.new
41
+ end
42
+ private
43
+ def expand_path(*args)
44
+ File.expand_path(
45
+ File.join(*args))
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,82 @@
1
+
2
+ module QemuToolkit
3
+ # A generic dsl class. You define a target keyword you want to associate with
4
+ # an object instance you also give during construction. The dsl will then
5
+ # react only to a call to TARGET, allowing to use a block to configure the
6
+ # object you give.
7
+ #
8
+ # Inside the block, the following delegations are made:
9
+ #
10
+ # foo 'some value'
11
+ # # delegated to obj.foo= 'some value' if possible
12
+ # # or then to obj.add_foo 'some_value'
13
+ #
14
+ # @example
15
+ # class Bar
16
+ # attr_accessor :name
17
+ # attr_accessor :test
18
+ # end
19
+ # bar = Bar.new
20
+ #
21
+ # dsl = QemuToolkit::DSL::File.new
22
+ # dsl.add_toplevel_target :foo, { |name| Bar.new(name) }
23
+ # dsl.load_file(some_file)
24
+ #
25
+ # # In this example, some_file would contain something like
26
+ # foo('name') do
27
+ # test 'something'
28
+ # end
29
+ # bar.name # => 'name'
30
+ # bar.test # => 'something'
31
+ #
32
+ class DSL
33
+ class File
34
+ attr_reader :objects
35
+
36
+ def initialize
37
+ @objects = []
38
+ end
39
+ def load_file(path)
40
+ eval(
41
+ ::File.read(path),
42
+ binding,
43
+ path)
44
+ end
45
+ def add_toplevel_target target, producer
46
+ define_singleton_method(target) { |*args, &block|
47
+ object = producer.call(*args)
48
+ Unit.new(object, &block)
49
+
50
+ @objects << object
51
+ }
52
+ end
53
+ end
54
+ class Unit
55
+ def initialize(obj, &block)
56
+ @object = obj
57
+ instance_eval(&block)
58
+ end
59
+
60
+ def method_missing(sym, *args, &block)
61
+ delegate = find_delegate_method(sym, @object)
62
+ return super unless delegate
63
+
64
+ if delegate.arity == args.size
65
+ delegate.call(*args, &block)
66
+ else
67
+ delegate.call(args, &block)
68
+ end
69
+ end
70
+ def respond_to?(sym)
71
+ find_delegate_method(sym, @object) || super
72
+ end
73
+
74
+ private
75
+ def find_delegate_method sym, obj
76
+ ["#{sym}=", "add_#{sym}"].each { |dm|
77
+ return obj.method(dm) if obj.respond_to? dm }
78
+ nil
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,80 @@
1
+ module QemuToolkit
2
+ class ISCSITarget
3
+ def initialize(iqn, address, backend)
4
+ @iqn, @address = iqn, address
5
+ @backend = backend
6
+
7
+ # Keep track if disks has ever returned an array of size > 0. This would
8
+ # mean that the target is connected and will stay connected until we
9
+ # change that.
10
+ @mapped = false
11
+ end
12
+
13
+ attr_reader :iqn
14
+ attr_reader :address
15
+
16
+ def mapped?
17
+ disks.size > 0
18
+ end
19
+
20
+ def ensure_exists
21
+ # If the target is mapped already, nothing to do.
22
+ return if mapped?
23
+
24
+ # Map the target
25
+ begin
26
+ @backend.iscsiadm :add, 'static-config', "#{iqn},#{address}", '2>/dev/null'
27
+ rescue
28
+ # Ignore already mapped targets
29
+ end
30
+
31
+ print "Waiting for iSCSI disks to come online..."
32
+ while !mapped?
33
+ print '.'
34
+ sleep 0.5 # takes a short while until login
35
+ end
36
+ puts 'OK.'
37
+ end
38
+
39
+ # A cached 'list target -vS'
40
+ #
41
+ def target_list
42
+ @backend.iscsiadm :list, :target, '-vS'
43
+ end
44
+
45
+ def disks
46
+ luns = []
47
+
48
+ state = 0
49
+ last_lun = nil
50
+
51
+ target_list.each_line do |line|
52
+ case state
53
+ when 0
54
+ state += 1 if line.match(/^Target: #{Regexp.escape(iqn)}\n/m)
55
+ lun = nil
56
+ when 1
57
+ if md=line.match(/^\s+LUN: (\d+)\n/m)
58
+ last_lun = md[1]
59
+ end
60
+
61
+ if last_lun && md=line.match(/^\s+OS Device Name: (\/dev\/rdsk\/.*)\n/m)
62
+ luns << [Integer(last_lun), md[1]]
63
+ end
64
+
65
+ state += 1 if line.match(/^Target: /)
66
+ end
67
+ end
68
+
69
+ luns.sort_by { |no,_| no }.map { |_, dev| p0ify(dev) }
70
+ end
71
+
72
+ private
73
+ def p0ify(str)
74
+ # /dev/rdsk/c4t600144F0503CC9000000503F8B09000Ad0s2 to
75
+ # /dev/rdsk/c4t600144F0503CC9000000503F8B09000Ad0p0
76
+
77
+ str.sub(/s2$/, 'p0')
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,39 @@
1
+ module QemuToolkit
2
+
3
+ class LocalDiskSet
4
+ def self.for(name, backend)
5
+ output = backend.zfs :list, "-oname,#{QemuToolkit::EXPORT_TAG} -H"
6
+ candidates = output.lines.map { |l| l.chomp.strip.split }
7
+ candidates.reject! { |n,_| ! n }
8
+
9
+ # Finds all qemu-toolkit storage spaces that end in '/name':
10
+ # Output from this step are only the spaces that match
11
+ storage_spaces = candidates.
12
+ select { |cand_name, flag|
13
+ %w(true false).include?(flag) && cand_name.end_with?('/'+name) }
14
+
15
+ # Finds all disks for each of the candidate exports:
16
+ # Output from this step should be <name, disks> tuples
17
+ storage_spaces.
18
+ map { |base_name, _| new(
19
+ base_name,
20
+ candidates.map(&:first).
21
+ select { |name|
22
+
23
+ name.match(/#{Regexp.escape(base_name)}\/disk\d+/) } ) }
24
+ end
25
+
26
+ def initialize(name, disks)
27
+ @name = name
28
+ @disks = disks
29
+ end
30
+
31
+ attr_reader :name
32
+
33
+ def each_disk
34
+ @disks.sort.each do |path|
35
+ yield "/dev/zvol/dsk/#{path}"
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,110 @@
1
+ require 'clamp'
2
+
3
+ require 'qemu-toolkit'
4
+
5
+ module QemuToolkit
6
+ class Storadm < Clamp::Command
7
+ option ['-v', '--verbose'], :flag, 'be chatty'
8
+ option ['-n', '--dry-run'], :flag, "don't execute commands, instead just print them"
9
+
10
+ # Command backend to use during the processing of subcommands.
11
+ #
12
+ def backend
13
+ Config.backend
14
+ end
15
+
16
+ # A factory method for VM storage.
17
+ #
18
+ def storage(name)
19
+ VMStorage.new(name, backend)
20
+ end
21
+
22
+ # Main execute method - delegates to _execute in the subcommands. This
23
+ # handles transforming Ruby errors into simple shell errors.
24
+ #
25
+ def execute
26
+ backend.verbose = verbose?
27
+
28
+ _execute
29
+ rescue => error
30
+ raise if verbose? || $rspec_executing
31
+
32
+ $stderr.puts error.to_s
33
+ exit 1
34
+ end
35
+
36
+ subcommand('clone',
37
+ 'Clones an existing VM storage as starting point for quickly creating new VMs.') do
38
+
39
+ parameter 'NAME', 'name of the new VM storage, ie: new_vm'
40
+ parameter 'TEMPLATE', 'VM storage to use as a template, ie: pool2/template'
41
+ parameter 'VERSION', 'template version to clone (aka zfs snapshot), ie: v1.2'
42
+
43
+ def _execute
44
+ storage(template).clone(name, version)
45
+ end
46
+ end
47
+
48
+ subcommand('export',
49
+ 'Creates an iSCSI target for the VM storage, with each disk mapped to a LUN.') do
50
+ parameter 'NAME', 'name of the VM storage, ie: pool1/new_vm'
51
+
52
+ def _execute
53
+ s = storage(name)
54
+ # Export the storage
55
+ s.export
56
+ # Print the IQN to be helpful.
57
+ puts "Created: #{s.iqn}"
58
+ end
59
+ end
60
+
61
+ subcommand('hide',
62
+ 'Removes iSCSI target for the VM storage from the system.') do
63
+ parameter 'NAME', 'name of the VM storage, ie: pool1/new_vm'
64
+
65
+ def _execute
66
+ storage(name).hide
67
+ end
68
+ end
69
+
70
+ subcommand('create',
71
+ "Creates a VM storage space from scratch. Pass the size for at least one disk volume.") do
72
+
73
+ parameter 'NAME', 'name of the VM storage, ie: pool1/new_vm'
74
+ parameter 'SIZE ...', 'sizes of one or more disks, in ZFS format, ie: 10G'
75
+
76
+ def _execute
77
+ fail "Must create at least one disk." if size_list.empty?
78
+
79
+ s = storage(name)
80
+
81
+ fail "Must specify absulute storage path (pool/dataset) for creation." \
82
+ if s.relative_name?
83
+
84
+ s.create(size_list)
85
+ end
86
+ end
87
+
88
+ subcommand('list',
89
+ "Lists all vm stores on this machine.") do
90
+
91
+ def _execute
92
+ re_splitter = /\s+/
93
+ store_list = backend.zfs :list, "-H -oname,#{QemuToolkit::EXPORT_TAG} -t filesystem"
94
+ store_list.each_line do |line|
95
+ clean_line = line.chomp.strip
96
+ last_space = clean_line.rindex(re_splitter)
97
+ next unless last_space
98
+
99
+ name, _, flag = clean_line.rpartition(re_splitter)
100
+ if flag != '-'
101
+ printf "%-20s", name
102
+ puts (flag == 'true') ?
103
+ storage(name).iqn :
104
+ '-'
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,331 @@
1
+ require 'qemu-toolkit/config'
2
+ require 'qemu-toolkit/dsl'
3
+ require 'qemu-toolkit/iscsi_target'
4
+
5
+ require 'socket'
6
+
7
+ module QemuToolkit
8
+ # Abstracts a virtual machine on a vm host. This class provides all sorts
9
+ # of methods that execute administration actions.
10
+ #
11
+ class VM
12
+ class << self # CLASS METHODS
13
+ # Load all vm descriptions and provide an iterator for them.
14
+ #
15
+ def all(backend=nil)
16
+ Enumerator.new do |yielder|
17
+ Dir[Config.etc('*.rb')].each do |vm_file|
18
+ # Load all virtual machines from the given file
19
+ dsl = DSL::File.new
20
+ dsl.add_toplevel_target :virtual_machine, lambda { |name|
21
+ VM.new(backend).tap { |vm| vm.name = name } }
22
+
23
+ dsl.load_file(vm_file)
24
+
25
+ # Yield them all in turn
26
+ dsl.objects.each do |vm|
27
+ yielder << vm
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ # Access the definition of a single vm.
34
+ #
35
+ def [](name, backend=nil)
36
+ all(backend).find { |vm| vm.name === name }
37
+ end
38
+ end
39
+
40
+ # VM name
41
+ attr_accessor :name
42
+ # iSCSI target iqn and ip address to connect to
43
+ attr_accessor :iscsi_target
44
+ # A list of network cards that will be connected to vnics on the host.
45
+ attr_reader :nics
46
+ # A list of network configuration statements that will be passed through
47
+ # to qemu.
48
+ attr_reader :nets
49
+ # The number of cpus to configure, defaults to 2.
50
+ attr_accessor :cpus
51
+ # Ram in megabytes
52
+ attr_accessor :ram
53
+ # VNC display port
54
+ attr_accessor :vnc_display
55
+
56
+ def initialize(backend)
57
+ @disks = []
58
+ @drives = []
59
+ @nics = []
60
+ @nets = []
61
+ @cpus = 2
62
+ @ram = 1024
63
+ @backend = backend
64
+ @vnc_display = nil
65
+ @extra_args = []
66
+ end
67
+
68
+ def add_drive(parameters)
69
+ @drives << parameters
70
+ end
71
+ def add_disk(path)
72
+ @disks << path
73
+ end
74
+ def add_nic(name, parameters)
75
+ @nics << [name, parameters]
76
+ end
77
+ def add_net(type, parameters)
78
+ @nets << [type, parameters]
79
+ end
80
+ def add_extra_arg(argument)
81
+ @extra_args << argument
82
+ end
83
+
84
+ # Runs the VM using qemu.
85
+ def start(dryrun, opts={})
86
+ if dryrun
87
+ puts command(opts)
88
+ else
89
+ # Make sure var/run/qemu-toolkit/VMNAME exists.
90
+ FileUtils.mkdir_p run_path
91
+
92
+ @backend.qemu("vm<#{name}>", command(opts))
93
+ end
94
+ end
95
+
96
+ # Returns the command that is needed to run this virtual machine. Note
97
+ # that this also modifies system configuration and is not just a routine
98
+ # that returns a string.
99
+ #
100
+ # @return String command to run the machine
101
+ #
102
+ def command opts={}
103
+ cmd = []
104
+ cmd << "-name #{name}"
105
+ cmd << "-m #{ram}"
106
+ cmd << "-daemonize"
107
+ cmd << '-nographic'
108
+ cmd << "-cpu qemu64"
109
+ cmd << "-smp #{cpus}"
110
+ cmd << "-no-hpet"
111
+ cmd << "-enable-kvm"
112
+ cmd << "-vga cirrus"
113
+ cmd << "-k de-ch"
114
+ cmd << "-parallel none"
115
+ cmd << "-usb"
116
+ cmd << '-usbdevice tablet'
117
+
118
+ # Add disks
119
+ cmd += disk_options
120
+
121
+ # Was an iso image given to boot from?
122
+ if iso_path=opts[:bootiso]
123
+ cmd << "-cdrom #{iso_path}"
124
+ cmd << "-boot order=cd,once=d"
125
+ else
126
+ cmd << '-boot order=cd'
127
+ end
128
+
129
+ # Set paths for communication with vm
130
+ cmd << "-pidfile #{pid_path}"
131
+
132
+ cmd << socket_chardev(:monitor, monitor_path)
133
+ cmd << "-monitor chardev:monitor"
134
+
135
+ cmd << socket_chardev(:serial0, run_path('vm.console'))
136
+ cmd << "-serial chardev:serial0"
137
+ cmd << socket_chardev(:serial1, run_path('vm.ttyb'))
138
+ cmd << "-serial chardev:serial1"
139
+
140
+ # vnc socket
141
+ cmd << "-vnc unix:#{run_path('vm.vnc')}"
142
+
143
+ # If vnc_display is set, allow configuring a TCP based VNC port:
144
+ if vnc_display
145
+ cmd << "-vnc #{vnc_display}"
146
+ end
147
+
148
+ # networking: nic
149
+ vlan = 0
150
+ # Look up all existing vnics for this virtual machine
151
+ vnics = Vnic.for_prefix(name, @backend)
152
+
153
+ nics.each do |nic_name, parameters|
154
+ # All vnics that travel via the given interface (:via)
155
+ vnics_for_interface = vnics[parameters[:via]] || []
156
+
157
+ # Get a vnic that travels via the given interface.
158
+ vnic = vnics_for_interface.shift ||
159
+ Vnic.create(name, parameters[:via], @backend)
160
+
161
+ cmd << "-net vnic,"+
162
+ parameter_list(
163
+ vlan: vlan, name: nic_name,
164
+ ifname: vnic.vnic_name,
165
+ macaddr: parameters[:macaddr])
166
+ cmd << "-net nic,"+
167
+ parameter_list(
168
+ vlan: vlan, name: nic_name,
169
+ model: parameters[:model] || 'virtio',
170
+ macaddr: parameters[:macaddr])
171
+
172
+ vlan += 1
173
+ end
174
+
175
+ # networking: net
176
+ nets.each do |type, parameters|
177
+ cmd << "-net #{type},"+
178
+ parameter_list(parameters)
179
+ end
180
+
181
+ # Extra arguments
182
+ cmd += @extra_args
183
+
184
+ return cmd
185
+ end
186
+ def disk_options
187
+ cmd = []
188
+
189
+ if @disks.empty? && !iscsi_target && @drives.empty?
190
+ raise "No disks defined, can't run."
191
+ end
192
+
193
+ disk_index = 0
194
+ if iscsi_target
195
+ target = produce_target(*iscsi_target)
196
+ target.ensure_exists
197
+
198
+ target.disks.each do |device|
199
+ params = {
200
+ file: device,
201
+ if: 'virtio',
202
+ index: disk_index,
203
+ media: 'disk'
204
+ }
205
+ params[:boot] = 'on' if disk_index == 0
206
+ cmd << "-drive " + parameter_list(params)
207
+
208
+ disk_index += 1
209
+ end
210
+ end
211
+
212
+ @disks.each do |path|
213
+ cmd << "-drive file=#{path},if=virtio,index=#{disk_index},"+
214
+ "media=disk,boot=on"
215
+ disk_index += 1
216
+ end
217
+
218
+ @drives.each do |drive_options|
219
+ cmd << "-drive " +
220
+ parameter_list(drive_options.merge(index: disk_index))
221
+ disk_index += 1
222
+ end
223
+
224
+ return cmd
225
+ end
226
+
227
+ # Connects the current terminal to the given socket. Available sockets
228
+ # include :monitor, :vnc, :console, :ttyb.
229
+ #
230
+ def connect(socket)
231
+ socket_path = run_path("vm.#{socket}")
232
+ cmd = "socat stdio unix-connect:#{socket_path}"
233
+
234
+ exec cmd
235
+ end
236
+
237
+ # Kills the vm the hard way.
238
+ #
239
+ def kill
240
+ run_cmd "kill #{pid}"
241
+ end
242
+
243
+ # Sends a shutdown command via the monitor socket of the virtual machine.
244
+ #
245
+ def shutdown
246
+ monitor_cmd 'system_powerdown'
247
+ end
248
+
249
+ # Returns an ISCSITarget for host and port.
250
+ #
251
+ def produce_target(host, port)
252
+ ISCSITarget.new(host, port, @backend)
253
+ end
254
+
255
+ # Returns true if the virtual machine seems to be currently running.
256
+ #
257
+ def running?
258
+ if File.exist?(pid_path)
259
+ # Prod the process using kill. This will not actually kill the
260
+ # process!
261
+ begin
262
+ Process.kill(0, pid)
263
+ rescue Errno::ESRCH
264
+ # When this point is reached, the process doesn't exist.
265
+ return false
266
+ end
267
+
268
+ return true
269
+ end
270
+
271
+ return false
272
+ end
273
+
274
+ # Attempts to read and return the pid of the running VM process.
275
+ #
276
+ def pid
277
+ Integer(File.read(pid_path).lines.first.chomp)
278
+ end
279
+
280
+ private
281
+ def monitor_cmd(cmd)
282
+ socket = UNIXSocket.new(monitor_path)
283
+ socket.puts cmd
284
+ socket.close
285
+ end
286
+
287
+ def socket_chardev(name, path)
288
+ "-chardev socket,id=#{name},path=#{path},server,nowait"
289
+ end
290
+
291
+ # Formats a parameter list as key=value,key=value
292
+ #
293
+ def parameter_list(parameters)
294
+ parameters.
295
+ map { |k,v| "#{k}=#{v}" }.
296
+ join(',')
297
+ end
298
+
299
+ # Returns the path below /var/run (usually) that contains runtime files
300
+ # for the virtual machine.
301
+ #
302
+ def run_path(*args)
303
+ Config.var_run(name, *args)
304
+ end
305
+
306
+ # Returns the file path of the vm pid file.
307
+ #
308
+ def pid_path
309
+ run_path 'vm.pid'
310
+ end
311
+
312
+ # Returns the file path of the monitor socket (unix socket below /var/run)
313
+ # usually. )
314
+ #
315
+ def monitor_path
316
+ run_path 'vm.monitor'
317
+ end
318
+
319
+ # Runs a command and returns its stdout. This raises an error if the
320
+ # command doesn't exit with a status of 0.
321
+ #
322
+ def run_cmd(*args)
323
+ cmd = args.join(' ')
324
+ ret = %x(#{cmd})
325
+
326
+ raise "Execution error: #{cmd}." unless $?.success?
327
+
328
+ ret
329
+ end
330
+ end
331
+ end
@@ -0,0 +1,180 @@
1
+ module QemuToolkit
2
+ class VMStorage
3
+ def initialize(name, backend)
4
+ @name = name
5
+ @backend = backend
6
+ end
7
+
8
+ attr_reader :backend
9
+ attr_reader :name
10
+
11
+ def create(sizes)
12
+ backend.zfs :create, "-o #{QemuToolkit::EXPORT_TAG}=false", name
13
+ sizes.each_with_index do |size, idx|
14
+ backend.zfs :create, "-V #{size}", name + "/disk#{idx+1}"
15
+ end
16
+ end
17
+
18
+ # Clones this vm storage to the given target name. If target_name is
19
+ # given without pool part, clone will live in the same pool as its parent
20
+ # vm storage. The version argument specifies which version should be
21
+ # cloned and must be a recursive snapshot on the parent vm.
22
+ #
23
+ def clone(target_name, version)
24
+ raise ArgumentError, "Must specify the dataset path for cloning." \
25
+ if relative_name?
26
+
27
+ path, vm_name = split
28
+ target_path = join path, target_name
29
+ backend.zfs :clone, "#@name@#{version}", target_path
30
+
31
+ backend.disks(name).each do |disk_path|
32
+ disk_name = subtract @name, disk_path
33
+ target_disk_name = join path, target_name, disk_name
34
+ backend.zfs :clone, "#{disk_path}@#{version}", target_disk_name
35
+ end
36
+
37
+ # Mark the dataset as hidden
38
+ backend.zfs :set, "#{QemuToolkit::EXPORT_TAG}=false", join(path, target_name)
39
+ end
40
+
41
+ # Exports all disks of a virtual machine (called 'diskN' below the main
42
+ # dataset) as LUNs below a single iqn for the machine.
43
+ #
44
+ def export
45
+ fail "VM storage #{name} does not exist." unless exist?
46
+ fail "VM storage #{name} is already exported." if exported?
47
+
48
+ path, vm_name = split
49
+
50
+ backend.stmfadm 'create-tg', vm_name
51
+
52
+ backend.disks(name).each do |disk_path|
53
+ output = backend.stmfadm 'create-lu', "/dev/zvol/rdsk/"+disk_path
54
+
55
+ md=output.match /Logical unit created: ([0-9A-F]+)/
56
+ raise "Could not parse created LU. (#{output.inspect})" unless md
57
+
58
+ backend.stmfadm 'add-view', "-t #{vm_name}", md[1]
59
+ end
60
+
61
+ backend.stmfadm 'add-tg-member', "-g #{vm_name}", iqn
62
+ backend.itadm 'create-target', "-n #{iqn}", '-t frontend'
63
+
64
+ # Mark the dataset as exported
65
+ backend.zfs :set, "#{QemuToolkit::EXPORT_TAG}=true", name
66
+ end
67
+
68
+ # Returns true if the storage exists and is exported currently. Returns
69
+ # false if the storage exists and is not exported. In all other cases
70
+ # this method returns nil.
71
+ #
72
+ def exported?
73
+ case (export_tag || '').chomp
74
+ when 'true'
75
+ return true
76
+ when 'false'
77
+ return false
78
+ end
79
+
80
+ return nil
81
+ end
82
+
83
+ # Returns true if the storage seems to exist and be a valid vm storage.
84
+ #
85
+ def exist?
86
+ ! export_tag.nil?
87
+ end
88
+
89
+ # Hides the vm storage from iSCSI.
90
+ #
91
+ def hide
92
+ fail "VM storage #{name} does not exist." unless exist?
93
+ fail "VM storage #{name} is already hidden." unless exported?
94
+
95
+ path, vm_name = split
96
+
97
+ raise "Cannot find an exported dataset named #{name}. " \
98
+ unless exported?
99
+
100
+ backend.stmfadm 'offline-target', iqn
101
+ backend.itadm 'delete-target', iqn
102
+
103
+ # Parse existing lus, look for vm_name/diskN
104
+ lus = backend.stmfadm 'list-lu', '-v'
105
+ last_lu = nil
106
+ lus.each_line do |line|
107
+ if md=line.match(/LU Name: ([0-9A-F]+)/)
108
+ last_lu = md[1]
109
+ end
110
+ if line.include?('Data File') &&
111
+ line.include?('/dev/zvol/rdsk') &&
112
+ line.match(%r(/#{Regexp.escape(vm_name)}/disk\d+))
113
+
114
+ backend.stmfadm 'delete-lu', last_lu
115
+ end
116
+ end
117
+
118
+ backend.stmfadm 'delete-tg', vm_name
119
+
120
+ # Mark the dataset as hidden
121
+ backend.zfs :set, "#{QemuToolkit::EXPORT_TAG}=false", name
122
+ end
123
+
124
+ # Returns whether the name used to construct this instance is relative
125
+ # or absolute. A relative name identifies a storage within its pool,
126
+ # an absolute name identifies it within the whole system.
127
+ #
128
+ # VMStorage.new('foo').relative_name? # => true
129
+ # VMStorage.new('b1/foo').relative_name? # => false
130
+ #
131
+ def relative_name?
132
+ @name.index('/') == nil
133
+ end
134
+
135
+ def iqn
136
+ _, n = split
137
+ "iqn.2012-01.com.qemu-toolkit:#{n}"
138
+ end
139
+
140
+ private
141
+
142
+ # Returns the (cached) value of QemuToolkit::EXPORT_TAG of this storage.
143
+ # This does nothing more than execute
144
+ # zfs get -Ho value #{QemuToolkit::EXPORT_TAG} NAME
145
+ # and handle an exception by returning nil.
146
+ #
147
+ def export_tag
148
+ @export_tag ||= begin
149
+ backend.zfs :get, "-Ho value #{QemuToolkit::EXPORT_TAG}", name
150
+ rescue
151
+ nil
152
+ end
153
+ end
154
+
155
+ # Returns a pair of dataset path and dataset name for the vm storage.
156
+ #
157
+ # @example
158
+ # VMStorage.new('foo/bar/baz').split
159
+ # # => ['foo/bar', 'baz']
160
+ #
161
+ def split
162
+ File.split(@name)
163
+ end
164
+
165
+ # Joins parts of a vm storage name.
166
+ #
167
+ def join(*args)
168
+ File.join(*args)
169
+ end
170
+
171
+ # Subtracts a prefix from a given string.
172
+ #
173
+ # @example
174
+ # subtract 'foo', 'foo/bar' # => '/bar'
175
+ def subtract(prefix, string)
176
+ raise ArgumentError unless string.start_with?(prefix)
177
+ string[prefix.size..-1]
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,182 @@
1
+ require 'clamp'
2
+
3
+ require 'qemu-toolkit/local_disk_set'
4
+
5
+ module QemuToolkit
6
+ class Vmadm < Clamp::Command
7
+
8
+ option ['-v', '--verbose'], :flag, 'be chatty'
9
+ option ['-n', '--dry-run'], :flag,
10
+ "don't execute commands, instead just print them"
11
+
12
+ option '--vmpath', "VMPATH",
13
+ "path to vm descriptions", default: '/etc/qemu-toolkit'
14
+ option '--varrun', 'VARRUN',
15
+ 'path to runtime files', default: '/var/run/qemu-toolkit'
16
+
17
+ # Command backend to use during the processing of subcommands.
18
+ #
19
+ def backend
20
+ Config.backend
21
+ end
22
+
23
+ # Main execute method - delegates to _execute in the subcommands. This
24
+ # handles transforming Ruby errors into simple shell errors.
25
+ #
26
+ def execute
27
+ backend.verbose = verbose?
28
+
29
+ Config.etc = vmpath
30
+ Config.var_run = varrun
31
+
32
+ fail NotImplementedError, "Missing subcommand." unless respond_to?(:_execute)
33
+ _execute
34
+ rescue => error
35
+ raise if verbose? || $rspec_executing
36
+
37
+ $stderr.puts error.to_s
38
+ exit 1
39
+ end
40
+
41
+ subcommand('list',
42
+ 'Lists all virtual machines on this system') do
43
+ def _execute
44
+ VM.all(backend).each do |vm|
45
+ printf "%-20s", vm.name
46
+ printf " %5s", vm.running? ? vm.pid : 'off'
47
+ puts
48
+ end
49
+ end
50
+ end
51
+
52
+ # subcommand('random-mac',
53
+ # 'Generates a random MAC address') do
54
+ # def _execute
55
+ # puts random_mac_address
56
+ # end
57
+ # end
58
+
59
+ subcommand('create',
60
+ 'Creates a configuration file for the VM from a template, filling in plausible values.') do
61
+
62
+ parameter 'NAME', 'name of the virtual machine'
63
+
64
+ def _execute
65
+ if VM[name]
66
+ puts "Machine already exists."
67
+ exit 1
68
+ end
69
+
70
+ File.write(
71
+ Config.etc("#{name}.rb"),
72
+ vm_template(name))
73
+
74
+ FileUtils.mkdir(Config.var_run(name))
75
+ end
76
+
77
+ def vm_template(name)
78
+ local_disks = StringIO.new
79
+
80
+ disk_sets = LocalDiskSet.for(name, backend)
81
+ disk_sets.each do |set|
82
+ local_disks.puts " # Disks for storage at #{set.name}"
83
+ set.each_disk do |dev_path|
84
+ local_disks.puts " disk '#{dev_path}'"
85
+ end
86
+ local_disks.puts
87
+ end
88
+
89
+ %Q(virtual_machine "#{name}" do
90
+ # Block device setup
91
+ #
92
+ # Either configure a remote disk (via iSCSI):
93
+ # iscsi_target 'iqn.2012-01.com.qemu-toolkit:#{name}', "10.40.0.1"
94
+ #
95
+ # Or a local disk, like a zvol for example:
96
+ # disk /dev/zvol/dsk/pool/#{name}/disk1
97
+ #{local_disks.string}
98
+
99
+ # Network configuration
100
+ # nic 'eth0',
101
+ # macaddr: '#{random_mac_address}',
102
+ # via: 'igbX'
103
+ end
104
+ )
105
+ end
106
+ end
107
+
108
+ subcommand('start',
109
+ 'Starts the virtual machine and daemonizes it.') do
110
+
111
+ parameter 'NAME', 'name of the virtual machine'
112
+ option '--bootiso', 'BOOTISO', 'boots this ISO instead of disk0'
113
+
114
+ def _execute
115
+ vm(name).start(dry_run?, bootiso: bootiso)
116
+ end
117
+ end
118
+
119
+ subcommand('monitor',
120
+ 'Enter QEMU monitor interactively for given VM.') do
121
+ parameter 'NAME', 'name of the virtual machine'
122
+
123
+ def _execute
124
+ vm(name).connect(:monitor)
125
+ end
126
+ end
127
+ subcommand('shutdown',
128
+ 'Shuts the VM down by issuing a system/powerdown event.') do
129
+ parameter 'NAME', 'name of the virtual machine'
130
+
131
+ def _execute
132
+ vm(name).shutdown
133
+ end
134
+ end
135
+
136
+ subcommand('kill',
137
+ 'Tries to kill the VM using the kill command.') do
138
+ parameter 'NAME', 'name of the virtual machine'
139
+
140
+ def _execute
141
+ vm(name).kill
142
+ end
143
+ end
144
+
145
+ subcommand('vnc',
146
+ 'Connect VM VNC server to standard IO. (use ssvnc to connect)') do
147
+ parameter 'NAME', 'name of the virtual machine'
148
+
149
+ def _execute
150
+ vm(name).connect(:vnc)
151
+ end
152
+ end
153
+
154
+ subcommand('console',
155
+ 'Opens serial console to VM. This only works if you configure your VM accordingly.') do
156
+ parameter 'NAME', 'name of the virtual machine'
157
+
158
+ def _execute
159
+ vm(name).connect(:console)
160
+ end
161
+ end
162
+
163
+ def random_mac_address
164
+ # Please see this discussion if improving this:
165
+ # http://stackoverflow.com/questions/8484877/mac-address-generator-in-python
166
+ mac = [ 0x00, 0x24, 0x81,
167
+ rand(0x7f),
168
+ rand(0xff),
169
+ rand(0xff) ]
170
+ mac.map { |e| e.to_s(16) }.join(':')
171
+ end
172
+ def vm(name)
173
+ vm = VM[name, backend]
174
+ unless vm
175
+ puts "No virtual machine by the name '#{name}' found."
176
+ exit 1
177
+ end
178
+
179
+ vm
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,61 @@
1
+ module QemuToolkit
2
+ class Vnic
3
+ class << self
4
+ def for_prefix(prefix, background)
5
+ vnics = Hash.new { |h,k| h[k] = Array.new; }
6
+ links = background.dladm 'show-vnic', '-po link,over,vid'
7
+
8
+ links.each_line do |line|
9
+ next unless line.start_with?(prefix)
10
+ link, over, vid = line.chomp.split(':')
11
+
12
+ # Assumes that vid 0 is always the 'no vlan' VLAN
13
+ over = "#{over}:#{vid}" if vid.to_i > 0
14
+
15
+ if md=link.match(/^(?<vm>.*)_(?<link_no>\d+)$/)
16
+ vnics[over] << Vnic.new(md[:vm], Integer(md[:link_no]), over)
17
+ end
18
+ end
19
+
20
+ vnics
21
+ end
22
+
23
+ def create(prefix, over, backend)
24
+ # Retrieve links that exist for this prefix and this over interface
25
+ vnics = for_prefix(prefix, backend).values.flatten
26
+ next_vnic_number = (vnics.map(&:number).max || 0) + 1
27
+
28
+ new(prefix, next_vnic_number, over).tap { |o|
29
+ o.create(backend) }
30
+ end
31
+
32
+ end
33
+
34
+ def initialize(prefix, number, over)
35
+ @prefix, @number, @over = prefix, number, over
36
+ end
37
+
38
+ attr_reader :prefix
39
+ attr_reader :number
40
+ attr_reader :over
41
+
42
+ def ==(other)
43
+ self.prefix == other.prefix &&
44
+ self.number == other.number &&
45
+ self.over == other.over
46
+ end
47
+
48
+ def create backend
49
+ if over.index(':')
50
+ iface, vlan = over.split(':')
51
+ backend.dladm 'create-vnic', "-l #{iface} -v #{vlan}", vnic_name
52
+ else
53
+ backend.dladm 'create-vnic', "-l #{over}", vnic_name
54
+ end
55
+ end
56
+
57
+ def vnic_name
58
+ "#{prefix}_#{number}"
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,12 @@
1
+
2
+ module QemuToolkit
3
+ EXPORT_TAG = 'qemu_toolkit:export'
4
+
5
+ module Backend; end
6
+ end
7
+
8
+ require 'qemu-toolkit/config'
9
+ require 'qemu-toolkit/vm'
10
+ require 'qemu-toolkit/vm_storage'
11
+ require 'qemu-toolkit/backend/illumos'
12
+ require 'qemu-toolkit/vnic'
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: qemu-toolkit
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.18
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Kaspar Schiess
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: clamp
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description:
31
+ email: kaspar.schiess@technologyastronauts.ch
32
+ executables:
33
+ - vmadm
34
+ - storadm
35
+ extensions: []
36
+ extra_rdoc_files:
37
+ - README
38
+ files:
39
+ - LICENSE
40
+ - README
41
+ - lib/qemu-toolkit/backend/illumos.rb
42
+ - lib/qemu-toolkit/config.rb
43
+ - lib/qemu-toolkit/dsl.rb
44
+ - lib/qemu-toolkit/iscsi_target.rb
45
+ - lib/qemu-toolkit/local_disk_set.rb
46
+ - lib/qemu-toolkit/storadm.rb
47
+ - lib/qemu-toolkit/vm.rb
48
+ - lib/qemu-toolkit/vm_storage.rb
49
+ - lib/qemu-toolkit/vmadm.rb
50
+ - lib/qemu-toolkit/vnic.rb
51
+ - lib/qemu-toolkit.rb
52
+ - bin/storadm
53
+ - bin/vmadm
54
+ - bin/vmconnect
55
+ homepage:
56
+ licenses: []
57
+ post_install_message:
58
+ rdoc_options:
59
+ - --main
60
+ - README
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ! '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ requirements: []
76
+ rubyforge_project:
77
+ rubygems_version: 1.8.24
78
+ signing_key:
79
+ specification_version: 3
80
+ summary: Manages QEMU kvm virtual machines on Illumos hosts.
81
+ test_files: []