vmit 0.0.3 → 0.0.3.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/bin/vmit CHANGED
@@ -41,14 +41,14 @@ module Vmit
41
41
 
42
42
  def execute
43
43
  options = {}
44
- vm = Vmit::VirtualMachine.new(File.expand_path(Dir.pwd))
44
+ vm = Vmit::Workspace.new(File.expand_path(Dir.pwd))
45
45
 
46
46
  OPTION_KEYS.each do |k|
47
47
  val = self.send(k)
48
48
  options[k] = val if val
49
49
  end
50
50
 
51
- vm.options.merge!(options)
51
+ vm.config.configure(options)
52
52
  vm.save_config!
53
53
 
54
54
  vm.disk_image_init!(options)
@@ -57,7 +57,7 @@ module Vmit
57
57
 
58
58
  end
59
59
 
60
- class RunCommand < BaseVmCommand
60
+ class UpCommand < BaseVmCommand
61
61
 
62
62
  OPTION_KEYS = [:memory, :cdrom]
63
63
 
@@ -70,23 +70,89 @@ module Vmit
70
70
 
71
71
  def execute
72
72
  options = {}
73
- vm = Vmit::VirtualMachine.new(File.expand_path(Dir.pwd))
73
+ workspace = Vmit::Workspace.new(File.expand_path(Dir.pwd))
74
74
 
75
75
  OPTION_KEYS.each do |k|
76
76
  val = self.send(k)
77
77
  options[k] = val if val
78
78
  end
79
79
 
80
- vm.run(options)
80
+ vm = Vmit::LibvirtVM.new(workspace, options)
81
+ vm.up
81
82
  end
82
83
 
83
84
  end
84
85
 
86
+ class SpiceCommand < Clamp::Command
87
+ def execute
88
+ vm = LibvirtVM.from_pwd
89
+ vm.spice
90
+ end
91
+ end
92
+
93
+ class VncCommand < Clamp::Command
94
+ def execute
95
+ vm = LibvirtVM.from_pwd
96
+ vm.vnc
97
+ end
98
+ end
99
+
100
+ class SshCommand < Clamp::Command
101
+ def execute
102
+ vm = LibvirtVM.from_pwd
103
+ vm.assert_up
104
+
105
+ while not vm.ip_address
106
+ sleep(1)
107
+ Vmit.logger.info("Waiting for ip address...")
108
+ end
109
+ Vmit.logger.info("ip address is #{vm.ip_address}")
110
+ Vmit::Utils.wait_for_port(vm.ip_address, 22)
111
+ system("ssh root@#{vm.ip_address}")
112
+ end
113
+ end
114
+
115
+ class StatusCommand < Clamp::Command
116
+ def execute
117
+ vm = LibvirtVM.from_pwd
118
+ st, reason = vm.state
119
+
120
+ puts "VM is #{st}...(#{reason})"
121
+ if vm.ip_address
122
+ puts " #{vm.ip_address}"
123
+ end
124
+
125
+ end
126
+ end
127
+
128
+ class RebootCommand < Clamp::Command
129
+ def execute
130
+ vm = LibvirtVM.from_pwd
131
+ vm.reboot
132
+ end
133
+ end
134
+
135
+ class ShutdownCommand < Clamp::Command
136
+ def execute
137
+ vm = LibvirtVM.from_pwd
138
+ vm.shutdown
139
+ end
140
+ end
141
+
142
+ class DestroyCommand < Clamp::Command
143
+ def execute
144
+ vm = LibvirtVM.from_pwd
145
+ vm.destroy
146
+ end
147
+ end
148
+
85
149
  class DiskSnapshotCommand < Clamp::Command
86
150
 
87
151
  def execute
88
- vm = Vmit::VirtualMachine.new(File.expand_path(Dir.pwd))
89
- vm.disk_snapshot!
152
+ vm = LibvirtVM.from_pwd
153
+ vm.assert_down
154
+
155
+ vm.workspace.disk_snapshot!
90
156
  end
91
157
 
92
158
  end
@@ -94,8 +160,8 @@ module Vmit
94
160
  class DiskImagesCommand < Clamp::Command
95
161
 
96
162
  def execute
97
- vm = Vmit::VirtualMachine.new(File.expand_path(Dir.pwd))
98
- puts vm.disk_images.last
163
+ vm = LibvirtVM.from_pwd
164
+ puts vm.workspace.disk_images.last
99
165
  end
100
166
 
101
167
  end
@@ -103,19 +169,30 @@ module Vmit
103
169
  class DiskRollbackCommand < Clamp::Command
104
170
 
105
171
  def execute
106
- vm = Vmit::VirtualMachine.new(File.expand_path(Dir.pwd))
107
- vm.disk_rollback!
172
+ vm = LibvirtVM.from_pwd
173
+ vm.assert_down
174
+
175
+ vm.workspace.disk_rollback!
108
176
  end
109
177
 
110
178
  end
111
179
 
180
+ Vmit.load_plugins!
181
+
112
182
  class MainCommand < Clamp::Command
113
183
 
114
184
  subcommand "init", "Initialize the vm", InitCommand
115
- subcommand "run", "Run the vm", RunCommand
185
+ subcommand "up", "Run the vm", UpCommand
186
+ subcommand "status", "Show VM status", StatusCommand
187
+ subcommand "shutdown", "Shutdown the VM", ShutdownCommand
188
+ subcommand "reboot", "Reboot the VM", RebootCommand
189
+ subcommand "destroy", "Destroy the VM", DestroyCommand
116
190
  subcommand "disk-images", "List disk images", DiskImagesCommand
117
191
  subcommand "disk-snapshot", "Creates a new disk-snapshot", DiskSnapshotCommand
118
192
  subcommand "disk-rollback", "Rollbacks to previous disk snapshot", DiskRollbackCommand
193
+ subcommand "spice", "SPICE into the machine", SpiceCommand
194
+ subcommand "ssh", "SSH into the machine", SshCommand
195
+ subcommand "vnc", "VNC into the machine", VncCommand
119
196
 
120
197
  # Add commands offered via plugins
121
198
  Vmit.plugins.each do |plugin|
@@ -127,7 +204,6 @@ module Vmit
127
204
  end
128
205
  end
129
206
 
130
-
131
207
  end
132
208
 
133
209
  begin
@@ -19,13 +19,18 @@
19
19
  # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
20
  #
21
21
  require 'vmit/version'
22
- require 'vmit/bootstrap'
22
+ require 'vmit/unattended_install'
23
23
  require 'vmit/logger'
24
- require 'vmit/refcounted_resource'
25
24
  require 'vmit/network'
26
25
  require 'vmit/vfs'
27
- require 'vmit/virtual_machine'
26
+ require 'vmit/workspace'
27
+ require 'vmit/libvirt_vm'
28
+ require 'vmit/autoyast'
29
+ require 'vmit/kickstart'
30
+ require 'vmit/debian_preseed'
31
+ require 'vmit/install_media'
28
32
  require 'vmit/ext'
33
+ require 'vmit/utils'
29
34
  require 'pidfile'
30
35
 
31
36
  module Vmit
@@ -47,21 +52,23 @@ module Vmit
47
52
  @plugins ||= []
48
53
  end
49
54
 
50
- end
55
+ def self.load_plugins!
56
+ # Scan plugins
57
+ plugin_glob = File.join(File.dirname(__FILE__), 'vmit', 'plugins', '*.rb')
58
+ Dir.glob(plugin_glob).each do |plugin|
59
+ Vmit.logger.debug("Loading file: #{plugin}")
60
+ #puts "Loading file: #{plugin}"
61
+ load plugin
62
+ end
51
63
 
52
- # Scan plugins
53
- plugin_glob = File.join(File.dirname(__FILE__), 'vmit', 'plugins', '*.rb')
54
- Dir.glob(plugin_glob).each do |plugin|
55
- Vmit.logger.debug("Loading file: #{plugin}")
56
- #puts "Loading file: #{plugin}"
57
- load plugin
58
- end
64
+ # instantiate plugins
65
+ ::Vmit::Plugins.constants.each do |cnt|
66
+ pl_class = ::Vmit::Plugins.const_get(cnt)
67
+ #pl_instance = pl_class.new
68
+ Vmit.add_plugin(pl_class)
69
+ Vmit.logger.debug("Loaded: #{pl_class}")
70
+ #puts "Loaded: #{pl_class}"
71
+ end
72
+ end
59
73
 
60
- # instantiate plugins
61
- ::Vmit::Plugins.constants.each do |cnt|
62
- pl_class = ::Vmit::Plugins.const_get(cnt)
63
- #pl_instance = pl_class.new
64
- Vmit.add_plugin(pl_class)
65
- Vmit.logger.debug("Loaded: #{pl_class}")
66
- #puts "Loaded: #{pl_class}"
67
74
  end
@@ -19,38 +19,92 @@
19
19
  # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
20
  #
21
21
  require 'nokogiri'
22
+ require 'vmit/unattended_install'
23
+ require 'vmit/vfs'
22
24
 
23
25
  module Vmit
24
26
 
25
- class AutoYaST
27
+ class AutoYaST < UnattendedInstall
26
28
 
27
- attr_accessor :patterns
28
- attr_accessor :packages
29
-
30
- def initialize
31
- @net_udev = {}
32
- @patterns = []
33
- @packages = []
29
+ def initialize(location)
30
+ super(location)
31
+ base!
34
32
  end
35
33
 
36
- def minimal_opensuse!
37
- @patterns << 'base'
34
+ def base!
35
+ config.add_patterns! 'base'
36
+ config.add_packages! 'zypper'
37
+ config.add_packages! 'openssh'
38
38
  end
39
39
 
40
- def minimal_sle!
41
- @patterns << 'Minimal'
40
+ # minimal installation
41
+ # does not work with openSUSE
42
+ def minimal!
43
+ config.add_patterns! 'Minimal'
44
+ config.add_packages! 'zypper'
45
+ config.add_packages! 'openssh'
42
46
  end
43
47
 
44
- # Map a network device name and make
45
- # it persistent
46
- #
47
- # @param [String] MAC mac address
48
- # @param [String] device name
49
- def name_network_device(mac, name)
50
- if @net_udev.has_key?(mac) or @net_udev.has_value?(mac)
51
- raise "Device with MAC #{mac} is already assigned to #{@net_udev[name]}"
48
+ # @param [Hash] args Arguments for 1st stage
49
+ def execute_autoinstall(vm, args)
50
+ vm.config.push!
51
+ begin
52
+ vm.config.configure(args)
53
+ media = Vmit::VFS.from(location)
54
+ kernel_append_arg = case media
55
+ when Vmit::VFS::URI then "install=#{location}"
56
+ when Vmit::VFS::ISO then 'install=cdrom'
57
+ else raise ArgumentError.new("Unsupported autoinstallation: #{location}")
58
+ end
59
+ vm.config.add_kernel_cmdline!(kernel_append_arg)
60
+
61
+ if media.is_a?(Vmit::VFS::ISO)
62
+ vm.config.cdrom = location.to_s
63
+ end
64
+
65
+ Dir.mktmpdir do |floppy_dir|
66
+ FileUtils.chmod_R 0775, floppy_dir
67
+ vm.config.floppy = floppy_dir
68
+ #vm.config.add_kernel_cmdline!('autoyast=device://fd0/autoinst.xml')
69
+ vm.config.add_kernel_cmdline!('autoyast=floppy')
70
+ vm.config.reboot = false
71
+
72
+ # WTF SLE and openSUSE have different
73
+ # base pattern names
74
+ #media.open('/content') do |content_file|
75
+ # content_file.each_line do |line|
76
+ # case line
77
+ # when /^DISTRIBUTION (.+)$/
78
+ # case $1
79
+ # when /SUSE_SLE/ then autoyast.minimal_sle!
80
+ # when /openSUSE/ then autoyast.minimal_opensuse!
81
+ # end
82
+ # end
83
+ # end
84
+ #end
85
+
86
+ File.write(File.join(floppy_dir, 'autoinst.xml'), to_xml)
87
+ Vmit.logger.info "AutoYaST: 1st stage."
88
+ puts vm.config.inspect
89
+ vm.up
90
+ vm.wait_until_shutdown! do
91
+ vm.vnc
92
+ end
93
+ vm.config.pop!
94
+
95
+ Vmit.logger.info "AutoYaST: 2st stage."
96
+ # 2nd stage
97
+ vm.config.push!
98
+ vm.config.configure(:reboot => false)
99
+ vm.up
100
+ vm.wait_until_shutdown! do
101
+ vm.vnc
102
+ end
103
+
104
+ end
105
+ ensure
106
+ vm.config.pop!
52
107
  end
53
- @net_udev[mac] = name
54
108
  end
55
109
 
56
110
  def to_xml
@@ -87,10 +141,15 @@ module Vmit
87
141
  }
88
142
  xml.software {
89
143
  xml.patterns('config:type' => 'list') {
90
- @patterns.each do |pat|
144
+ config.patterns.each do |pat|
91
145
  xml.pattern pat
92
146
  end
93
147
  }
148
+ xml.packages('config:type' => 'list') {
149
+ config.packages.each do |pkg|
150
+ xml.package pkg
151
+ end
152
+ }
94
153
  }
95
154
  # SLE 11 can do without this basic partitioning but
96
155
  # SLE 10 is not that smart.
@@ -19,23 +19,54 @@
19
19
  # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
20
  #
21
21
  require 'erb'
22
+ require 'vmit/unattended_install'
22
23
 
23
24
  module Vmit
24
25
 
25
26
  # Some good references here:
26
27
  # http://www.hps.com/~tpg/notebook/autoinstall.php
27
- class DebianPreseed
28
+ class DebianPreseed < UnattendedInstall
29
+
30
+ def execute_autoinstall(vm, args)
31
+ vm.config.push!
32
+ begin
33
+ vm.config.configure(args)
34
+ Dir.mktmpdir do |floppy_dir|
35
+ FileUtils.chmod_R 0755, floppy_dir
36
+ vm.config.floppy = floppy_dir
37
+ vm.config.add_kernel_cmdline!('preseed/file=/floppy/preseed.cfg')
38
+ vm.config.add_kernel_cmdline!('auto=true')
39
+ vm.config.add_kernel_cmdline!('priority=critical')
40
+ vm.config.reboot = false
41
+
42
+ File.write(File.join(floppy_dir, 'preseed.cfg'), to_txt)
43
+ Vmit.logger.info "Preseed: 1st stage."
44
+ vm.up
45
+ vm.wait_until_shutdown! do
46
+ vm.vnc
47
+ end
48
+ end
49
+ ensure
50
+ vm.config.pop!
51
+ end
52
+ end
28
53
 
29
54
  def to_txt
30
55
  template = ERB.new <<-EOF
56
+ d-i debconf/priority select critical
57
+ d-i auto-install/enabled boolean true
31
58
  d-i debian-installer/locale string en_US
32
59
  d-i console-tools/archs select at
33
60
  d-i console-keymaps-at/keymap select American English
61
+
34
62
  d-i debian-installer/keymap string us
63
+
64
+ d-i netcfg/choose_interface select auto
35
65
  d-i netcfg/get_hostname string unassigned-hostname
36
66
  d-i netcfg/get_hostname seen true
37
67
  d-i netcfg/get_domain string unassigned-domain
38
68
  d-i netcfg/get_domain seen true
69
+
39
70
  d-i mirror/protocol string ftp
40
71
  d-i mirror/ftp/hostname string ftp.de.debian.org
41
72
  d-i mirror/ftp/directory string /debian/
@@ -51,7 +82,10 @@ d-i clock-setup/utc boolean true
51
82
  d-i time/zone string US/Eastern
52
83
  d-i clock-setup/ntp boolean true
53
84
  popularity-contest popularity-contest/participate boolean false
85
+
54
86
  d-i pkgsel/include string ssh rsync initrd-tools cramfsprogs lzop
87
+
88
+ d-i passwd/root-login boolean true
55
89
  d-i passwd/root-password password linux
56
90
  d-i passwd/root-password-again password linux
57
91
  d-i passwd/make-user boolean false
@@ -0,0 +1,224 @@
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 'vmit/utils'
22
+ require 'vmit/vfs'
23
+ require 'confstruct'
24
+
25
+ module Vmit
26
+
27
+ class InstallMedia
28
+ abstract_method :unattended_install_class
29
+ abstract_method :kernel_path
30
+ abstract_method :initrd_path
31
+
32
+ attr_reader :location
33
+
34
+ def initialize(location)
35
+ @location = location
36
+ end
37
+
38
+ def to_s
39
+ "#{super.to_s}:#{location}"
40
+ end
41
+
42
+ def unattended_install
43
+ unless @unattended_install
44
+ @unattended_install = unattended_install_class.new(location)
45
+ end
46
+ @unattended_install
47
+ end
48
+
49
+ def self.class_for_media_type(type)
50
+ case type.to_s.downcase
51
+ when /fedora|redhat|centos/ then FedoraInstallMedia
52
+ when /suse/ then SUSEInstallMedia
53
+ when /debian/ then DebianInstallMedia
54
+ else
55
+ raise "Don't know how to bootstrap media #{location}"
56
+ end
57
+ end
58
+
59
+ # @return [InstallMedia] an install media for an arbitrary
60
+ # string like 'sles 11 sp2' or 'SLED-11_SP3'
61
+ #
62
+ # @raise [ArgumentError] if can't figure out an install media
63
+ # from the string
64
+ def self.alias_to_install_media(key)
65
+ case key.to_s.downcase.gsub(/[\s_\-]+/, '')
66
+ when /^opensuse(\d+\.\d+)$/
67
+ SUSEInstallMedia.new(
68
+ 'http://download.opensuse.org/distribution/$version/repo/oss/'
69
+ .gsub('$version', $1))
70
+ when /^(opensuse)?factory$/
71
+ SUSEInstallMedia.new(
72
+ 'http://download.opensuse.org/factory/repo/oss/')
73
+ when /^debian(.+)$/
74
+ DebianInstallMedia.new(
75
+ 'http://cdn.debian.net/debian/dists/$version'
76
+ .gsub('$version', $1))
77
+ when /^ubuntu(.+)$/
78
+ UbuntuInstallMedia.new(
79
+ 'http://archive.ubuntu.com/ubuntu/dists/$version'
80
+ .gsub('$version', $1))
81
+ when /^fedora(\d+)/
82
+ FedoraInstallMedia.new(
83
+ 'http://mirrors.n-ix.net/fedora/linux/releases/$release/Fedora/$arch/os/'
84
+ .gsub('$arch', Vmit::Utils.arch)
85
+ .gsub('$release', $1))
86
+ when /^sle(s|d)?(\d+)(sp(\d+))?$/
87
+ edition = case $1
88
+ when 's' then 'sle-server'
89
+ when 'd' then 'sle-desktop'
90
+ else
91
+ Vmit.logger.warn "SLE given. Assuming server."
92
+ 'sle-server'
93
+ end
94
+ release = $2
95
+ sp = $4 || '0'
96
+ klass = if release.to_i > 9
97
+ SUSEInstallMedia
98
+ else
99
+ SUSE9InstallMedia
100
+ end
101
+ suffix = if (release.to_i > 9)
102
+ '/DVD1'
103
+ else
104
+ if (sp.to_i > 0)
105
+ '/CD1'
106
+ else
107
+ ''
108
+ end
109
+ end
110
+ klass.new(
111
+ "http://schnell.suse.de/BY_PRODUCT/$edition-$release-sp$sp-$arch$topdir"
112
+ .gsub('$edition', edition)
113
+ .gsub('$arch', Vmit::Utils.arch)
114
+ .gsub('$release', release)
115
+ .gsub('$sp', sp)
116
+ .gsub('$topdir', suffix))
117
+ else raise ArgumentError.new("Unknown install media '#{key}'")
118
+ end
119
+ end
120
+
121
+ # @return [InstallMedia] an install media for a url.
122
+ # it accesses the network in order to detect the
123
+ # url type.
124
+ #
125
+ # @raise [ArgumentError] if can't figure out an install media
126
+ # from the string
127
+ def self.url_to_install_media(url)
128
+ begin
129
+ media = Vmit::VFS.from(url)
130
+ media.open('/content')
131
+ return SUSEInstallMedia.new(url)
132
+ rescue
133
+ raise ArgumentError.new("Don't know the install media '#{url}'")
134
+ end
135
+ end
136
+
137
+ # @return [InstallMedia] scans the install media
138
+ # and returns a specific type (suse, debian, etc...)
139
+ def self.scan(key)
140
+ case key
141
+ when /^http:\/|ftp:\// then url_to_install_media(key)
142
+ else alias_to_install_media(key)
143
+ end
144
+ end
145
+
146
+ def autoinstall(vm)
147
+ Vmit.logger.debug("Autoinstall from #{location}")
148
+ media = Vmit::VFS.from(location)
149
+ kernel = media.open(kernel_path)
150
+ initrd = media.open(initrd_path)
151
+ opts = {:kernel => kernel.path, :initrd => initrd.path}
152
+ unattended_install.execute_autoinstall(vm, opts)
153
+ end
154
+
155
+ end
156
+
157
+ class SUSEInstallMedia < InstallMedia
158
+
159
+ def unattended_install_class
160
+ Vmit::AutoYaST
161
+ end
162
+
163
+ def initrd_path
164
+ "/boot/#{Vmit::Utils.arch}/loader/initrd"
165
+ end
166
+
167
+ def kernel_path
168
+ "/boot/#{Vmit::Utils.arch}/loader/linux"
169
+ end
170
+ end
171
+
172
+ class SUSE9InstallMedia < SUSEInstallMedia
173
+ def initrd_path
174
+ "/boot/loader/initrd"
175
+ end
176
+
177
+ def kernel_path
178
+ "/boot/loader/linux"
179
+ end
180
+ end
181
+
182
+ class FedoraInstallMedia < InstallMedia
183
+
184
+ def unattended_install_class
185
+ Vmit::Kickstart
186
+ end
187
+
188
+ def initrd_path
189
+ '/images/pxeboot/initrd.img'
190
+ end
191
+
192
+ def kernel_path
193
+ '/images/pxeboot/vmlinuz'
194
+ end
195
+ end
196
+
197
+ class DebianInstallMedia < InstallMedia
198
+
199
+ def name
200
+ 'debian'
201
+ end
202
+
203
+ def unattended_install_class
204
+ Vmit::DebianPreseed
205
+ end
206
+
207
+ def initrd_path
208
+ arch = Vmit::Utils.arch.gsub(/x86_64/, 'amd64')
209
+ "/main/installer-#{arch}/current/images/netboot/#{name}-installer/#{arch}/initrd.gz"
210
+ end
211
+
212
+ def kernel_path
213
+ arch = Vmit::Utils.arch.gsub(/x86_64/, 'amd64')
214
+ "/main/installer-#{arch}/current/images/netboot/#{name}-installer/#{arch}/linux"
215
+ end
216
+ end
217
+
218
+ class UbuntuInstallMedia < DebianInstallMedia
219
+ def name
220
+ 'ubuntu'
221
+ end
222
+ end
223
+
224
+ end