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.
- data/.gitignore +5 -0
- data/Gemfile +9 -0
- data/LICENSE +21 -0
- data/README.rdoc +86 -0
- data/Rakefile +26 -0
- data/bin/dister +3 -0
- data/dister.gemspec +30 -0
- data/lib/adapters/mysql.yml +13 -0
- data/lib/adapters/mysql2.yml +13 -0
- data/lib/adapters/postgresql.yml +12 -0
- data/lib/adapters/sqlite3.yml +7 -0
- data/lib/dister.rb +21 -0
- data/lib/dister/cli.rb +198 -0
- data/lib/dister/core.rb +488 -0
- data/lib/dister/db_adapter.rb +57 -0
- data/lib/dister/downloader.rb +58 -0
- data/lib/dister/options.rb +113 -0
- data/lib/dister/utils.rb +46 -0
- data/lib/dister/version.rb +5 -0
- data/lib/studio_api/build.rb +7 -0
- data/lib/templates/boot_script.erb +37 -0
- data/lib/templates/build_script.erb +22 -0
- data/lib/templates/passenger.erb +13 -0
- data/test/cli_test.rb +141 -0
- data/test/core_test.rb +79 -0
- data/test/db_adapter_test.rb +82 -0
- data/test/fixtures/supported_database.yml +17 -0
- data/test/fixtures/templates.yml +231 -0
- data/test/fixtures/unsupported_database.yml +17 -0
- data/test/options_test.rb +128 -0
- data/test/test_helper.rb +36 -0
- metadata +235 -0
data/lib/dister/core.rb
ADDED
@@ -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
|