vmit 0.0.3 → 0.0.3.99

Sign up to get free protection for your applications and to get access to all the features.
@@ -18,17 +18,53 @@
18
18
  # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
19
  # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
20
  #
21
- require 'vmit'
22
21
  require 'erb'
22
+ require 'vmit/unattended_install'
23
+ require 'vmit/vfs'
23
24
 
24
25
  module Vmit
25
26
 
26
- class Kickstart
27
+ class Kickstart < UnattendedInstall
27
28
 
28
- attr_accessor :install
29
+ def initialize(location)
30
+ super(location)
29
31
 
30
- # ks=floppy
31
- def initialize
32
+ media = Vmit::VFS.from(location)
33
+ case media
34
+ when Vmit::VFS::URI
35
+ @install = location
36
+ when Vmit::VFS::ISO
37
+ @install = :cdrom
38
+ vm.config.configure(:cdrom => location.to_s)
39
+ else raise ArgumentError.new("Unsupported autoinstallation: #{location}")
40
+ end
41
+ end
42
+
43
+ def execute_autoinstall(vm, args)
44
+ vm.config.push!
45
+ begin
46
+ vm.config.configure(args)
47
+ if @install == :cdrom
48
+ vm.config.configure(:cdrom => location.to_s)
49
+ end
50
+
51
+ Dir.mktmpdir do |floppy_dir|
52
+ FileUtils.chmod_R 0755, floppy_dir
53
+ vm.config.floppy = floppy_dir
54
+ vm.config.add_kernel_cmdline!('ks=floppy')
55
+ vm.config.add_kernel_cmdline!("repo=#{@install}")
56
+ vm.config.reboot = false
57
+
58
+ File.write(File.join(floppy_dir, 'ks.cfg'), to_ks_script)
59
+ Vmit.logger.info "Kickstart: 1st stage."
60
+ vm.up
61
+ vm.wait_until_shutdown! do
62
+ vm.vnc
63
+ end
64
+ end
65
+ ensure
66
+ vm.config.pop!
67
+ end
32
68
  end
33
69
 
34
70
  def to_ks_script
@@ -41,10 +77,10 @@ keyboard us
41
77
  timezone --utc America/New_York
42
78
  bootloader --location=mbr --driveorder=sda --append="rhgb quiet"
43
79
  install
44
- <% if install.is_a?(String) || install.is_a?(::URI)%>
45
- url --url=<%= install.to_s.strip %>
80
+ <% if @install.is_a?(String) || @install.is_a?(::URI)%>
81
+ url --url=<%= @install.to_s.strip %>
46
82
  <% else %>
47
- <%= install %>
83
+ <%= @install %>
48
84
  <% end %>
49
85
  network --device eth0 --bootproto dhcp
50
86
  zerombr yes
@@ -0,0 +1,274 @@
1
+ require 'confstruct'
2
+ require 'libvirt'
3
+ require 'nokogiri'
4
+ require 'vmit/workspace'
5
+
6
+ module Vmit
7
+
8
+ class LibvirtVM
9
+
10
+ attr_reader :workspace
11
+ attr_reader :config
12
+ attr_reader :conn
13
+
14
+ def self.from_pwd(opts={})
15
+ workspace = Vmit::Workspace.from_pwd
16
+ LibvirtVM.new(workspace, opts)
17
+ end
18
+
19
+ # @param [Vmit::Workspace] workspace to run
20
+ # @param [Hash] runtime options that override
21
+ # the virtual machine options
22
+ def initialize(workspace, opts={})
23
+ @workspace = workspace
24
+ @config = Confstruct::Configuration.new(@workspace.config)
25
+ @config.configure(opts)
26
+
27
+ @conn = ::Libvirt::open("qemu:///system")
28
+ if not @conn
29
+ raise 'Can\'t initialize hypervisor'
30
+ end
31
+ end
32
+
33
+ # @return [Libvirt::Domain] returns the libvirt domain
34
+ # or creates one for the workspace if it does not exist
35
+ def domain
36
+ conn.lookup_domain_by_name(workspace.name) rescue nil
37
+ end
38
+
39
+ def up
40
+ unless down?
41
+ Vmit.logger.error "#{workspace.name} is already up. Run 'vmit ssh' or 'vmit vnc' to access it."
42
+ return
43
+ end
44
+
45
+ Vmit.logger.debug "\n#{conn.capabilities}"
46
+ Vmit.logger.info "Starting VM..."
47
+
48
+ network = conn.lookup_network_by_name('default')
49
+ Vmit.logger.debug "\n#{network.xml_desc}"
50
+ if not network.active?
51
+ network.create
52
+ end
53
+ Vmit.logger.debug "\n#{self.to_libvirt_xml}"
54
+
55
+ puts domain.inspect
56
+ domain.destroy if domain
57
+ if domain.nil?
58
+ conn.create_domain_xml(self.to_libvirt_xml)
59
+ end
60
+ end
61
+
62
+ def state
63
+ assert_up
64
+ st, reason = domain.state
65
+ st_sym = case st
66
+ when Libvirt::Domain::NOSTATE then :unknown
67
+ when Libvirt::Domain::RUNNING then :running
68
+ when Libvirt::Domain::BLOCKED then :blocked
69
+ when Libvirt::Domain::PAUSED then :paused
70
+ when Libvirt::Domain::SHUTDOWN then :shutdown
71
+ when Libvirt::Domain::SHUTOFF then :shutoff
72
+ when Libvirt::Domain::CRASHED then :crashed
73
+ when Libvirt::Domain::PMSUSPENDED then :pmsuspended
74
+ end
75
+ return st_sym, reason
76
+ end
77
+
78
+ def up?
79
+ !domain.nil? && domain.active?
80
+ end
81
+
82
+ def assert_up
83
+ unless up?
84
+ raise "VM is not running. Try 'vmit up'..."
85
+ end
86
+ end
87
+
88
+ def assert_down
89
+ if up?
90
+ raise "VM is running. Try 'vmit down'..."
91
+ end
92
+ end
93
+
94
+ def down?
95
+ !up?
96
+ end
97
+
98
+ def reboot
99
+ assert_up
100
+ domain.reboot
101
+ end
102
+
103
+ def shutdown
104
+ assert_up
105
+ domain.shutdown
106
+ end
107
+
108
+ def destroy
109
+ if domain
110
+ domain.destroy
111
+ end
112
+ end
113
+
114
+ # Waits until the machine is shutdown
115
+ # executing the passed block.
116
+ #
117
+ # If the machine is shutdown, the
118
+ # block will be killed. If the block
119
+ # exits, the machine will be stopped
120
+ # immediately (domain destroyed)
121
+ #
122
+ # @example
123
+ # vm.wait_until_shutdown! do
124
+ # vm.vnc
125
+ # end
126
+ #
127
+ def wait_until_shutdown!(&block)
128
+ chars = %w{ | / - \\ }
129
+ thread = Thread.new(&block)
130
+ thread.abort_on_exception = true
131
+
132
+ Vmit.logger.info "Waiting for machine..."
133
+ while true
134
+ print chars[0]
135
+
136
+ if down?
137
+ Thread.kill(thread)
138
+ return
139
+ end
140
+ if not thread.alive?
141
+ domain.destroy
142
+ end
143
+ sleep(1)
144
+ print "\b"
145
+ chars.push chars.shift
146
+ end
147
+
148
+ end
149
+
150
+ def ip_address
151
+ File.open('/var/lib/libvirt/dnsmasq/default.leases') do |f|
152
+ f.each_line do |line|
153
+ parts = line.split(' ')
154
+ if parts[1] == config.mac_address
155
+ return parts[2]
156
+ end
157
+ end
158
+ end
159
+ nil
160
+ end
161
+
162
+ def spice_address
163
+ assert_up
164
+ doc = Nokogiri::XML(domain.xml_desc)
165
+ port = doc.xpath("//graphics[@type='spice']/@port")
166
+ listen = doc.xpath("//graphics[@type='spice']/listen[@type='address']/@address")
167
+ return listen, port
168
+ end
169
+
170
+ def vnc_address
171
+ assert_up
172
+ doc = Nokogiri::XML(domain.xml_desc)
173
+ port = doc.xpath("//graphics[@type='vnc']/@port")
174
+ listen = doc.xpath("//graphics[@type='vnc']/listen[@type='address']/@address")
175
+ "#{listen}:#{port}"
176
+ end
177
+
178
+ # synchronus spice viewer
179
+ def spice
180
+ assert_up
181
+ addr, port = spice_address
182
+ raise "Can't get the SPICE information from the VM" unless addr
183
+ system("spicec --host #{addr} --port #{port}")
184
+ end
185
+
186
+ # synchronus vnc viewer
187
+ def vnc
188
+ assert_up
189
+ addr = vnc_address
190
+ raise "Can't get the VNC information from the VM" unless addr
191
+ system("vncviewer #{addr}")
192
+ end
193
+
194
+ def [](key)
195
+ if @runtime_opts.has_key?(key)
196
+ @runtime_opts[key]
197
+ else
198
+ workspace[key]
199
+ end
200
+ end
201
+
202
+ def to_libvirt_xml
203
+ builder = Nokogiri::XML::Builder.new do |xml|
204
+ xml.domain(:type => 'kvm') {
205
+ xml.name workspace.name
206
+ xml.uuid config.uuid
207
+ match = /([0-9+])([^0-9+])/.match config.memory
208
+ xml.memory(match[1], :unit => match[2])
209
+ xml.vcpu 1
210
+ xml.os {
211
+ xml.type('hvm', :arch => 'x86_64')
212
+ if config.lookup!('kernel')
213
+ xml.kernel config.kernel
214
+ if config.lookup!('kernel_cmdline')
215
+ xml.cmdline config.kernel_cmdline.join(' ')
216
+ end
217
+ end
218
+ xml.initrd config.initrd if config.lookup!('initrd')
219
+ xml.boot(:dev => 'cdrom') if config.lookup!('cdrom')
220
+ }
221
+ # for shutdown to work
222
+ xml.features {
223
+ xml.acpi
224
+ }
225
+ #xml.on_poweroff 'destroy'
226
+ unless config.lookup!('reboot').nil? || config.lookup!('reboot')
227
+ xml.on_reboot 'destroy'
228
+ end
229
+ #xml.on_crash 'destroy'
230
+ #xml.on_lockfailure 'poweroff'
231
+
232
+ xml.devices {
233
+ xml.emulator '/usr/bin/qemu-kvm'
234
+ xml.channel(:type => 'spicevmc') do
235
+ xml.target(:type => 'virtio', :name => 'com.redhat.spice.0')
236
+ end
237
+
238
+ xml.disk(:type => 'file', :device => 'disk') {
239
+ xml.driver(:name => 'qemu', :type => 'qcow2')
240
+ xml.source(:file => workspace.current_image)
241
+ if config.virtio
242
+ xml.target(:dev => 'sda', :bus => 'virtio')
243
+ else
244
+ xml.target(:dev => 'sda', :bus => 'ide')
245
+ end
246
+ }
247
+ if config.lookup!('cdrom')
248
+ xml.disk(:type => 'file', :device => 'cdrom') {
249
+ xml.source(:file => config.cdrom)
250
+ xml.target(:dev => 'hdc')
251
+ xml.readonly
252
+ }
253
+ end
254
+ if config.lookup!('floppy')
255
+ xml.disk(:type => 'dir', :device => 'floppy') {
256
+ xml.source(:dir => config.floppy)
257
+ xml.target(:dev => 'fda')
258
+ xml.readonly
259
+ }
260
+ end
261
+ xml.graphics(:type => 'vnc', :autoport => 'yes')
262
+ xml.graphics(:type => 'spice', :autoport => 'yes')
263
+ xml.interface(:type => 'network') {
264
+ xml.source(:network => 'default')
265
+ xml.mac(:address => config.mac_address)
266
+ }
267
+ }
268
+ }
269
+ end
270
+ builder.to_xml
271
+ end
272
+
273
+ end
274
+ end
@@ -18,8 +18,6 @@
18
18
  # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
19
  # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
20
  #
21
- require 'vmit/autoyast'
22
- require 'vmit/kickstart'
23
21
  require 'abstract_method'
24
22
  require 'clamp'
25
23
  require 'net/http'
@@ -46,34 +44,52 @@ module Vmit
46
44
  disk_size
47
45
  end
48
46
 
49
- parameter "LOCATION", "Repository URL or ISO image to bootstrap from"
47
+ option ['-F', '--packages'], "PACKAGES",
48
+ "Add packages. Either a file with one package name per line or a
49
+ comma separated list", :default => [] do |pkgs|
50
+ case
51
+ when File.exist?(pkgs)
52
+ begin
53
+ File.read(pkgs).each_line.to_a.map(&:strip)
54
+ rescue
55
+ raise ArgumentError, "Can't read package list from #{pkgs}"
56
+ end
57
+ else
58
+ list = pkgs.split(',')
59
+ if list.empty?
60
+ raise ArgumentError, "Not a valid comma separated list of packages"
61
+ end
62
+ list
63
+ end
64
+ end
65
+
66
+ parameter "LOCATION ...", "Repository URL, ISO image or distribution name"
50
67
 
51
68
  def execute
52
69
  Vmit.logger.info 'Starting bootstrap'
53
- curr_dir = File.expand_path(Dir.pwd)
54
- vm = Vmit::VirtualMachine.new(curr_dir)
70
+ workspace = Vmit::Workspace.from_pwd
55
71
 
56
72
  Vmit.logger.info ' Deleting old images'
57
73
  FileUtils.rm_f(Dir.glob('*.qcow2'))
58
74
  opts = {}
59
75
  opts[:disk_size] = disk_size if disk_size
60
- vm.disk_image_init!(opts)
61
- vm.save_config!
76
+ workspace.disk_image_init!(opts)
77
+ workspace.save_config!
62
78
 
63
- uri = URI.parse(location)
64
- bootstrap = [Vmit::Bootstrap::FromImage,
65
- Vmit::Bootstrap::FromMedia].find do |method|
66
- method.accept?(uri)
67
- end
79
+ location = location_list.join(' ')
80
+ install_media = Vmit::InstallMedia.scan(location)
81
+
82
+ Vmit.logger.info "Install media: #{install_media}"
68
83
 
69
- if bootstrap
70
- bootstrap.new(vm, uri).execute
71
- else
72
- raise "Can't bootstrap from #{location}"
84
+ packages.each do |pkg|
85
+ install_media.unattended_install.config.add_packages!(pkg)
73
86
  end
74
87
 
88
+ vm = Vmit::LibvirtVM.from_pwd
89
+ install_media.autoinstall(vm)
90
+
75
91
  Vmit.logger.info 'Creating snapshot of fresh system.'
76
- vm.disk_snapshot!
92
+ workspace.disk_snapshot!
77
93
  Vmit.logger.info 'Bootstraping done. Call vmit run to start your system.'
78
94
  end
79
95
 
@@ -0,0 +1,208 @@
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 'yaml'
22
+
23
+ module Vmit
24
+
25
+ class Registry
26
+ include Enumerable
27
+ end
28
+
29
+ # Wraps a registry providing buffered
30
+ # semantics. All writes are buffered
31
+ # until +save!+ is called.
32
+ #
33
+ # Changes in the wrapped registry for
34
+ # already read keys are not reflected
35
+ # until +reload!+ is called.
36
+ #
37
+ # reg = ufferedRegistry.new(backing_registry)
38
+ # reg[:key1] = "value" # backing_registry not changed
39
+ # reg.save! # backing_registry changed
40
+ #
41
+ class BufferedRegistry < Registry
42
+
43
+ def initialize(registry)
44
+ @buffer = Hash.new
45
+ @registry = registry
46
+ end
47
+
48
+ def [](key)
49
+ if not @buffer.has_key?(key)
50
+ @buffer[key] = @registry[key]
51
+ end
52
+ @buffer[key]
53
+ end
54
+
55
+ def []=(key, val)
56
+ @buffer[key] = val
57
+ end
58
+
59
+ def save!
60
+ @buffer.each do |key, val|
61
+ @registry[key] = val
62
+ end
63
+ end
64
+
65
+ def reload!
66
+ @buffer.keys.each do |key|
67
+ @buffer[key] = @registry[key]
68
+ end
69
+ end
70
+
71
+ end
72
+
73
+ # Add types to keys
74
+ # You need to inherit from this class
75
+ #
76
+ # @example
77
+ # class MyTypes < TypedRegistry
78
+ # type :key1, Fixnum
79
+ # type :key2, Float
80
+ # end
81
+ #
82
+ # reg = MyTypes.new(backing_registry)
83
+ #
84
+ class TypedRegistry < Registry
85
+
86
+ class << self
87
+ def type(key, t=nil)
88
+ @type_info ||= Hash.new
89
+ @type_info[key] = t unless t.nil?
90
+ @type_info[key]
91
+ end
92
+ end
93
+
94
+ def initialize(registry)
95
+ @registry = registry
96
+ end
97
+
98
+ def type(key)
99
+ self.class.type(key)
100
+ end
101
+
102
+ def [](key)
103
+ rawval = @registry[key]
104
+ case type(key).to_s
105
+ when 'String' then rawval.to_s
106
+ when 'Fixnum' then rawval.to_i
107
+ when 'Float' then rawval.to_f
108
+ else rawval
109
+ end
110
+ end
111
+
112
+ def []=(key, val)
113
+ if type(key)
114
+ unless val.is_a?(type(key))
115
+ raise TypeError.new("Expected #{type(key)} for #{key}")
116
+ end
117
+ end
118
+ @registry[key] = val
119
+ end
120
+ end
121
+
122
+ # Takes configuration options from a yml
123
+ # file.
124
+ class YamlRegistry < Registry
125
+ def initialize(file_path)
126
+ @file_path = file_path
127
+ reload!
128
+ end
129
+
130
+ def reload!
131
+ @data = YAML::load(File.read(@file_path))
132
+ end
133
+
134
+ def save!
135
+ File.write(@file_path, @data.to_yaml)
136
+ end
137
+
138
+ def [](key)
139
+ # YAML uses strings for keys
140
+ # we use symbols.
141
+ if @data.has_key?(key)
142
+ @data[key]
143
+ else
144
+ @data[key.to_s]
145
+ end
146
+ end
147
+
148
+ def []=(key, val)
149
+ @data[key.to_s] = val
150
+ save!
151
+ reload!
152
+ end
153
+
154
+ def each(&block)
155
+ Enumerator.new do |enum|
156
+ @data.each do |key, val|
157
+ enum.yield key.to_sym, val
158
+ end
159
+ end
160
+ end
161
+
162
+ def keys
163
+ each.to_a.map(&:first)
164
+ end
165
+ end
166
+
167
+ # Takes configuration options from a
168
+ # filesystem tree where the files are
169
+ # the keys and the content the values
170
+ class FilesystemRegistry < Registry
171
+ def initialize(base_path)
172
+ @base_path = base_path
173
+ end
174
+
175
+ def [](key)
176
+ begin
177
+ path = File.join(@base_path, key.to_s)
178
+ unless File.directory?(path)
179
+ File.read(path)
180
+ else
181
+ return FilesystemRegistry.new(File.join(@base_path, path))
182
+ end
183
+ rescue Errno::ENOENT
184
+ nil
185
+ end
186
+ end
187
+
188
+ def []=(key, val)
189
+ File.write(File.join(@base_path, key.to_s), val)
190
+ end
191
+
192
+ def each(&block)
193
+ Enumerator.new do |enum|
194
+ Dir.entries(@base_path).reject do |elem|
195
+ ['.', '..'].include?(elem)
196
+ end.each do |key|
197
+ enum.yield key.to_sym, self[key]
198
+ end
199
+ end
200
+ end
201
+
202
+ def keys
203
+ each.to_a.map(&:first)
204
+ end
205
+
206
+ end
207
+
208
+ end