nuri 0.5.3 → 0.5.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -1
  3. data/VERSION +1 -1
  4. data/bin/nuri +60 -14
  5. data/bin/nuri-install-module +17 -9
  6. data/examples/mockcloud/apache2.sfp +14 -0
  7. data/examples/mockcloud/ping.rb +38 -0
  8. data/examples/openstack/openstack-hadoop1-cluster.sfp +37 -0
  9. data/examples/openstack/openstack-hadoop2-cluster.sfp +39 -0
  10. data/examples/v2/apache.sfp +30 -0
  11. data/examples/v2/aptpackage.sfp +6 -0
  12. data/examples/v2/mock1.sfp +12 -0
  13. data/examples/v2/package.sfp +22 -0
  14. data/examples/v2/service.sfp +94 -0
  15. data/examples/v2/tarpackage.sfp +5 -0
  16. data/lib/nuri.rb +14 -10
  17. data/lib/nuri/choreographer.rb +3 -3
  18. data/lib/nuri/helper.rb +20 -10
  19. data/lib/nuri/master.rb +82 -54
  20. data/lib/nuri/orchestrator.rb +1 -1
  21. data/modules/.gitignore +0 -4
  22. data/modules/README.md +11 -0
  23. data/modules/apache/apache.sfp +2 -1
  24. data/modules/file/file.rb +49 -19
  25. data/modules/hadoop1/hadoop1.rb +18 -11
  26. data/modules/hadoop2/hadoop2.rb +11 -11
  27. data/modules/hadoop2/hadoop2.sfp +7 -6
  28. data/modules/hadoop2/yarn-site.xml +5 -0
  29. data/modules/machine/machine.rb +24 -14
  30. data/modules/openstack/README.md +24 -0
  31. data/modules/openstack/config.yml +5 -0
  32. data/modules/openstack/example.sfp +9 -0
  33. data/modules/openstack/openstack.rb +329 -0
  34. data/modules/openstack/openstack.sfp +24 -0
  35. data/modules/os/os.rb +1 -1
  36. data/modules/package2/apt-repo-list.sh +15 -0
  37. data/modules/package2/package2.rb +213 -43
  38. data/modules/package2/package2.sfp +3 -2
  39. data/modules/pyfile/README.md +4 -0
  40. data/modules/vm/vm.rb +3 -1
  41. data/modules/vm/vm.sfp +4 -3
  42. metadata +20 -3
  43. data/modules/hpcloud/test.sfp +0 -5
@@ -0,0 +1,329 @@
1
+ require 'rubygems'
2
+ require 'thread'
3
+ require 'json'
4
+ require 'fog'
5
+ require 'yaml'
6
+ require 'net/ssh'
7
+
8
+ ##############################
9
+ #
10
+ # Order of config file for credentials:
11
+ # 1) <HOME_DIRECTORY>/.hpcloud
12
+ # 2) <MODULE_DIRECTORY>/config.yml
13
+ #
14
+ #
15
+ # Order of SSH key file
16
+ # 1) <HOME_DIRECTORY>/.ssh/<KEY_PAIR_NAME>.pem
17
+ # 2) <HOME_DIRECTORY>/.ssh/<KEY_PAIR_NAME>
18
+ # 3) <MODULE_DIRECTORY>/<KEY_PAIR_NAME>.pem
19
+ # 4) <MODULE_DIRECTORY>/<KEY_PAIR_NAME>
20
+ # 5) <MODULE_DIRECTORY>/hpcloud.pem
21
+ #
22
+ ##############################
23
+
24
+ class Sfp::Module::OpenStack
25
+ include Sfp::Resource
26
+
27
+ ### Sleep time in waiting a task to be finished - default: 5s
28
+ SleepTime = 5
29
+
30
+ ### Number of tries in waiting a task to be finished - default: 120 (10 minutes)
31
+ Tries = 600 / SleepTime
32
+
33
+ def initialize
34
+ @conn = nil
35
+ end
36
+
37
+ def update_state
38
+ to_model
39
+
40
+ @state['running'] = running?
41
+ @state['vms'] = get_vms
42
+
43
+ end
44
+
45
+ ##############################
46
+ #
47
+ # Action methods (see hpcloud.sfp and cloud.sfp)
48
+ #
49
+ ##############################
50
+
51
+ def create_vm(params={})
52
+ return false if not self.running?
53
+
54
+ ### determine VM's name
55
+ name = params['vm'].sub(/^\$\./, '')
56
+
57
+ log.info "Creating VM #{name} [WAIT]"
58
+
59
+ ### check if VM is already exist
60
+ if get_vms.has_key?(name)
61
+ log.info "VM #{name} is already exist - Creating VM #{name} [OK]"
62
+ return true
63
+ end
64
+
65
+ ### get VM model from cache
66
+ vm_model = resolve_model(params['vm'])
67
+
68
+ ### determine VM parameters
69
+ flavor = (vm_model['flavor'].to_s.length > 0 ? vm_model['flavor'].to_s : @model['vm_flavor'])
70
+ image = (vm_model['image'].to_s.length > 0 ? vm_model['image'].to_s : @model['vm_image'])
71
+ security_group = (vm_model['security_group'].to_s.length > 0 ? vm_model['security_group'].to_s : @model['vm_security_group'])
72
+ ssh_key_name = (vm_model['ssh_key_name'].to_s.length > 0 ? vm_model['ssh_key_name'].to_s : @model['vm_ssh_key_name'])
73
+ ssh_key_file = self.ssh_key_file(ssh_key_name)
74
+ ssh_user = (vm_model['ssh_user'].to_s.length > 0 ? vm_model['ssh_user'].to_s : @model['vm_ssh_user'].to_s)
75
+
76
+ ### set network
77
+ networks = vm_model['networks'].keys.select! { |k| k[0] != '_' }
78
+ networks = Array(@model['vm_network']) if networks.length <= 0 and @model['vm_network'].strip.length > 0
79
+ nets = get_networks
80
+ networks.map! { |net| {'net_id' => nets[net]['id']} }
81
+
82
+ ### check SSH key file
83
+ if ssh_key_file.nil?
84
+ log.info "SSH key file '#{ssh_key_file}' is not available! #{ssh_key_name} - Creating VM #{name} [Failed]"
85
+ return false
86
+ end
87
+ log.info "#{name}:VM - SSH user: #{ssh_user}, SSH key file: #{ssh_key_file}"
88
+
89
+ spec = {
90
+ :name => name,
91
+ :flavor_ref => flavor,
92
+ :image_ref => image,
93
+ :key_name => ssh_key_name,
94
+ :security_groups => [security_group],
95
+ :metadata => {'name' => name},
96
+ }
97
+ spec['nics'] = networks if networks.length > 0
98
+
99
+ log.info "#{name}:VM - #{spec.inspect}"
100
+
101
+ ### if not exist, then create the VM
102
+ created = false
103
+ begin
104
+ ### submit create VM request
105
+ vm = @conn.servers.create(spec)
106
+ log.info "#{name}:VM - spec has been submitted"
107
+
108
+ ### set SSH config
109
+ vm.username = ssh_user # 'ubuntu'
110
+ vm.private_key_path = ssh_key_file
111
+
112
+ log.info "#{name}:VM - ready [Wait]"
113
+ ### wait until SSH is enabled
114
+ vm.wait_for { ready? }
115
+ log.info "#{name}:VM - ready [OK]"
116
+
117
+ log.info "#{name}:VM - sshable [Wait]"
118
+ ### wait until SSH is sshable
119
+ ip = nil
120
+ vm.wait_for {
121
+ #vm.sshable? ### this does not work (fog's bug?)
122
+ ip = vm.addresses['default'].first['addr'].to_s if
123
+ vm.addresses['default'].is_a?(Array) and vm.addresses['default'].length > 0
124
+ if ip.length > 0
125
+ port = 22
126
+ begin
127
+ Timeout::timeout(1) do
128
+ begin
129
+ s = TCPSocket.new(ip, port)
130
+ s.close
131
+ true
132
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
133
+ false
134
+ end
135
+ end
136
+ rescue Timeout::Error
137
+ false
138
+ end
139
+ else
140
+ false
141
+ end
142
+ }
143
+ log.info "#{name}:VM - sshable, ip: #{ip} [OK]"
144
+
145
+ ### install sfpagent
146
+ created = install_agent(vm, name, ssh_key_file, ssh_user, ip)
147
+
148
+ rescue Exception => exp
149
+ log.info "#{exp}\n#{exp.backtrace.join("\n")}"
150
+ end
151
+
152
+ if not created
153
+ log.error "Creating VM #{name} [Failed]"
154
+
155
+ ### delete if any error occured
156
+ delete_vm(params)
157
+ else
158
+ log.info "Creating VM #{name} [OK]"
159
+ end
160
+
161
+ created
162
+ end
163
+
164
+ def delete_vm(params={})
165
+ return false if not self.running?
166
+
167
+ ### determine VM's name
168
+ name = params['vm'].sub(/^\$\./, '')
169
+
170
+ ### check if VM is not exist
171
+ return true if !get_vms.has_key?(name)
172
+
173
+ ### delete if VM with given name exists
174
+ @conn.servers.each { |s|
175
+ if s.name == name
176
+ @conn.delete_server(s.id)
177
+ break
178
+ end
179
+ }
180
+
181
+ ### wait until the VM is completely deleted
182
+ tries = Tries
183
+ deleted = false
184
+ while not deleted and tries > 0
185
+ begin
186
+ deleted = !get_vms.has_key?(name)
187
+ break if deleted
188
+ rescue
189
+ end
190
+ log.info "VM:#{name} has been deleted."
191
+ tries -= 1
192
+ sleep SleepTime
193
+ end
194
+
195
+ deleted
196
+ end
197
+
198
+ def is_port_open?(ip, port)
199
+ begin
200
+ Timeout::timeout(1) do
201
+ begin
202
+ s = TCPSocket.new(ip, port)
203
+ s.close
204
+ true
205
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
206
+ false
207
+ end
208
+ end
209
+ rescue Timeout::Error
210
+ false
211
+ end
212
+ end
213
+
214
+ ##############################
215
+ #
216
+ # Helper methods
217
+ #
218
+ ##############################
219
+
220
+ protected
221
+ def config_file
222
+ return Dir.home + '/.openstack' if ::File.exist?(Dir.home + '/.openstack')
223
+ ::File.dirname(__FILE__) + '/config.yml'
224
+ end
225
+
226
+ def read_config
227
+ return {} if not ::File.exist?(config_file)
228
+ return YAML.load_file(config_file)
229
+ end
230
+
231
+ def ssh_key_file(key_name)
232
+ file = Dir.home + '/.ssh/' + key_name + '.pem'
233
+ return file if ::File.exist?(file)
234
+
235
+ file = Dir.home + '/.ssh/' + key_name
236
+ return file if ::File.exist?(file)
237
+
238
+ file = File.dirname(__FILE__) + '/' + key_name + '.pem'
239
+ return file if ::File.exist?(file)
240
+
241
+ file = File.dirname(__FILE__) + '/' + key_name
242
+ return file if ::File.exist?(file)
243
+
244
+ file = File.dirname(__FILE__) + '/hpcloud.pem'
245
+ return file if ::File.exist?(file)
246
+
247
+ nil
248
+ end
249
+
250
+ def running?
251
+ return true if not @conn.nil?
252
+ begin
253
+ config = self.read_config
254
+ credential = {
255
+ :provider => :openstack,
256
+ :openstack_username => config['username'],
257
+ :openstack_api_key => config['password'],
258
+ :openstack_auth_url => @model['auth_uri'],
259
+ :openstack_tenant => config['tenant_id'],
260
+ }
261
+ @conn = Fog::Compute.new(credential)
262
+ @network = Fog::Network.new(credential)
263
+
264
+ rescue Exception => exp
265
+ log.error "#{exp}\n#{exp.backtrace.join("\n")}"
266
+ @conn = nil
267
+ end
268
+ return !!@conn
269
+ end
270
+
271
+ def get_vms
272
+ return {} if not running?
273
+ vms = {}
274
+ @conn.servers.each { |s|
275
+ spec = {
276
+ 'running' => s.ready?,
277
+ 'ip' => s.public_ip_address.to_s,
278
+ }
279
+ spec['ip'] = s.addresses['default'].first['addr'] if
280
+ spec['ip'].length <= 0 and s.addresses['default'].is_a?(Array) and s.addresses['default'].length > 0
281
+ vms[s.name] = spec
282
+ }
283
+ vms
284
+ end
285
+
286
+ def get_networks
287
+ nets = {}
288
+ @network.networks.each do |net|
289
+ nets[net.name] = {
290
+ 'status' => net.status.downcase,
291
+ 'id' => net.id
292
+ }
293
+ end
294
+ nets
295
+ end
296
+
297
+ def install_agent(vm, name, ssh_key_file=nil, ssh_user="ubuntu", ip=nil)
298
+ ip = vm.public_ip_address if ip.nil?
299
+
300
+ log.info "Installing agent on #{name}:VM, ip=#{ip} [Wait]"
301
+
302
+ begin
303
+ Net::SSH.start(ip, ssh_user, :keys => [ssh_key_file]) do |ssh|
304
+ if ssh.exec!('which sfpagent').strip.length <= 0
305
+ ### install sfpagent
306
+ ssh.exec! 'apt-get update && ' +
307
+ 'apt-get -y install sudo ruby1.9.1 ruby1.9.1-dev libz-dev libaugeas-ruby1.9.1 && ' +
308
+ 'gem install sfp sfpagent net-ssh --no-ri --no-rdoc'
309
+ ssh.exec! 'sfpagent -t'
310
+ end
311
+ ssh.exec! 'sfpagent -s'
312
+ ssh.exec! "echo '#{path}' > /var/sfpagent/vm.in_cloud"
313
+
314
+ if ssh.exec!("hostname").strip != name
315
+ ssh.exec! "/bin/echo '#{name}' > /etc/hostname"
316
+ ssh.exec! "service hostname start; service hostname.sh start"
317
+ ssh.exec! "/bin/sed -i 's/#{name}.*/#{name}/g' /etc/hosts"
318
+ ssh.exec! "/bin/echo '#{ip} #{name}' >> /etc/hosts"
319
+ end
320
+ end
321
+ log.info "Installing agent on #{name}:VM, ip=#{ip} [OK]"
322
+ true
323
+ rescue Exception => exp
324
+ log.error "Installing agent on #{name}:VM, ip=#{ip} [Failed] - #{exp}\n#{exp.backtrace.join("\n")}"
325
+ false
326
+ end
327
+ end
328
+ end
329
+
@@ -0,0 +1,24 @@
1
+ include "../cloud/cloud.sfp"
2
+
3
+ schema OpenStack extends Cloud {
4
+ final description = "Openstack"
5
+ final auth_uri = "https://openstack.com/v2.0/tokens"
6
+
7
+ // default SSH username
8
+ final vm_ssh_user = "root"
9
+
10
+ // small instance
11
+ final vm_flavor = "2"
12
+
13
+ // default image
14
+ final vm_image = "67074"
15
+
16
+ // default key pair which will be used to SSH to VM
17
+ final vm_ssh_key_name = "default"
18
+
19
+ // default security group
20
+ final vm_security_group = "default"
21
+
22
+ // default network
23
+ final vm_network = "default"
24
+ }
@@ -21,7 +21,7 @@ class Sfp::Module::OS
21
21
  @state["type"] = `uname -s`.strip
22
22
  @state["version"] = `uname -r`.strip
23
23
  @state["arch"] = `uname -p`.strip
24
- @state["platform"] = (File.exist?('/etc/issue') ? `cat /etc/issue`.strip : '')
24
+ @state["platform"] = RUBY_PLATFORM
25
25
  end
26
26
 
27
27
  def apply(p={})
@@ -0,0 +1,15 @@
1
+ #! /bin/sh
2
+ # Script to get all the PPA installed on a system
3
+ for APT in `find /etc/apt/ -name \*.list`; do
4
+ grep -Po "(?<=^deb\s).*?(?=#|$)" $APT | while read ENTRY ; do
5
+ HOST=`echo $ENTRY | cut -d/ -f3`
6
+ USER=`echo $ENTRY | cut -d/ -f4`
7
+ PPA=`echo $ENTRY | cut -d/ -f5`
8
+ #echo sudo apt-add-repository ppa:$USER/$PPA
9
+ if [ "ppa.launchpad.net" = "$HOST" ]; then
10
+ echo ppa:$USER/$PPA
11
+ else
12
+ echo ${ENTRY}
13
+ fi
14
+ done
15
+ done
@@ -4,66 +4,236 @@ class Sfp::Module::Package2
4
4
  include Sfp::Resource
5
5
 
6
6
  def update_state
7
- to_model
7
+ provider = @model['provider'].strip
8
+ if provider != self.provider
9
+ case provider
10
+ when 'apt'
11
+ self.extend(AptPackage)
12
+ when 'tar'
13
+ self.extend(TarPackage)
14
+ else
15
+ self.extend(GenericPackage)
16
+ end
17
+ end
18
+
19
+ get_state
20
+ end
8
21
 
9
- @state['installed'] = installed?
10
- @state['version'] = version?
22
+ def provider
23
+ nil
11
24
  end
12
25
 
13
- ##############################
14
- #
15
- # Action methods (see Package2.sfp)
16
- #
17
- ##############################
26
+ module GenericPackage
27
+ def provider
28
+ nil
29
+ end
18
30
 
19
- @@lock = Mutex.new
31
+ def get_state
32
+ end
20
33
 
21
- def installed?
22
- name = @model['name'].to_s.strip
23
- if name.length > 0
24
- data = `/usr/bin/dpkg-query -W #{name} 2>/dev/null`.strip.chop.split(' ')
25
- (data[0].to_s == name)
26
- else
34
+ def install(p={})
27
35
  false
28
36
  end
29
- end
30
37
 
31
- def version?
32
- name = @model['name'].to_s.strip
33
- if name.length > 0
34
- data = `/usr/bin/dpkg-query -W #{name} 2>/dev/null`.strip.chop.split(' ')
35
- (data[0].to_s == name ? data[1] : "")
36
- else
37
- ""
38
+ def uninstall(p={})
39
+ false
38
40
  end
39
41
  end
40
42
 
41
- def install(p={})
42
- name = @model['name'].to_s.strip
43
- if name.length <= 0
44
- false
45
- elsif installed?
46
- true
47
- else
48
- shell "dpkg --configure -a; apt-get -y --purge autoremove"
49
- if shell "apt-get -y install #{name}"
50
- true
43
+ module TarPackage
44
+ def provider
45
+ 'tar'
46
+ end
47
+
48
+ def get_state
49
+ to_model
50
+ @state['installed'] = installed?
51
+ @state['version'] = version?
52
+ @state['home'] = (@state['installed'] ? @model['home'] : '')
53
+ end
54
+
55
+ def install(p={})
56
+ home = @model['home'].strip
57
+ return false if home.length <= 0
58
+
59
+ # create home directory if not exist
60
+ shell "rm -rf #{home} && mkdir -p #{home}" if not ::File.directory?(home)
61
+ if not ::File.directory?(home)
62
+ log.info "Target directory #{home} cannot be created."
63
+ return false
64
+ end
65
+
66
+ file = url.split('/').last
67
+ dest = "#{home}/#{file}"
68
+ download(url, dest)
69
+
70
+ # if downloaded file is not exist, then return false
71
+ if not ::File.exist?(dest)
72
+ log.error "Failed to download file from #{url}"
73
+ return false
74
+ end
75
+
76
+ # extract tar file, and then delete it
77
+ shell "cd #{home} && tar xvzf #{file} && rm -f #{file}"
78
+
79
+ basename = case ::File.extname(file)
80
+ when '.gz'
81
+ ::File.basename(file, '.tar.gz')
82
+ when '.tgz'
83
+ ::File.basename(file, '.tgz')
84
+ else
85
+ file
86
+ end
87
+
88
+ shell "bash -c 'cd #{home}/#{basename} && shopt -s dotglob && mv -f * .. && cd .. && rm -rf #{basename}'"
89
+
90
+ File.open("#{home}/#{signature}", 'w') { |f| f.write(Time.now.to_s) }
91
+
92
+ if not ::File.exist?(version_file)
93
+ File.open(version_file, 'w') { |f| f.write(@model['version']) }
94
+ end
95
+
96
+ installed?
97
+ end
98
+
99
+ def uninstall(p={})
100
+ shell "rm -rf #{@model['home']}" if ::File.exist?(@model['home'])
101
+ not installed?
102
+ end
103
+
104
+ def installed?
105
+ ::File.file?("#{@model['home']}/#{signature}")
106
+ end
107
+
108
+ def version?
109
+ if ::File.file?(version_file)
110
+ File.read(version_file).strip
51
111
  else
52
- shell "dpkg --configure -a; apt-get -y update && apt-get -y install #{name}"
112
+ ''
53
113
  end
54
114
  end
115
+
116
+ def version_file
117
+ "#{@model['home']}/VERSION"
118
+ end
119
+
120
+ def signature
121
+ ".nuri.package"
122
+ end
123
+
124
+ def url
125
+ "#{@model['source']}/#{@model['name']}-#{@model['version']}.tar.gz"
126
+ end
55
127
  end
56
128
 
57
- def uninstall(package)
58
- name = @model['name'].to_s.strip
59
- if name.length <= 0
60
- false
61
- elsif not installed?
62
- true
63
- else
64
- shell "dpkg --configure -a; apt-get -y --purge autoremove"
65
- shell "apt-get -y --purge remove #{name} && apt-get -y --purge autoremove && apt-get -y --purge autoremove"
129
+ module AptPackage
130
+ def provider
131
+ 'apt'
132
+ end
133
+
134
+ def get_state
135
+ to_model
136
+
137
+ @state['installed'] = installed?
138
+ @state['version'] = version?
139
+ end
140
+
141
+ ##############################
142
+ #
143
+ # Action methods (see Package2.sfp)
144
+ #
145
+ ##############################
146
+
147
+ @@lock = Mutex.new
148
+
149
+ def installed?(p={})
150
+ name = (p.has_key?(:name) ? p[:name] : @model['name'].to_s.strip)
151
+ if name.length > 0
152
+ data = `/usr/bin/dpkg-query -W #{name} 2>/dev/null`.strip.chop.split(' ')
153
+ (data[0].to_s == name)
154
+ else
155
+ false
156
+ end
66
157
  end
158
+
159
+ def version?(p={})
160
+ name = (p.has_key?(:name) ? p[:name] : @model['name'].to_s.strip)
161
+ if name.length > 0
162
+ data = `/usr/bin/dpkg-query -W #{name} 2>/dev/null`.strip.chop.split(' ')
163
+ (data[0].to_s == name ? data[1] : "")
164
+ else
165
+ ""
166
+ end
167
+ end
168
+
169
+ def install(p={})
170
+ return false if not repo_installed?
171
+
172
+ name = @model['name'].to_s.strip
173
+ if name.length <= 0
174
+ false
175
+ elsif installed?
176
+ true
177
+ else
178
+ shell "dpkg --configure -a; apt-get -y --purge autoremove"
179
+ if not shell "apt-get -y install #{name}"
180
+ shell "dpkg --configure -a; apt-get -y update && apt-get -y install #{name}"
181
+ end
182
+ installed?
183
+ end
184
+ end
185
+
186
+ def uninstall(package)
187
+ name = @model['name'].to_s.strip
188
+ if name.length <= 0
189
+ false
190
+ elsif not installed?
191
+ true
192
+ else
193
+ shell "dpkg --configure -a; apt-get -y --purge autoremove"
194
+ shell "apt-get -y --purge remove #{name} && apt-get -y --purge autoremove && apt-get -y --purge autoremove"
195
+ not installed?
196
+ end
197
+ end
198
+
199
+ ##############################
200
+ #
201
+ # Helper methods
202
+ #
203
+ ##############################
204
+
205
+ def repo_installed?
206
+ if @model['provider'] == 'apt'
207
+ case @model['source'].strip
208
+ when 'default', ''
209
+ true
210
+ else
211
+ apt_repo_installed?
212
+ end
213
+ else
214
+ true
215
+ end
216
+ end
217
+
218
+ def apt_repo_installed?
219
+ type, _ = @model['source'].split(':')
220
+ if type == 'ppa'
221
+ repos = `/bin/bash #{File.dirname(__FILE__)}/apt-repo-list.sh`.strip.split("\n")
222
+ if not repos.include?(@model['source'])
223
+ log.info "Missing APT repository: #{@model['source']}"
224
+ if not installed?({:name => 'python-software-properties'})
225
+ shell "apt-get install -y python-software-properties"
226
+ end
227
+ log.info "Adding APT repository: #{@model['source']}"
228
+ shell "add-apt-repository #{model['source']} && apt-get update"
229
+ else
230
+ false
231
+ end
232
+ else
233
+ false
234
+ end
235
+ end
236
+
67
237
  end
68
238
 
69
239
  =begin