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
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
module Dister
|
4
|
+
class DbAdapter
|
5
|
+
|
6
|
+
def initialize db_config_file, dump=nil
|
7
|
+
config = YAML.load_file(db_config_file)
|
8
|
+
if !config.has_key?("production")
|
9
|
+
STDERR.puts "There's no configuration for the production environment"
|
10
|
+
end
|
11
|
+
|
12
|
+
@adapter = config["production"]["adapter"]
|
13
|
+
@user = config["production"]["username"]
|
14
|
+
@password = config["production"]["password"]
|
15
|
+
@dbname = config["production"]["adapter"]
|
16
|
+
@dump = dump
|
17
|
+
|
18
|
+
filename = File.expand_path("../../adapters/#{@adapter}.yml", __FILE__)
|
19
|
+
raise "There's no adapter for #{@adapter}" if !File.exists?(filename)
|
20
|
+
|
21
|
+
@adapter_config = YAML.load_file(filename)
|
22
|
+
end
|
23
|
+
|
24
|
+
def has_dump?
|
25
|
+
return false if @dump.nil?
|
26
|
+
return File.exists? @dump
|
27
|
+
end
|
28
|
+
|
29
|
+
def cmdline_tool
|
30
|
+
@adapter_config["cmdline_tool"]
|
31
|
+
end
|
32
|
+
|
33
|
+
def packages
|
34
|
+
@adapter_config["packages"]
|
35
|
+
end
|
36
|
+
|
37
|
+
def daemon_name
|
38
|
+
@adapter_config["daemon_name"]
|
39
|
+
end
|
40
|
+
|
41
|
+
def create_user_cmd
|
42
|
+
compile_cmd @adapter_config["create_user_cmd"]
|
43
|
+
end
|
44
|
+
|
45
|
+
def restore_dump_cmd
|
46
|
+
compile_cmd @adapter_config["restore_dump_cmd"]
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
def compile_cmd cmd
|
51
|
+
return "" if cmd.nil?
|
52
|
+
|
53
|
+
erb = ERB.new cmd
|
54
|
+
erb.result(binding)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'ruby-debug'
|
2
|
+
module Dister
|
3
|
+
class Downloader
|
4
|
+
attr_reader :filename
|
5
|
+
|
6
|
+
|
7
|
+
def initialize url, message
|
8
|
+
@filename = File.basename(url)
|
9
|
+
@message = message
|
10
|
+
|
11
|
+
# setup curl
|
12
|
+
@curl = Curl::Easy.new
|
13
|
+
@curl.url = url
|
14
|
+
@curl.follow_location = true
|
15
|
+
|
16
|
+
@curl.on_body { |data| self.on_body(data); data.size }
|
17
|
+
@curl.on_complete { |data| self.on_complete }
|
18
|
+
@curl.on_failure { |data| self.on_failure }
|
19
|
+
@curl.on_progress do |dl_total, dl_now, ul_total, ul_now|
|
20
|
+
self.on_progress(dl_now, dl_total, @curl.download_speed, @curl.total_time)
|
21
|
+
true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def start
|
26
|
+
@file = File.open(@filename, "wb")
|
27
|
+
@pbar = ProgressBar.new(@message, 100)
|
28
|
+
@curl.perform
|
29
|
+
end
|
30
|
+
|
31
|
+
def on_body(data)
|
32
|
+
@file.write(data)
|
33
|
+
end
|
34
|
+
|
35
|
+
def on_progress(downloaded_size, total_size, download_speed, downloading_time)
|
36
|
+
if total_size > 0
|
37
|
+
@pbar.set(downloaded_size / total_size * 100)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def on_complete
|
42
|
+
@pbar.finish
|
43
|
+
@file.close
|
44
|
+
end
|
45
|
+
|
46
|
+
def on_failure
|
47
|
+
begin
|
48
|
+
unless code == 'Curl::Err::CurlOKNo error'
|
49
|
+
@pbar.finish
|
50
|
+
STDOUT.flush
|
51
|
+
raise "Download failed with error code: #{code}"
|
52
|
+
end
|
53
|
+
ensure
|
54
|
+
@file.close
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Dister
|
4
|
+
class Options
|
5
|
+
|
6
|
+
SUSE_STUDIO_DOT_COM_API_PATH = 'https://susestudio.com/api/v2/user'
|
7
|
+
GLOBAL_PATH = "#{File.expand_path('~')}/.dister"
|
8
|
+
LOCAL_PATH = "#{Dister::Core::APP_ROOT}/.dister/options.yml"
|
9
|
+
|
10
|
+
attr_reader :use_only_local
|
11
|
+
|
12
|
+
# Read options from file.
|
13
|
+
def initialize use_only_local=false
|
14
|
+
@use_only_local = use_only_local
|
15
|
+
reload
|
16
|
+
end
|
17
|
+
|
18
|
+
# Provides setter and getter for all options.
|
19
|
+
def method_missing(method, *args)
|
20
|
+
method_name = method.to_s
|
21
|
+
if (method_name =~ /=$/).nil?
|
22
|
+
# Getter
|
23
|
+
provide[method_name]
|
24
|
+
else
|
25
|
+
# Setter
|
26
|
+
store(method_name[0..-2], args.first)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Read @global and @local option files.
|
31
|
+
def reload
|
32
|
+
if @use_only_local
|
33
|
+
@global = {}
|
34
|
+
else
|
35
|
+
# Global options hold the user's credentials to access SUSE Studio.
|
36
|
+
# They are stored inside the user's home directory.
|
37
|
+
@global = read_options_from_file(GLOBAL_PATH)
|
38
|
+
|
39
|
+
# make sure the default api path is available
|
40
|
+
unless @global.has_key? 'api_path'
|
41
|
+
@global['api_path'] = SUSE_STUDIO_DOT_COM_API_PATH
|
42
|
+
end
|
43
|
+
end
|
44
|
+
# Local options hold application specific data (e.g. appliance_id)
|
45
|
+
# They are stored inside the application's root directory.
|
46
|
+
@local = read_options_from_file(LOCAL_PATH)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# Reads from global or local options file and returns an options hash.
|
52
|
+
def read_options_from_file(file_path)
|
53
|
+
values_hash = YAML.load_file(file_path)
|
54
|
+
# In the unlikely case that the options file is empty, return an empty hash.
|
55
|
+
values_hash ? values_hash : {}
|
56
|
+
rescue Errno::ENOENT
|
57
|
+
# File does not exist.
|
58
|
+
options_dir = File.dirname(file_path)
|
59
|
+
FileUtils.mkdir_p(options_dir) unless File.directory?(options_dir)
|
60
|
+
File.new(file_path, 'w')
|
61
|
+
retry
|
62
|
+
end
|
63
|
+
|
64
|
+
# Writes an options_hash back to a specified options file.
|
65
|
+
def write_options_to_file(options_hash, file_path)
|
66
|
+
File.open(file_path, 'w') do |out|
|
67
|
+
YAML.dump(options_hash, out)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Determines to which file an option gets written.
|
72
|
+
def determine_options_file(option_key)
|
73
|
+
return 'local' if @use_only_local
|
74
|
+
|
75
|
+
# Search in local options first, since they override global options.
|
76
|
+
case option_key
|
77
|
+
when @local.keys.include?(option_key) then 'local'
|
78
|
+
when @global.keys.include?(option_key) then 'global'
|
79
|
+
else
|
80
|
+
if %w(username api_key api_path).include?(option_key)
|
81
|
+
# Credentials are stored globally per default.
|
82
|
+
'global'
|
83
|
+
else
|
84
|
+
# New options get stored locally.
|
85
|
+
'local'
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Stores a specified option_key inside its originating options file.
|
91
|
+
def store(option_key, option_value)
|
92
|
+
if determine_options_file(option_key) == 'local'
|
93
|
+
@local[option_key] = option_value
|
94
|
+
options_hash = @local
|
95
|
+
file_path = LOCAL_PATH
|
96
|
+
else
|
97
|
+
@global[option_key] = option_value
|
98
|
+
options_hash = @global
|
99
|
+
file_path = GLOBAL_PATH
|
100
|
+
end
|
101
|
+
write_options_to_file(options_hash, file_path)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns a hash consisting of both global and local options.
|
105
|
+
# All options can be read through this method.
|
106
|
+
# NOTE: Local options override global options.
|
107
|
+
def provide
|
108
|
+
@global.merge(@local)
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
data/lib/dister/utils.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module Dister
|
2
|
+
module Utils
|
3
|
+
module_function
|
4
|
+
# Shows message and prints a dot per second until the block code
|
5
|
+
# terminates its execution.
|
6
|
+
# Exceptions raised by the block are displayed and program exists with
|
7
|
+
# error status 1.
|
8
|
+
def execute_printing_progress message
|
9
|
+
t = Thread.new do
|
10
|
+
print "#{message}"
|
11
|
+
while(true) do
|
12
|
+
print "."
|
13
|
+
STDOUT.flush
|
14
|
+
sleep 1
|
15
|
+
end
|
16
|
+
end
|
17
|
+
shell = Thor::Shell::Color.new
|
18
|
+
begin
|
19
|
+
ret = yield
|
20
|
+
t.kill if t.alive?
|
21
|
+
shell.say_status "[DONE]", "", :GREEN
|
22
|
+
return ret
|
23
|
+
rescue
|
24
|
+
t.kill if t.alive?
|
25
|
+
shell.say_status "[ERROR]", $!, :RED
|
26
|
+
exit 1
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
GIGA_SIZE = 1073741824.0
|
31
|
+
MEGA_SIZE = 1048576.0
|
32
|
+
KILO_SIZE = 1024.0
|
33
|
+
|
34
|
+
# Return the file size with a readable style.
|
35
|
+
def readable_file_size(size, precision)
|
36
|
+
case
|
37
|
+
when size == 1 then "1 Byte"
|
38
|
+
when size < KILO_SIZE then "%d Bytes" % size
|
39
|
+
when size < MEGA_SIZE then "%.#{precision}f KB" % (size / KILO_SIZE)
|
40
|
+
when size < GIGA_SIZE then "%.#{precision}f MB" % (size / MEGA_SIZE)
|
41
|
+
else "%.#{precision}f GB" % (size / GIGA_SIZE)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
#
|
3
|
+
# This script is executed whenever your appliance boots. Here you can add
|
4
|
+
# commands to be executed before the system enters the first runlevel. This
|
5
|
+
# could include loading kernel modules, starting daemons that aren't managed
|
6
|
+
# by init files, asking questions at the console, etc.
|
7
|
+
#
|
8
|
+
# The 'kiwi_type' variable will contain the format of the appliance (oem =
|
9
|
+
# disk image, vmx = VMware, iso = CD/DVD, xen = Xen).
|
10
|
+
#
|
11
|
+
|
12
|
+
# read in some variables
|
13
|
+
. /studio/profile
|
14
|
+
|
15
|
+
if [ -f /etc/init.d/suse_studio_firstboot ]
|
16
|
+
then
|
17
|
+
# Put commands to be run on the first boot of your appliance here
|
18
|
+
echo "Running SUSE Studio first boot script..."
|
19
|
+
cd <%= rails_root %>
|
20
|
+
bundle install --local
|
21
|
+
<% unless @db_adapter.nil? %>
|
22
|
+
# create db user
|
23
|
+
if [ -f /root/create_db_user.sql ]
|
24
|
+
then
|
25
|
+
/etc/init.d/<%= @db_adapter.daemon_name %> start
|
26
|
+
<%= @db_adapter.cmdline_tool %> < /root/create_db_user.sql
|
27
|
+
fi
|
28
|
+
|
29
|
+
<% if !@db_adapter.has_dump? %>
|
30
|
+
echo "Loading database schema"
|
31
|
+
RAILS_ENV=production rake db:create
|
32
|
+
RAILS_ENV=production rake db:schema:load
|
33
|
+
<% else %>
|
34
|
+
<%= @db_adapter.restore_dump_cmd %>
|
35
|
+
<% end %>
|
36
|
+
<% end %>
|
37
|
+
fi
|
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/bin/bash -e
|
2
|
+
#
|
3
|
+
# This script is executed at the end of appliance creation. Here you can do
|
4
|
+
# one-time actions to modify your appliance before it is ever used, like
|
5
|
+
# removing files and directories to make it smaller, creating symlinks,
|
6
|
+
# generating indexes, etc.
|
7
|
+
#
|
8
|
+
# The 'kiwi_type' variable will contain the format of the appliance (oem =
|
9
|
+
# disk image, vmx = VMware, iso = CD/DVD, xen = Xen).
|
10
|
+
#
|
11
|
+
|
12
|
+
# read in some variables
|
13
|
+
. /studio/profile
|
14
|
+
|
15
|
+
# add passenger module to apache
|
16
|
+
sed -i.bak 's/^\(APACHE_MODULES=.*\)"/\1 passenger"/' /etc/sysconfig/apache2
|
17
|
+
|
18
|
+
# enable services
|
19
|
+
insserv /etc/init.d/apache2
|
20
|
+
<% unless @db_adapter.nil? %>
|
21
|
+
insserv /etc/init.d/<%= @db_adapter.daemon_name %>
|
22
|
+
<% end %>
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<VirtualHost *:80>
|
2
|
+
PassengerEnabled on
|
3
|
+
RailsEnv production
|
4
|
+
ServerAdmin you@example.com
|
5
|
+
DocumentRoot /srv/www/<%= @options.app_name %>/public
|
6
|
+
<Directory /srv/www/<%= @options.app_name %>/public>
|
7
|
+
Options FollowSymlinks
|
8
|
+
Allow from all
|
9
|
+
</Directory>
|
10
|
+
ErrorLog /var/log/apache2/error.log
|
11
|
+
LogLevel warn
|
12
|
+
CustomLog /var/log/apache2/access.log combined
|
13
|
+
</VirtualHost>
|
data/test/cli_test.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
if !defined? FakeTemplates
|
4
|
+
Struct.new("FakeTemplate", :name, :basesystem, :appliance_id, :description)
|
5
|
+
|
6
|
+
FakeTemplates = []
|
7
|
+
YAML.load_file(File.expand_path('../fixtures/templates.yml', __FILE__)).each do |item|
|
8
|
+
info = item.last
|
9
|
+
FakeTemplates << Struct::FakeTemplate.new(info["name"],
|
10
|
+
info["basesystem"],
|
11
|
+
info["appliance_id"],
|
12
|
+
info["description"])
|
13
|
+
end
|
14
|
+
FakeTemplates.freeze
|
15
|
+
end
|
16
|
+
|
17
|
+
class CliTest < Test::Unit::TestCase
|
18
|
+
|
19
|
+
context "Run with FakeFS" do
|
20
|
+
|
21
|
+
setup do
|
22
|
+
FakeFS.activate!
|
23
|
+
end
|
24
|
+
|
25
|
+
teardown do
|
26
|
+
FakeFS.deactivate!
|
27
|
+
end
|
28
|
+
|
29
|
+
context "no parameter passed" do
|
30
|
+
|
31
|
+
setup do
|
32
|
+
@out = capture(:stdout) { Dister::Cli.start() }
|
33
|
+
end
|
34
|
+
|
35
|
+
should "show help message" do
|
36
|
+
assert @out.include?("Tasks:")
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
context "wrong param" do
|
42
|
+
|
43
|
+
setup do
|
44
|
+
@out = capture(:stdout) do
|
45
|
+
@err = capture(:stderr) { Dister::Cli.start(['foo']) }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
should "show help message" do
|
50
|
+
assert_equal 'Could not find task "foo".', @err.chomp
|
51
|
+
assert @out.empty?
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
context "config handling" do
|
57
|
+
setup do
|
58
|
+
FakeFS.activate!
|
59
|
+
end
|
60
|
+
|
61
|
+
teardown do
|
62
|
+
FakeFS.deactivate!
|
63
|
+
end
|
64
|
+
|
65
|
+
should "write to local file" do
|
66
|
+
FileUtils.rm_rf Dister::Options::GLOBAL_PATH
|
67
|
+
FileUtils.rm_rf Dister::Options::LOCAL_PATH
|
68
|
+
@out = capture(:stdout) do
|
69
|
+
Dister::Cli.start(['config', 'foo', 'foo_value', '--local'])
|
70
|
+
end
|
71
|
+
assert !File.exists?(Dister::Options::GLOBAL_PATH)
|
72
|
+
assert File.exists?(Dister::Options::LOCAL_PATH)
|
73
|
+
options = Dister::Options.new(true)
|
74
|
+
assert_equal "foo_value", options.foo
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
context "creating a new appliance" do
|
80
|
+
|
81
|
+
setup do
|
82
|
+
Dister::Core.any_instance.stubs(:puts)
|
83
|
+
Dister::Core.any_instance.stubs(:templates).returns(FakeTemplates)
|
84
|
+
basesystems = ["11.1", "SLED10_SP2", "SLES10_SP2", "SLED11", "SLES11",
|
85
|
+
"11.2", "SLES11_SP1", "SLED11_SP1", "11.3", "SLED10_SP3",
|
86
|
+
"SLES10_SP3", "SLES11_SP1_VMware"]
|
87
|
+
Dister::Core.any_instance.stubs(:basesystems).returns(basesystems)
|
88
|
+
end
|
89
|
+
|
90
|
+
should "refuse invalid archs" do
|
91
|
+
STDERR.stubs(:puts)
|
92
|
+
assert_raise SystemExit do
|
93
|
+
Dister::Cli.start(['create', 'foo','--arch', 'ppc'])
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
should "accept valid archs" do
|
98
|
+
fake_app = mock()
|
99
|
+
fake_app.stubs(:edit_url).returns("http://susestudio.com")
|
100
|
+
Dister::Core.any_instance.expects(:create_appliance).returns(fake_app)
|
101
|
+
assert_nothing_raised do
|
102
|
+
Dister::Cli.start(['create', 'foo','--arch', 'x86_64'])
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
should "guess latest version of openSUSE if no base system is specified" do
|
107
|
+
fake_app = mock()
|
108
|
+
fake_app.stubs(:edit_url).returns("http://susestudio.com")
|
109
|
+
Dister::Core.any_instance.expects(:create_appliance).\
|
110
|
+
with("foo", "JeOS", "11.3", "i686").\
|
111
|
+
returns(fake_app)
|
112
|
+
assert_nothing_raised do
|
113
|
+
Dister::Cli.start(['create', 'foo'])
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
should "detect bad combination of template and basesystem" do
|
118
|
+
STDERR.stubs(:puts)
|
119
|
+
assert_raise(SystemExit) do
|
120
|
+
Dister::Cli.start(['create', 'foo', "--template", "jeos",
|
121
|
+
"--basesystem", "SLES11_SP1_VMware"])
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
context "When executing 'dister bundle' it" do
|
128
|
+
|
129
|
+
should 'package all required gems' do
|
130
|
+
FileUtils.stubs(:rm).returns(:true)
|
131
|
+
Dister::Core.any_instance.expects(:package_gems).once
|
132
|
+
Dister::Core.any_instance.expects(:package_config_files).once
|
133
|
+
Dister::Core.any_instance.expects(:package_app).once
|
134
|
+
Dister::Cli.start ['bundle']
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|