nuri 0.5.3 → 0.5.4

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.
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