gogetit 0.1.16
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.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +52 -0
- data/Rakefile +6 -0
- data/bin/console +6 -0
- data/bin/gogetit +10 -0
- data/bin/setup +8 -0
- data/gogetit.gemspec +43 -0
- data/lib/etcd.rb +37 -0
- data/lib/executionhooks.rb +50 -0
- data/lib/gogetit.rb +103 -0
- data/lib/gogetit/cli.rb +44 -0
- data/lib/gogetit/version.rb +3 -0
- data/lib/maas.rb +119 -0
- data/lib/multilogger.rb +56 -0
- data/lib/providers/libvirt.rb +260 -0
- data/lib/providers/lxd.rb +89 -0
- data/lib/sample_conf/ceph.yml +17 -0
- data/lib/sample_conf/default.yml +10 -0
- data/lib/sample_conf/gogetit.yml +24 -0
- data/lib/template/disk.xml +5 -0
- data/lib/template/domain.xml +49 -0
- data/lib/template/nic.xml +4 -0
- data/lib/template/volume.xml +11 -0
- data/lib/util.rb +56 -0
- metadata +247 -0
data/lib/gogetit/cli.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'gogetit'
|
3
|
+
|
4
|
+
module Gogetit
|
5
|
+
class CLI < Thor
|
6
|
+
package_name 'Gogetit'
|
7
|
+
|
8
|
+
desc 'list', 'List containers and instances, running currently.'
|
9
|
+
def list
|
10
|
+
puts "Listing LXD containers on #{Gogetit.config[:lxd][:url]}.."
|
11
|
+
system("lxc list #{Gogetit.config[:lxd][:name]}:")
|
12
|
+
puts ''
|
13
|
+
puts "Listing KVM domains on #{Gogetit.config[:libvirt][:url]}.."
|
14
|
+
system("virsh -c #{Gogetit.config[:libvirt][:url]} list --all")
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'create (TYPE) NAME', 'Create either a container or KVM domain.'
|
18
|
+
def create(type=nil, name)
|
19
|
+
case type
|
20
|
+
when 'lxd', nil
|
21
|
+
Gogetit.lxd.create(name)
|
22
|
+
when 'libvirt'
|
23
|
+
Gogetit.libvirt.create(name)
|
24
|
+
else
|
25
|
+
puts 'Invalid argument entered'
|
26
|
+
end
|
27
|
+
Gogetit.config[:default][:user] ||= ENV['USER']
|
28
|
+
puts "ssh #{Gogetit.config[:default][:user]}@#{name}"
|
29
|
+
end
|
30
|
+
|
31
|
+
desc 'destroy NAME', 'Destroy either a container or KVM domain.'
|
32
|
+
def destroy(name)
|
33
|
+
type = Gogetit.get_provider_of(name)
|
34
|
+
if type
|
35
|
+
case type
|
36
|
+
when 'lxd', nil
|
37
|
+
Gogetit.lxd.destroy(name)
|
38
|
+
when 'libvirt'
|
39
|
+
Gogetit.libvirt.destroy(name)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/maas.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'maas/client'
|
2
|
+
|
3
|
+
module Gogetit
|
4
|
+
class GogetMAAS
|
5
|
+
attr_reader :config, :conn, :domain, :logger
|
6
|
+
|
7
|
+
def initialize(conf, logger)
|
8
|
+
@config = conf
|
9
|
+
@conn = Maas::Client::MaasClient.new(
|
10
|
+
config[:maas][:key],
|
11
|
+
config[:maas][:url]
|
12
|
+
)
|
13
|
+
@logger = logger
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_domain
|
17
|
+
return @domain if @domain
|
18
|
+
logger.info("Calling <#{__method__.to_s}>")
|
19
|
+
@domain = conn.request(:get, ['domains'])[0]['name']
|
20
|
+
end
|
21
|
+
|
22
|
+
def machine_exists?(name)
|
23
|
+
logger.info("Calling <#{__method__.to_s}>")
|
24
|
+
conn.request(:get, ['machines']).each do |m|
|
25
|
+
return true if m['hostname'] == name
|
26
|
+
end
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
30
|
+
def dnsresource_exists?(name)
|
31
|
+
logger.info("Calling <#{__method__.to_s}>")
|
32
|
+
conn.request(:get, ['dnsresources']).each do |item|
|
33
|
+
return true if item['fqdn'] == name + '.' + get_domain
|
34
|
+
end
|
35
|
+
false
|
36
|
+
end
|
37
|
+
|
38
|
+
def domain_name_exists?(name)
|
39
|
+
return true if dnsresource_exists?(name) or machine_exists?(name)
|
40
|
+
end
|
41
|
+
|
42
|
+
def ipaddresses(op = nil, params = nil)
|
43
|
+
case op
|
44
|
+
when nil
|
45
|
+
conn.request(:get, ['ipaddresses'])
|
46
|
+
when 'reserve'
|
47
|
+
# sample = {
|
48
|
+
# 'subnet' => '10.1.2.0/24',
|
49
|
+
# 'ip' => '10.1.2.8',
|
50
|
+
# 'hostname' => 'hostname',
|
51
|
+
# 'mac' => 'blahblah'
|
52
|
+
# }
|
53
|
+
default_param = { 'op' => op }
|
54
|
+
conn.request(:post, ['ipaddresses'], default_param.merge!(params))
|
55
|
+
when 'release'
|
56
|
+
# Gogetit.maas.ipaddresses('release', {'ip' => '10.1.2.8'})
|
57
|
+
# sample = {
|
58
|
+
# 'ip' => '10.1.2.8',
|
59
|
+
# 'hostname' => 'hostname',
|
60
|
+
# 'mac' => 'blahblah'
|
61
|
+
# }
|
62
|
+
default_param = { 'op' => op }
|
63
|
+
conn.request(:post, ['ipaddresses'], default_param.merge!(params))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def delete_dns_record(name)
|
68
|
+
logger.info("Calling <#{__method__.to_s}>")
|
69
|
+
id = nil
|
70
|
+
conn.request(:get, ['dnsresources']).each do |item|
|
71
|
+
if item['fqdn'] == name + '.' + get_domain
|
72
|
+
id = item['id']
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
if ! id.nil?
|
77
|
+
conn.request(:delete, ['dnsresources', id.to_s])
|
78
|
+
else
|
79
|
+
logger.warn('No such record found.')
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def refresh_pods
|
84
|
+
logger.info("Calling <#{__method__.to_s}>")
|
85
|
+
pod_id = conn.request(:get, ['pods'])
|
86
|
+
pod_id.each do |pod|
|
87
|
+
conn.request(:post, ['pods', pod['id']], { 'op' => 'refresh' } )
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def get_system_id(name)
|
92
|
+
logger.info("Calling <#{__method__.to_s}>")
|
93
|
+
conn.request(:get, ['machines']).each do |m|
|
94
|
+
return m['system_id'] if m['hostname'] == name
|
95
|
+
end
|
96
|
+
nil
|
97
|
+
end
|
98
|
+
|
99
|
+
def wait_until_state(system_id, state)
|
100
|
+
logger.info("Calling <#{__method__.to_s}> for being #{state}")
|
101
|
+
until conn.request(:get, ['machines', system_id])['status_name'] == state
|
102
|
+
sleep 3
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def get_machine_state(system_id)
|
107
|
+
logger.info("Calling <#{__method__.to_s}>")
|
108
|
+
conn.request(:get, ['machines']).each do |m|
|
109
|
+
return m['status_name'] if m['system_id'] == system_id
|
110
|
+
end
|
111
|
+
false
|
112
|
+
end
|
113
|
+
|
114
|
+
def change_hostname(system_id, hostname)
|
115
|
+
logger.info("Calling <#{__method__.to_s}>")
|
116
|
+
conn.request(:put, ['machines', system_id], { 'hostname' => hostname })
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
data/lib/multilogger.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Gogetit
|
4
|
+
# It was just taken from below source. Thanks to clowder!
|
5
|
+
# https://gist.github.com/clowder/3639600
|
6
|
+
class MultiLogger
|
7
|
+
attr_reader :level
|
8
|
+
|
9
|
+
def initialize(args={})
|
10
|
+
@level = args[:level] || Logger::Severity::INFO
|
11
|
+
@loggers = []
|
12
|
+
|
13
|
+
Array(args[:loggers]).each { |logger| add_logger(logger) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_logger(logger)
|
17
|
+
logger.level = level
|
18
|
+
@loggers << logger
|
19
|
+
end
|
20
|
+
|
21
|
+
def level=(level)
|
22
|
+
@level = level
|
23
|
+
@loggers.each { |logger| logger.level = level }
|
24
|
+
end
|
25
|
+
|
26
|
+
def datetime_format=(format)
|
27
|
+
@loggers.each { |logger| logger.datetime_format = format }
|
28
|
+
end
|
29
|
+
|
30
|
+
def formatter=(format)
|
31
|
+
@loggers.each { |logger| logger.formatter = format }
|
32
|
+
end
|
33
|
+
|
34
|
+
def progname=(name)
|
35
|
+
@loggers.each { |logger| logger.progname = name }
|
36
|
+
end
|
37
|
+
|
38
|
+
def close
|
39
|
+
@loggers.map(&:close)
|
40
|
+
end
|
41
|
+
|
42
|
+
def add(level, *args)
|
43
|
+
@loggers.each { |logger| logger.add(level, args) }
|
44
|
+
end
|
45
|
+
|
46
|
+
Logger::Severity.constants.each do |level|
|
47
|
+
define_method(level.downcase) do |*args|
|
48
|
+
@loggers.each { |logger| logger.send(level.downcase, args) }
|
49
|
+
end
|
50
|
+
|
51
|
+
define_method("#{ level.downcase }?".to_sym) do
|
52
|
+
@level <= Logger::Severity.const_get(level)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,260 @@
|
|
1
|
+
require 'libvirt'
|
2
|
+
require 'securerandom'
|
3
|
+
require 'oga'
|
4
|
+
require 'rexml/document'
|
5
|
+
require 'util'
|
6
|
+
|
7
|
+
module Gogetit
|
8
|
+
class GogetLibvirt
|
9
|
+
include Gogetit::Util
|
10
|
+
|
11
|
+
attr_reader :config, :conn, :maas, :logger
|
12
|
+
|
13
|
+
def initialize(conf, maas, logger)
|
14
|
+
@config = conf
|
15
|
+
@conn = Libvirt::open(config[:libvirt][:url])
|
16
|
+
@maas = maas
|
17
|
+
@logger = logger
|
18
|
+
end
|
19
|
+
|
20
|
+
def get_domain_list
|
21
|
+
logger.info("Calling <#{__method__.to_s}>")
|
22
|
+
domains = []
|
23
|
+
conn.list_all_domains.each do |d|
|
24
|
+
domains << d.name
|
25
|
+
end
|
26
|
+
domains
|
27
|
+
end
|
28
|
+
|
29
|
+
def domain_exists?(name)
|
30
|
+
logger.info("Calling <#{__method__.to_s}>")
|
31
|
+
get_domain_list.each do |d|
|
32
|
+
return true if d == name
|
33
|
+
end
|
34
|
+
false
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_mac_addr(domain_name)
|
38
|
+
logger.info("Calling <#{__method__.to_s}>")
|
39
|
+
Oga.parse_xml(conn.lookup_domain_by_name(domain_name).xml_desc)
|
40
|
+
.at_xpath('domain/devices/interface[1]/mac')
|
41
|
+
.attribute('address')
|
42
|
+
.value
|
43
|
+
end
|
44
|
+
|
45
|
+
# subject.create(name: 'test01')
|
46
|
+
def create(name, conf_file = nil)
|
47
|
+
logger.info("Calling <#{__method__.to_s}>")
|
48
|
+
if maas.domain_name_exists?(name) or domain_exists?(name)
|
49
|
+
puts "Domain #{name} already exists! Please check both on MAAS and libvirt."
|
50
|
+
return false
|
51
|
+
end
|
52
|
+
|
53
|
+
conf_file ||= config[:default_provider_conf_file]
|
54
|
+
domain = symbolize_keys(YAML.load_file(conf_file))
|
55
|
+
domain[:name] = name
|
56
|
+
domain[:uuid] = SecureRandom.uuid
|
57
|
+
|
58
|
+
dom = conn.define_domain_xml(define_domain(domain))
|
59
|
+
maas.refresh_pods
|
60
|
+
|
61
|
+
system_id = maas.get_system_id(domain[:name])
|
62
|
+
maas.wait_until_state(system_id, 'Ready')
|
63
|
+
logger.info("Calling to deploy...")
|
64
|
+
maas.conn.request(:post, ['machines', system_id], {'op' => 'deploy'})
|
65
|
+
maas.wait_until_state(system_id, 'Deployed')
|
66
|
+
logger.info("#{domain[:name]} has been created.")
|
67
|
+
true
|
68
|
+
end
|
69
|
+
|
70
|
+
def destroy(name)
|
71
|
+
logger.info("Calling <#{__method__.to_s}>")
|
72
|
+
system_id = maas.get_system_id(name)
|
73
|
+
if maas.machine_exists?(name)
|
74
|
+
if maas.get_machine_state(system_id) == 'Deployed'
|
75
|
+
logger.info("Calling to release...")
|
76
|
+
maas.conn.request(:post, ['machines', system_id], {'op' => 'release'})
|
77
|
+
maas.wait_until_state(system_id, 'Ready')
|
78
|
+
end
|
79
|
+
maas.conn.request(:delete, ['machines', system_id])
|
80
|
+
end
|
81
|
+
|
82
|
+
pools = []
|
83
|
+
conn.list_storage_pools.each do |name|
|
84
|
+
pools << self.conn.lookup_storage_pool_by_name(name)
|
85
|
+
end
|
86
|
+
|
87
|
+
dom = conn.lookup_domain_by_name(name)
|
88
|
+
dom.destroy if dom.active?
|
89
|
+
Oga.parse_xml(dom.xml_desc).xpath('domain/devices/disk/source').each do |d|
|
90
|
+
pool_path = d.attribute('file').value.split('/')[0..2].join('/')
|
91
|
+
pools.each do |p|
|
92
|
+
if Oga.parse_xml(p.xml_desc).at_xpath('pool/target/path').inner_text == pool_path
|
93
|
+
logger.info("Deleting volume in #{p.name} pool.")
|
94
|
+
p.lookup_volume_by_name(d.attribute('file').value.split('/')[3]).delete
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
dom.undefine
|
99
|
+
|
100
|
+
maas.refresh_pods
|
101
|
+
logger.info("#{name} has been destroyed.")
|
102
|
+
true
|
103
|
+
end
|
104
|
+
|
105
|
+
def define_domain(domain)
|
106
|
+
logger.info("Calling <#{__method__.to_s}>")
|
107
|
+
template = File.read(config[:lib_dir] + '/template/domain.xml')
|
108
|
+
doc = Oga.parse_xml(template)
|
109
|
+
|
110
|
+
name = domain[:name]
|
111
|
+
doc.at_xpath('domain/name').inner_text = name
|
112
|
+
uuid = domain[:uuid]
|
113
|
+
doc.at_xpath('domain/uuid').inner_text = uuid
|
114
|
+
vcpu = domain[:vcpu].to_s
|
115
|
+
doc.at_xpath('domain/vcpu').inner_text = vcpu
|
116
|
+
memory = domain[:memory].to_s
|
117
|
+
doc.at_xpath('domain/memory').inner_text = memory
|
118
|
+
doc.at_xpath('domain/currentMemory').inner_text = memory
|
119
|
+
|
120
|
+
doc = define_volumes(doc, domain)
|
121
|
+
doc = add_nic(doc, domain[:nic])
|
122
|
+
|
123
|
+
#print_xml(doc)
|
124
|
+
#volumes.each do |v|
|
125
|
+
# print_xml(v)
|
126
|
+
#end
|
127
|
+
|
128
|
+
return Oga::XML::Generator.new(doc).to_xml
|
129
|
+
end
|
130
|
+
|
131
|
+
def print_xml(doc)
|
132
|
+
logger.info("Calling <#{__method__.to_s}>")
|
133
|
+
output = REXML::Document.new(Oga::XML::Generator.new(doc).to_xml)
|
134
|
+
formatter = REXML::Formatters::Pretty.new
|
135
|
+
formatter.compact = true
|
136
|
+
formatter.write(output, $stdout)
|
137
|
+
end
|
138
|
+
|
139
|
+
def get_pool_path(pool)
|
140
|
+
logger.info("Calling <#{__method__.to_s}>")
|
141
|
+
path = nil
|
142
|
+
conn.list_all_storage_pools.each do |p|
|
143
|
+
if p.name == pool
|
144
|
+
pool_doc = Oga.parse_xml(p.xml_desc)
|
145
|
+
path = pool_doc.at_xpath('pool/target/path').inner_text
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
if path
|
150
|
+
return path
|
151
|
+
else
|
152
|
+
raise 'No such pool found.'
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def define_volumes(document, domain)
|
157
|
+
logger.info("Calling <#{__method__.to_s}>")
|
158
|
+
disk_template = File.read(config[:lib_dir] + '/template/disk.xml')
|
159
|
+
disk_doc = Oga.parse_xml(disk_template)
|
160
|
+
volume_template = File.read(config[:lib_dir] + '/template/volume.xml')
|
161
|
+
volume_doc = Oga.parse_xml(volume_template)
|
162
|
+
|
163
|
+
defined_volumes = []
|
164
|
+
|
165
|
+
# For root volume
|
166
|
+
pool_path = get_pool_path(domain[:disk][:root][:pool])
|
167
|
+
volume_name = "#{domain[:name]}_root_sda.qcow2"
|
168
|
+
volume_file = pool_path + "/" + volume_name
|
169
|
+
disk_doc.at_xpath('disk/source').attribute('file').value = volume_file
|
170
|
+
document.at_xpath('domain/devices').children << disk_doc.at_xpath('disk')
|
171
|
+
|
172
|
+
volume_doc.at_xpath('volume/name').inner_text = volume_name
|
173
|
+
volume_doc.at_xpath('volume/target/path').inner_text = volume_file
|
174
|
+
volume_doc.at_xpath('volume/capacity').inner_text = domain[:disk][:root][:capacity].to_s
|
175
|
+
|
176
|
+
create_volume(domain[:disk][:root][:pool], Oga::XML::Generator.new(volume_doc).to_xml)
|
177
|
+
defined_volumes << volume_doc
|
178
|
+
|
179
|
+
# For data(secondary) volumes
|
180
|
+
if domain[:disk][:data] != [] and domain[:disk][:data] != nil
|
181
|
+
disk_index = 98
|
182
|
+
domain[:disk][:data].each do |v|
|
183
|
+
pool_path = get_pool_path(v[:pool])
|
184
|
+
volume_index = "sd" + disk_index.chr
|
185
|
+
volume_name = "#{domain[:name]}_data_#{volume_index}.qcow2"
|
186
|
+
volume_file = pool_path + "/" + volume_name
|
187
|
+
disk_doc = Oga.parse_xml(disk_template)
|
188
|
+
disk_doc.at_xpath('disk/source').attribute('file').value = volume_file
|
189
|
+
disk_doc.at_xpath('disk/target').attribute('dev').value = volume_index
|
190
|
+
document.at_xpath('domain/devices').children << disk_doc.at_xpath('disk')
|
191
|
+
|
192
|
+
volume_doc = Oga.parse_xml(volume_template)
|
193
|
+
volume_doc.at_xpath('volume/name').inner_text = volume_name
|
194
|
+
volume_doc.at_xpath('volume/target/path').inner_text = volume_file
|
195
|
+
volume_doc.at_xpath('volume/capacity').inner_text = v[:capacity].to_s
|
196
|
+
create_volume(v[:pool], Oga::XML::Generator.new(volume_doc).to_xml)
|
197
|
+
defined_volumes << volume_doc
|
198
|
+
disk_index += 1
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
return document
|
203
|
+
end
|
204
|
+
|
205
|
+
def create_volume(pool_name, volume_doc)
|
206
|
+
logger.info("Calling <#{__method__.to_s}> to create volume in #{pool_name} pool.")
|
207
|
+
pool = conn.lookup_storage_pool_by_name(pool_name)
|
208
|
+
pool.create_volume_xml(volume_doc)
|
209
|
+
pool.refresh
|
210
|
+
end
|
211
|
+
|
212
|
+
def add_nic(document, nic_conf)
|
213
|
+
logger.info("Calling <#{__method__.to_s}>")
|
214
|
+
template = File.read(config[:lib_dir] + "/template/nic.xml")
|
215
|
+
doc = Oga.parse_xml(template)
|
216
|
+
|
217
|
+
nic_conf.each do |nic|
|
218
|
+
doc = Oga.parse_xml(template)
|
219
|
+
doc.at_xpath('interface/source').attribute('network').value = nic[:network]
|
220
|
+
doc.at_xpath('interface/source').attribute('portgroup').value = nic[:portgroup]
|
221
|
+
document.at_xpath('domain/devices').children << doc.at_xpath('interface')
|
222
|
+
end
|
223
|
+
|
224
|
+
document
|
225
|
+
end
|
226
|
+
|
227
|
+
#def generate_xml
|
228
|
+
# domain = Oga::XML::Element.new(name: 'domain')
|
229
|
+
# domain.add_attribute(Oga::XML::Attribute.new(name: "type", value: "kvm"))
|
230
|
+
# name = Oga::XML::Element.new(name: 'name')
|
231
|
+
# name.inner_text = 'ceph'
|
232
|
+
# domain.children << name
|
233
|
+
# doc = REXML::Document.new(Oga::XML::Generator.new(domain).to_xml)
|
234
|
+
# formatter = REXML::Formatters::Pretty.new
|
235
|
+
# formatter.compact = true
|
236
|
+
# formatter.write(doc, $stdout)
|
237
|
+
|
238
|
+
# disk = create_element(name: 'domain')
|
239
|
+
# disk.add_attribute(Oga::XML::Attribute.new(name: "type", value: "file"))
|
240
|
+
# disk.add_attribute(Oga::XML::Attribute.new(name: "device", value: "disk"))
|
241
|
+
# driver = create_element(name: 'domain')
|
242
|
+
# driver.add_attribute(Oga::XML::Attribute.new(name: "name", value: "qemu"))
|
243
|
+
# driver.add_attribute(Oga::XML::Attribute.new(name: "type", value: "qcow2"))
|
244
|
+
# source = create_element(name: 'source')
|
245
|
+
# source.add_attribute(Oga::XML::Attribute.new(name: "file", value: file))
|
246
|
+
# target = create_element(name: 'target')
|
247
|
+
# target.add_attribute(Oga::XML::Attribute.new(name: "dev", value: seq_name))
|
248
|
+
# target.add_attribute(Oga::XML::Attribute.new(name: "bus", value: "scsi"))
|
249
|
+
|
250
|
+
#end
|
251
|
+
|
252
|
+
#def create_element(name)
|
253
|
+
# Oga::XML::Element.new(name: name)
|
254
|
+
#end
|
255
|
+
|
256
|
+
#def add_attribute(element, hash)
|
257
|
+
# element.add_attribute(Oga::XML::Attribute.new(hash))
|
258
|
+
#end
|
259
|
+
end
|
260
|
+
end
|