vmit 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,23 @@
1
+ #
2
+ # Copyright (C) 2013 Duncan Mac-Vicar P. <dmacvicar@suse.de>
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of
5
+ # this software and associated documentation files (the "Software"), to deal in
6
+ # the Software without restriction, including without limitation the rights to
7
+ # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8
+ # the Software, and to permit persons to whom the Software is furnished to do so,
9
+ # subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in all
12
+ # copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
+ # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18
+ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+ #
21
+ module Vmit
22
+ VERSION = "0.0.3"
23
+ end
data/lib/vmit/vfs.rb ADDED
@@ -0,0 +1,210 @@
1
+ #
2
+ # Copyright (C) 2013 Duncan Mac-Vicar P. <dmacvicar@suse.de>
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of
5
+ # this software and associated documentation files (the "Software"), to deal in
6
+ # the Software without restriction, including without limitation the rights to
7
+ # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8
+ # the Software, and to permit persons to whom the Software is furnished to do so,
9
+ # subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in all
12
+ # copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
+ # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18
+ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+ #
21
+ require 'abstract_method'
22
+ require 'cheetah'
23
+ require 'uri'
24
+ require 'open-uri'
25
+ require 'progressbar'
26
+ require 'tempfile'
27
+
28
+ module Vmit
29
+
30
+ module VFS
31
+
32
+ # Opens a location
33
+ def self.from(location, *rest, &block)
34
+ [ISO, URI, Local].each do |handler|
35
+ if handler.accept?(location)
36
+ return handler.from(location)
37
+ end
38
+ end
39
+ raise ArgumentError.new("#{location} not supported")
40
+ end
41
+
42
+ class Handler
43
+ # alias for new
44
+ def self.from(*args)
45
+ self.new(*args)
46
+ end
47
+ end
48
+
49
+ class URI < Handler
50
+
51
+ # Whether this handler accepts the
52
+ # given location
53
+ #
54
+ # @param uri [URI,String] location
55
+ def self.accept?(location)
56
+ uri = case location
57
+ when ::URI then location
58
+ else ::URI.parse(location.to_s)
59
+ end
60
+ ['http', 'ftp'].include?(uri.scheme)
61
+ end
62
+
63
+ # @param [String] base_url Base location
64
+ def initialize(location)
65
+ @base_uri = case location
66
+ when ::URI then location
67
+ else ::URI.parse(location.to_s)
68
+ end
69
+ end
70
+
71
+ def self.open(loc, *rest, &block)
72
+ uri = case loc
73
+ when ::URI then loc
74
+ else ::URI.parse(loc.to_s)
75
+ end
76
+ unless accept?(uri)
77
+ raise ArgumentError.new('Only HTTP/FTP supported')
78
+ end
79
+ @pbar = nil
80
+ @filename = File.basename(uri.path)
81
+ ret = OpenURI.open_uri(uri.to_s,
82
+ :content_length_proc => lambda { |t|
83
+ if t && 0 < t
84
+ @pbar = ProgressBar.new(@filename, t)
85
+ @pbar.file_transfer_mode
86
+ end
87
+ },
88
+ :progress_proc => lambda { |s|
89
+ @pbar.set s if @pbar
90
+ }, &block)
91
+ @pbar = nil
92
+ # So that the progress bar line get overwriten
93
+ STDOUT.print "\r"
94
+ STDOUT.flush
95
+ ret
96
+ end
97
+ # Open a filename relative to the
98
+ # base location.
99
+ #
100
+ # @param [String] loc Location to open.
101
+ # If a base URI was given for HTTP then
102
+ # the path will be relative to that
103
+ def open(loc, *rest, &block)
104
+ uri = @base_uri.clone
105
+ uri.path = File.join(uri.path, loc.to_s)
106
+
107
+ Vmit::VFS::URI.open(uri, *rest, &block)
108
+ end
109
+ end
110
+
111
+ class ISO < Handler
112
+
113
+ attr_reader :iso_file
114
+
115
+ # Whether this handler accepts the
116
+ # given location.
117
+ #
118
+ # @param uri [URI,String] location
119
+ def self.accept?(location)
120
+ uri = case location
121
+ when ::URI then location
122
+ else ::URI.parse(location)
123
+ end
124
+
125
+ # either an iso:// url or a local file
126
+ unless (uri.scheme == 'iso' || uri.scheme.nil?)
127
+ return false
128
+ end
129
+ return false unless File.exist?(uri.path)
130
+ File.open(uri.path) do |f|
131
+ f.seek(0x8001)
132
+ return true if f.read(5) == 'CD001'
133
+ end
134
+ false
135
+ end
136
+
137
+ # Creates a ISO handler for +iso+
138
+ # @param [URI, String] iso ISO file
139
+ def initialize(location, *rest)
140
+ raise ArgumentError.new(location) unless self.class.accept?(location)
141
+ path = case location
142
+ when ::URI then location.path
143
+ else ::URI.parse(location).path
144
+ end
145
+ @iso_file = path
146
+ end
147
+
148
+ # Takes a iso URI on the form
149
+ # iso:///path/tothe/file.iso?path=/file/to/get
150
+ #
151
+ # @param [URI, String] uri ISO file and path as query string
152
+ def self.open(location)
153
+ uri = case location
154
+ when ::URI then location
155
+ else ::URI.parse(location)
156
+ end
157
+ handler = self.new(uri)
158
+ query = Hash[*uri.query.split('&').map {|p| p.split('=')}.flatten]
159
+ unless query.has_key?("path")
160
+ raise ArgumentError.new("#{uri}: missing path in query string")
161
+ end
162
+ handler.open(query["path"])
163
+ end
164
+
165
+ # Takes a path relative to +iso_file+
166
+ # @see iso_file
167
+ def open(name, *rest)
168
+ index = Cheetah.run('isoinfo', '-f', '-R', '-i', iso_file, :stdout => :capture)
169
+ files = index.each_line.to_a.map(&:strip)
170
+ raise Errno::ENOENT.new(name) if not files.include?(name)
171
+ tmp = Tempfile.new('vmit-vfs-iso-')
172
+ Cheetah.run('isoinfo', '-R', '-i', iso_file, '-x', name, :stdout => tmp)
173
+ tmp.close
174
+ tmp.open
175
+ if block_given?
176
+ yield tmp
177
+ end
178
+ tmp
179
+ end
180
+ end
181
+
182
+ class Local < Handler
183
+
184
+ # Whether this handler accepts the
185
+ # given location
186
+ #
187
+ # @param uri [URI,String] location
188
+ def self.accept?(location)
189
+ File.directory?(location.to_s)
190
+ end
191
+
192
+ def initialize(base_path=nil)
193
+ @base_path = base_path
194
+ @base_path ||= '/'
195
+ unless File.exist?(@base_path)
196
+ raise Errno::ENOENT.new(@base_path)
197
+ end
198
+ end
199
+
200
+ def self.open(dir, *rest, &block)
201
+ self.new(dir).open(name, *rest, &block)
202
+ end
203
+
204
+ def open(name, *rest, &block)
205
+ Kernel.open(File.join(@base_path, name), *rest, &block)
206
+ end
207
+ end
208
+
209
+ end
210
+ end
@@ -0,0 +1,299 @@
1
+ #
2
+ # Copyright (C) 2013 Duncan Mac-Vicar P. <dmacvicar@suse.de>
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of
5
+ # this software and associated documentation files (the "Software"), to deal in
6
+ # the Software without restriction, including without limitation the rights to
7
+ # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8
+ # the Software, and to permit persons to whom the Software is furnished to do so,
9
+ # subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in all
12
+ # copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
+ # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18
+ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+ #
21
+ require 'cheetah'
22
+ require 'drb'
23
+ require 'fileutils'
24
+ require 'stringio'
25
+ require 'yaml'
26
+
27
+ require 'vmit/utils'
28
+
29
+ module Vmit
30
+
31
+ class VirtualMachine
32
+
33
+ attr_accessor :work_dir
34
+
35
+ VM_DEFAULTS = {
36
+ :memory => '1G',
37
+ }
38
+ SWITCH = 'br0'
39
+
40
+ # Accessor to current options
41
+ def [](key)
42
+ @opts[key]
43
+ end
44
+
45
+ def config_file
46
+ File.join(work_dir, 'config.yml')
47
+ end
48
+
49
+ def initialize(work_dir)
50
+ @pidfile = PidFile.new(:piddir => work_dir, :pidfile => "vmit.pid")
51
+ @work_dir = work_dir
52
+
53
+ @opts = {}
54
+ @opts.merge!(VM_DEFAULTS)
55
+
56
+ if File.exist?(config_file)
57
+ @opts.merge!(YAML::load(File.open(config_file)))
58
+ end
59
+
60
+ # By default the following keys are useful to be
61
+ # generated if they don't exist and then use the
62
+ # same in the future UNLESS they are
63
+ # overriden with vmit run
64
+ if not @opts.has_key?(:mac_address)
65
+ @opts[:mac_address] = Vmit::Utils.random_mac_address
66
+ end
67
+
68
+ if not @opts.has_key?(:uuid)
69
+ @opts[:uuid] = File.read("/proc/sys/kernel/random/uuid").strip
70
+ end
71
+
72
+ @network = if @opts.has_key?(:network)
73
+ Network.create(@opts[:network])
74
+ else
75
+ Vmit.logger.info 'No network selected. Using default.'
76
+ Network.default
77
+ end
78
+ Vmit.logger.info "Network: #{@network}"
79
+ end
80
+
81
+ # @return [Array,<String>] sorted list of snapshots
82
+ def disk_images
83
+ Dir.glob(File.join(work_dir, '*.qcow2')).sort do |a,b|
84
+ File.ctime(a) <=> File.ctime(b)
85
+ end
86
+ end
87
+
88
+ # Takes a disk snapshot
89
+ def disk_snapshot!
90
+ disk_image_shift!
91
+ end
92
+
93
+ def disk_image_init!(opts={})
94
+ disk_image_shift!(opts)
95
+ end
96
+
97
+ DISK_INIT_DEFAULTS = {:disk_size => '10G'}
98
+
99
+ # Shifts an image, adding a new one using the
100
+ # previous newest one as backing file
101
+ #
102
+ # @param [Hash] opts options for the disk shift
103
+ # @option opts [String] :disk_size Disk size. Only used for image creation
104
+ def disk_image_shift!(opts={})
105
+ runtime_opts = DISK_INIT_DEFAULTS.merge(opts)
106
+
107
+ file_name = File.join(work_dir, "sda-#{Time.now.to_i}.qcow2")
108
+ images = disk_images
109
+
110
+ file_name = 'base.qcow2' if images.size == 0
111
+
112
+ args = ['/usr/bin/qemu-img', 'create',
113
+ '-f', "qcow2"]
114
+
115
+ if not images.empty?
116
+ args << '-b'
117
+ args << images.last
118
+ end
119
+ args << file_name
120
+ if images.empty?
121
+ args << runtime_opts[:disk_size]
122
+ end
123
+
124
+ Vmit.logger.info "Shifted image. Current is '#{file_name}'."
125
+ Cheetah.run(*args)
126
+ end
127
+
128
+ # Rolls back to the previous snapshot
129
+ def disk_rollback!
130
+ images = disk_images
131
+
132
+ return if images.empty?
133
+
134
+ if images.size == 1
135
+ Vmit.logger.fatal "Only the base snapshot left!"
136
+ return
137
+ end
138
+ Vmit.logger.info "Removing #{images.last}"
139
+ FileUtils.rm(images.last)
140
+ end
141
+
142
+ # @returns [String] The latest COW snapshot
143
+ def current_image
144
+ curr = disk_images.last
145
+ raise "No hard disk image available" if curr.nil?
146
+ curr
147
+ end
148
+
149
+ def options
150
+ @opts
151
+ end
152
+
153
+ # @return [Hash] Config of the virtual machine
154
+ # This is all options plus the defaults
155
+ def config
156
+ VM_DEFAULTS.merge(@opts)
157
+ end
158
+
159
+ # @return [Hash] config that differs from default
160
+ # and therefore relevant to be persisted in config.yml
161
+ def relevant_config
162
+ config.diff(VM_DEFAULTS)
163
+ end
164
+
165
+ # Saves the configuration in config.yml
166
+ def save_config!
167
+ if not relevant_config.empty?
168
+ Vmit.logger.info "Writing config.yml..."
169
+ File.open(config_file, 'w') do |f|
170
+ f.write(relevant_config.to_yaml)
171
+ end
172
+ end
173
+ end
174
+
175
+ def to_s
176
+ config.to_s
177
+ end
178
+
179
+ BINDIR = File.join(File.dirname(__FILE__), '../../bin')
180
+
181
+ # Starts the virtual machine
182
+ #
183
+ # @param [Hash] runtime_opts Runtime options
184
+ # @option runtime_opts [String] :cdrom CDROM image
185
+ # @option runtime_opts [String] :kernel Kernel image
186
+ # @option runtime_opts [String] :initrd initrd image
187
+ # @option runtime_opts [String] :append Kernel command line
188
+ # @option runtime_opts [String] :floppy Floppy (image or directory)
189
+ def run(runtime_opts)
190
+ Vmit.logger.info "Starting VM..."
191
+ # Don't overwrite @opts so that
192
+ # run can be called various times
193
+ opts = {}
194
+ opts.merge!(@opts)
195
+ opts.merge!(runtime_opts)
196
+
197
+ config.each do |k,v|
198
+ Vmit.logger.info " => #{k} : #{v}"
199
+ end
200
+
201
+ begin
202
+ # HACK, will be replaced by a better config system
203
+ use_virtio = ! File.exist?(File.join(work_dir, '.disable-virtio'))
204
+
205
+ ifup = File.expand_path(File.join(BINDIR, 'vmit-ifup'))
206
+ ifdown = File.expand_path(File.join(BINDIR, 'vmit-ifdown'))
207
+
208
+ args = ['/usr/bin/qemu-kvm', '-boot', 'c',
209
+ '-m', "#{opts[:memory]}",
210
+ '-pidfile', File.join(work_dir, 'qemu.pid')]
211
+
212
+ if use_virtio
213
+ args << '-drive'
214
+ args << "file=#{current_image},if=virtio"
215
+
216
+ args << '-netdev'
217
+ args << "type=tap,script=#{ifup},downscript=#{ifdown},id=vnet0"
218
+ args << '-device'
219
+ args << "virtio-net-pci,netdev=vnet0,mac=#{opts[:mac_address]}"
220
+ else
221
+ args << '-drive'
222
+ args << "file=#{current_image}"
223
+
224
+ args << '-net'
225
+ args << "nic,macaddr=#{opts[:mac_address]}"
226
+ args << '-net'
227
+ args << "tap,script=#{ifup},downscript=#{ifdown}"
228
+ end
229
+
230
+ # advanced options, mostly to be used by plugins
231
+ [:cdrom, :kernel, :initrd, :append].each do |key|
232
+ if opts.has_key?(key)
233
+ args << "-#{key}"
234
+ args << case opts[key]
235
+ # append is multple
236
+ when Array then opts[key].join(' ')
237
+ else opts[key]
238
+ end
239
+ end
240
+ end
241
+
242
+ if opts.has_key?(:floppy)
243
+ if File.directory?(opts[:floppy])
244
+ args << '-fda'
245
+ args << "fat:floppy:#{opts[:floppy]}"
246
+ else
247
+ Vmit.logger.warn "#{opts[:floppy]} : only directories supported"
248
+ end
249
+ end
250
+
251
+ # options that translate to
252
+ # -no-something if :something => false
253
+ [:reboot].each do |key|
254
+ if opts.has_key?(key)
255
+ # default is true
256
+ args << "-no-#{key}" if not opts[key]
257
+ end
258
+ end
259
+
260
+ unless ENV['DISABLE_UUID']
261
+ args << '-uuid'
262
+ args << "#{opts[:uuid]}"
263
+ end
264
+
265
+ DRb.start_service nil, self
266
+ ENV['VMIT_SERVER'] = DRb.uri
267
+
268
+ ENV['VMIT_SWITCH'] = SWITCH
269
+ Vmit.logger.debug "Vmit server listening at #{DRb.uri}"
270
+
271
+ @network.auto do
272
+ begin
273
+ Cheetah.run(*args)
274
+ ensure
275
+ FileUtils.rm_f File.join(work_dir, 'qemu.pid')
276
+ end
277
+ end
278
+ rescue PidFile::DuplicateProcessError => e
279
+ Vmit.logger.fatal "VM in '#{work_dir}'' is already running (#{e})"
280
+ raise
281
+ end
282
+ end
283
+
284
+ # Called by vmit-ifup
285
+ def ifup(device)
286
+ Vmit.logger.info " Bringing interface #{device} up"
287
+ Cheetah.run '/sbin/ifconfig', device, '0.0.0.0', 'up'
288
+ @network.connect_interface(device)
289
+ end
290
+
291
+ # Called by vmit-ifdown
292
+ def ifdown(device)
293
+ Vmit.logger.info " Bringing down interface #{device}"
294
+ Cheetah.run '/sbin/ifconfig', device, '0.0.0.0', 'down'
295
+ @network.disconnect_interface(device)
296
+ end
297
+ end
298
+
299
+ end