dister 0.1.0

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