dister 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,488 @@
1
+ require 'digest/md5'
2
+ require 'erb'
3
+
4
+ module Dister
5
+
6
+ class Core
7
+
8
+ attr_reader :options, :shell
9
+
10
+ APP_ROOT = File.expand_path('.')
11
+
12
+ # Connect to SUSE Studio and verify the user's credentials.
13
+ # Sets @options, @shell and @connection for further use.
14
+ def initialize
15
+ @options ||= Options.new
16
+ @shell = Thor::Shell::Basic.new
17
+ @connection = StudioApi::Connection.new(
18
+ @options.username,
19
+ @options.api_key,
20
+ @options.api_path,
21
+ :proxy => @options.proxy, # proxy can be nil
22
+ :timeout => (@options.timeout || 60) # default to 60s
23
+ )
24
+ # Try the connection once to determine whether credentials are correct.
25
+ @connection.api_version
26
+ StudioApi::Util.configure_studio_connection @connection
27
+
28
+ # Ensure app_name is stored for further use.
29
+ if @options.app_name.nil?
30
+ @options.app_name = APP_ROOT.split(/(\/|\\)/).last
31
+ end
32
+
33
+ true
34
+ rescue ActiveResource::UnauthorizedAccess
35
+ puts 'A connection to SUSE Studio could not be established.'
36
+ keep_trying = @shell.ask(
37
+ 'Would you like to re-enter your credentials and try again? (y/n)'
38
+ )
39
+ if keep_trying == 'y'
40
+ update_credentials
41
+ retry
42
+ else
43
+ abort('Exiting dister.')
44
+ end
45
+ end
46
+
47
+ # Creates a new appliance.
48
+ # Returns the new appliance.
49
+ def create_appliance(name, template, basesystem, arch)
50
+ match = check_template_and_basesystem_availability(template, basesystem)
51
+ exit 1 if match.nil?
52
+
53
+ @db_adapter = get_db_adapter
54
+ app = Utils::execute_printing_progress "Cloning appliance" do
55
+ StudioApi::Appliance.clone(match.appliance_id, {:name => name,
56
+ :arch => arch})
57
+ end
58
+ @options.appliance_id = app.id
59
+ ensure_devel_languages_ruby_extensions_repo_is_added
60
+ self.add_package "devel_C_C++"
61
+ self.add_package "devel_ruby"
62
+ self.add_package 'rubygem-bundler'
63
+ self.add_package 'rubygem-passenger-apache2'
64
+ unless @db_adapter.nil?
65
+ @db_adapter.packages.each do |p|
66
+ self.add_package p
67
+ end
68
+ end
69
+
70
+ Utils::execute_printing_progress "Uploading build scripts" do
71
+ upload_configurations_scripts
72
+ end
73
+ puts "SUSE Studio appliance successfull created:"
74
+ puts " #{app.edit_url}"
75
+ app
76
+ end
77
+
78
+ def build
79
+ verify_status
80
+ #TODO: build using another format
81
+ build = StudioApi::RunningBuild.create(
82
+ :appliance_id => @options.appliance_id,
83
+ :image_type => "oem"
84
+ )
85
+
86
+ build.reload
87
+ if build.state == "queued"
88
+ puts "Your build is queued. It will be automatically processed by "\
89
+ "SUSE Studio. You can keep waiting or you can exit from dister."
90
+ puts "Exiting from dister won't remove your build from the queue."
91
+ shell = Thor::Shell::Basic.new
92
+ keep_waiting = @shell.ask('Do you want to keep waiting (y/n)')
93
+ if keep_waiting == 'n'
94
+ exit 0
95
+ end
96
+
97
+ Utils::execute_printing_progress "Build queued..." do
98
+ while build.state == 'queued' do
99
+ sleep 5
100
+ build.reload
101
+ end
102
+ end
103
+ end
104
+
105
+ # build is no longer queued
106
+ pbar = ProgressBar.new "Building", 100
107
+
108
+ while not ['finished', 'error', 'failed', 'cancelled'].include?(build.state)
109
+ pbar.set build.percent.to_i
110
+ sleep 5
111
+ build.reload
112
+ end
113
+ pbar.finish
114
+ build.state == 'finished'
115
+ end
116
+
117
+ # Returns an app's appliance (or nil if none exist).
118
+ def appliance
119
+ if @appliance.nil?
120
+ begin
121
+ appliance_id = self.options.appliance_id
122
+ return nil if appliance_id.nil?
123
+ @appliance = StudioApi::Appliance.find(appliance_id.to_i)
124
+ rescue ActiveResource::BadRequest
125
+ self.options.appliance_id = nil
126
+ nil
127
+ end
128
+ else
129
+ @appliance
130
+ end
131
+ end
132
+
133
+ def builds
134
+ StudioApi::Build.find(:all, :params => {:appliance_id => @options.appliance_id})
135
+ end
136
+
137
+ def templates
138
+ reply = StudioApi::TemplateSet.find(:first, :conditions => {:name => "default"})
139
+ if reply.nil?
140
+ STDERR.puts "There is no default template set named 'default'"
141
+ STDERR.puts "Please contact SUSE Studio admin"
142
+ exit 1
143
+ else
144
+ return reply.template
145
+ end
146
+ end
147
+
148
+ def basesystems
149
+ templates.collect(&:basesystem).uniq
150
+ end
151
+
152
+ def check_template_and_basesystem_availability template, basesystem
153
+ available_templates = self.templates
154
+ match = available_templates.find do |t|
155
+ t.basesystem == basesystem && t.name.downcase.include?(template.downcase)
156
+ end
157
+
158
+ if match.nil?
159
+ STDERR.puts "The #{basesystem} doesn't have the #{template} template."
160
+ STDERR.puts "Available templates are:"
161
+ available_templates.find_all do |t|
162
+ t.basesystem.downcase == basesystem.downcase
163
+ end.each do |t|
164
+ STDERR.puts " - #{t.name}"
165
+ end
166
+ end
167
+ match
168
+ end
169
+
170
+ # Uploads a file identified by filename to a SuSE Studio Appliance
171
+ # options is an hash. it can have the following keys:
172
+ # - filename (optional) - The name of the file in the filesystem.
173
+ # - path (optional) - The path where the file will be stored.
174
+ # - owner (optional) - The owner of the file.
175
+ # - group (optional) - The group of the file.
176
+ # - permissions (optional) - The permissions of the file.
177
+ # - enabled (optional) - Used to enable/disable this file for the builds.
178
+ # - url (optional) - The url of the file to add from the internet (HTTP and FTP are supported) when using the web upload method
179
+ # This method returns true if the file has been successfully uploaded
180
+ def file_upload filename, upload_options={}
181
+ if File.exists? filename
182
+ # Delete existing (obsolete) file.
183
+ StudioApi::File.find(:all, :params => {
184
+ :appliance_id => self.options.appliance_id
185
+ }).select { |file|
186
+ file.path == (upload_options[:path] || '/') and file.filename == File.basename(filename)
187
+ }.each(&:destroy)
188
+ # Upload new file.
189
+ message = "Uploading #{filename} "
190
+ message += "(#{Utils.readable_file_size(File.size(filename),2)})"
191
+ Utils::execute_printing_progress message do
192
+ File.open(filename) do |file|
193
+ StudioApi::File.upload file, @options.appliance_id, upload_options
194
+ end
195
+ end
196
+ true
197
+ else
198
+ STDERR.puts "Cannot upload #{filename}, it doesn't exists."
199
+ false
200
+ end
201
+ end
202
+
203
+ # Use bundler to download and package all required gems for the app.
204
+ def package_gems
205
+ if !File.exists?("#{APP_ROOT}/Gemfile")
206
+ puts "Gemfile missing, cannot use bundler"
207
+ puts 'Either create a Gemfile or use "dister package add" command'
208
+ return
209
+ end
210
+
211
+ puts 'Packaging gems...'
212
+ system "cd #{APP_ROOT}"
213
+ system "rm -R vendor/cache" if File.exists?("#{APP_ROOT}/vendor/cache")
214
+ system 'bundle package'
215
+ puts "Done!"
216
+ end
217
+
218
+ # Creates a tarball that holds the application's source-files.
219
+ # Previously packaged versions get overwritten.
220
+ def package_app
221
+ puts 'Packaging application...'
222
+ package = ".dister/#{@options.app_name}_application.tar.gz"
223
+ system "rm #{package}" if File.exists?(package)
224
+ system "tar -czf .dister/#{@options.app_name}_application.tar.gz ../#{@options.app_name}/ --exclude=.dister &> /dev/null"
225
+ puts "Done!"
226
+ end
227
+
228
+ # Creates all relevant config files (e.g. apache.conf) for the appliance.
229
+ def package_config_files
230
+ filename = File.expand_path('../../templates/passenger.erb', __FILE__)
231
+ erb = ERB.new(File.read(filename))
232
+ config_content = erb.result(binding)
233
+
234
+ config_path = "#{APP_ROOT}/.dister/#{@options.app_name}_apache.conf"
235
+ FileUtils.rm(config_path, :force => true)
236
+ File.open(config_path, 'w') do |config_file|
237
+ config_file.write(config_content)
238
+ end
239
+
240
+ @db_adapter = get_db_adapter
241
+ unless @db_adapter.nil?
242
+ create_db_user_file = "#{APP_ROOT}/.dister/create_db_user.sql"
243
+ FileUtils.rm(create_db_user_file, :force => true)
244
+ File.open(create_db_user_file, 'w') do |file|
245
+ file.write(@db_adapter.create_user_cmd)
246
+ end
247
+ end
248
+ end
249
+
250
+ # Uploads the app tarball and the config file to the appliance.
251
+ def upload_bundled_files
252
+ upload_options = {:path => "/srv/www", :owner => 'root', :group => 'root'}
253
+ # Upload tarball.
254
+ self.file_upload("#{APP_ROOT}/.dister/#{@options.app_name}_application.tar.gz", upload_options)
255
+ # Upload config files to separate location.
256
+ upload_options[:path] = "/etc/apache2/vhosts.d"
257
+ self.file_upload("#{APP_ROOT}/.dister/#{@options.app_name}_apache.conf", upload_options)
258
+ # Upload db related files to separate location.
259
+ upload_options[:path] = "/root"
260
+ self.file_upload("#{APP_ROOT}/.dister/create_db_user.sql", upload_options)
261
+ end
262
+
263
+ def add_package package
264
+ appliance_basesystem = appliance.basesystem
265
+ result = appliance.search_software(package)#.find{|s| s.name == package }
266
+ #TODO: better handling
267
+ #Blocked by bnc#
268
+ if result.empty? #it is not found in available repos
269
+ puts "'#{package}' has not been found in the repositories currently "\
270
+ "added to your appliance."
271
+ keep_trying = @shell.ask('Would you like to search for this package '\
272
+ 'inside other repositories? (y/n)')
273
+ if keep_trying == 'y'
274
+ matches = appliance.search_software(package, :all_repos => true)\
275
+ .find_all { |s| s.name == package }
276
+ repositories = matches.map do |r|
277
+ StudioApi::Repository.find r.repository_id
278
+ end.find_all{|r| r.base_system == appliance_basesystem}
279
+
280
+ if repositories.empty?
281
+ puts "Cannot find #{package}, please look at this page: "
282
+ puts URI.encode "http://software.opensuse.org/search?p=1&"\
283
+ "baseproject=ALL&q=#{package}"
284
+ else
285
+ puts "Package #{package} can be installed from one of the "\
286
+ "following repositories:"
287
+ repositories.each_with_index do |repo, index|
288
+ puts "#{index+1} - #{repo.name} (#{repo.base_url})"
289
+ end
290
+ puts "#{repositories.size+1} - None of them."
291
+ begin
292
+ choice = @shell.ask("Which repo do you want to use? "\
293
+ "[1-#{repositories.size+1}]")
294
+ end while (choice.to_i > (repositories.size+1))
295
+ if (choice.to_i == (repositories.size+1))
296
+ abort("Package not added.")
297
+ else
298
+ repo_id = repositories[choice.to_i-1].id
299
+ end
300
+ appliance.add_repository repo_id
301
+ end
302
+ else
303
+ exit 0
304
+ end
305
+ # add repo which contain samba
306
+ #appliance.add_repository result.repository_id
307
+ end
308
+ Utils::execute_printing_progress "Adding #{package} package" do
309
+ appliance.add_package(package)
310
+ end
311
+ end
312
+
313
+ def rm_package package
314
+ Utils::execute_printing_progress "Removing #{package} package" do
315
+ appliance.remove_package(package)
316
+ end
317
+ end
318
+
319
+ # Uploads our configuration scripts
320
+ def upload_configurations_scripts
321
+ rails_root = "/srv/www/#{@options.app_name}"
322
+
323
+ filename = File.expand_path('../../templates/boot_script.erb', __FILE__)
324
+ erb = ERB.new(File.read(filename))
325
+ boot_script = erb.result(binding)
326
+
327
+ filename = File.expand_path('../../templates/build_script.erb', __FILE__)
328
+ erb = ERB.new(File.read(filename))
329
+ build_script = erb.result(binding)
330
+
331
+ conf = appliance.configuration
332
+ conf.scripts.boot.script = boot_script
333
+ conf.scripts.boot.enabled = true
334
+
335
+ conf.scripts.build.script = build_script
336
+ conf.scripts.build.enabled = true
337
+ conf.save
338
+ true
339
+ end
340
+
341
+ # Asks Studio to mirror a repository.
342
+ # Returns a StudioApi::Repository object
343
+ def import_repository url, name
344
+ StudioApi::Repository.import url, name
345
+ end
346
+
347
+ def ensure_devel_languages_ruby_extensions_repo_is_added
348
+ name = "devel:language:ruby:extensions"
349
+ url = "http://download.opensuse.org/repositories/devel:/languages:/ruby:/extensions/"
350
+
351
+ case appliance.basesystem
352
+ when "11.1"
353
+ url += "openSUSE_11.1"
354
+ name += " 11.1"
355
+ when "11.2"
356
+ url += "openSUSE_11.2"
357
+ name += " 11.2"
358
+ when "11.3"
359
+ url += "openSUSE_11.3"
360
+ name += " 11.3"
361
+ when "11.4"
362
+ url += "openSUSE_11.4"
363
+ name += " 11.4"
364
+ when "SLED10_SP2", "SLED10_SP3", "SLES10_SP2", "SLES10_SP3"
365
+ url += "SLE_10/"
366
+ name += " SLE10"
367
+ when "SLED11", "SLES11"
368
+ url += "SLE_11"
369
+ name += " SLE 11"
370
+ when "SLED11_SP1", "SLES11_SP1", "SLES11_SP1_VMware"
371
+ url += "SLE_11_SP1"
372
+ name += " SLE11 SP1"
373
+ else
374
+ STDERR.puts "#{appliance.basesystem}: unknown base system"
375
+ exit 1
376
+ end
377
+
378
+ Utils::execute_printing_progress "Adding #{name} repository" do
379
+ repos = StudioApi::Repository.find(:all, :params => {:filter => url.downcase})
380
+ if repos.size > 0
381
+ repo = repos.first
382
+ else
383
+ repo = import_repository url, name
384
+ end
385
+ appliance.add_repository repo.id
386
+ end
387
+ end
388
+
389
+ # Make sure the appliance doesn't have conflicts.
390
+ # In this case an error message is shown and the program halts.
391
+ def verify_status
392
+ Utils::execute_printing_progress "Verifying appliance status" do
393
+ if appliance.status.state != "ok"
394
+ message = "Appliance is not OK - #{appliance.status.issues.inspect}"
395
+ message += "\nVisit #{appliance.edit_url} to manually fix the issue."
396
+ raise message
397
+ end
398
+ end
399
+ end
400
+
401
+ def testdrive(build_set)
402
+ build = build_set[0] # for now we just take the first available build
403
+ testdrive = Utils::execute_printing_progress "Starting testdrive" do
404
+ begin
405
+ StudioApi::Testdrive.create(:build_id => build.id)
406
+ rescue
407
+ STDERR.puts $!
408
+ exit 1
409
+ end
410
+ end
411
+ # NOTE can't get http to work, so lets just provide vnc info for now
412
+ puts "Connect to your testdrive using VNC:"
413
+ vnc = testdrive.server.vnc
414
+ puts "Server: #{vnc.host}:#{vnc.port}"
415
+ puts "Password: #{vnc.password}"
416
+ end
417
+
418
+ def download(build_set)
419
+ # Choose the build(s) to download.
420
+ to_download = []
421
+ if build_set.size == 1
422
+ to_download << build_set.first
423
+ else
424
+ build_set.each_with_index do |build, index|
425
+ puts "#{index+1}) #{build.to_s}"
426
+ end
427
+ puts "#{build_set.size+1}) All of them."
428
+ puts "#{build_set.size+2}) None."
429
+ begin
430
+ choice = @shell.ask "Which appliance do you want to download? [1-#{build_set.size+1}]"
431
+ end while (choice.to_i > (build_set.size+2))
432
+ if choice.to_i == (build_set.size+2)
433
+ # none selected
434
+ exit 0
435
+ elsif choice.to_i == (build_set.size+1)
436
+ # all selected
437
+ to_download = build_set
438
+ else
439
+ to_download << build_set[choice.to_i-1]
440
+ end
441
+ end
442
+ # Download selected builds.
443
+ to_download.each do |b|
444
+ puts "Going to download #{b.to_s}"
445
+ d = Downloader.new(b.download_url.sub("https:", "http:"),"Downloading")
446
+ if File.exists? d.filename
447
+ overwrite = @shell.ask("Do you want to overwrite file #{d.filename}? (y/n)")
448
+ exit 0 if overwrite == 'n'
449
+ end
450
+ begin
451
+ d.start
452
+ Utils::execute_printing_progress "Calculating md5sum" do
453
+ digest = Digest::MD5.file d.filename
454
+ raise "digest check not passed" if digest.to_s != b.checksum.md5
455
+ end
456
+ rescue
457
+ STDOUT.puts
458
+ STDERR.puts
459
+ STDERR.flush
460
+ STDERR.puts $!
461
+ exit 1
462
+ end
463
+ end
464
+ end
465
+
466
+ private
467
+
468
+ # Updates a user's credentials and stores them inside the global options file.
469
+ def update_credentials
470
+ puts 'Please enter your SUSE Studio credentials (https://susestudio.com/user/show_api_key).'
471
+ @options.username = @shell.ask("Username:\t")
472
+ @options.api_key = @shell.ask("API key:\t")
473
+ end
474
+
475
+ def get_db_adapter
476
+ db_config_file = "#{APP_ROOT}/config/database.yml"
477
+ if !File.exists?(db_config_file)
478
+ print "Cannot find database configuration file, "\
479
+ "database handling disabled."
480
+ shell = Thor::Shell::Color.new
481
+ shell.say_status("[WARN]", "", :YELLOW)
482
+ nil
483
+ else
484
+ Dister::DbAdapter.new db_config_file
485
+ end
486
+ end
487
+ end
488
+ end