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,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
@@ -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,5 @@
1
+ module Dister
2
+
3
+ VERSION = "0.1.0"
4
+
5
+ end
@@ -0,0 +1,7 @@
1
+ module StudioApi
2
+ class Build
3
+ def to_s
4
+ "version #{self.version}, #{self.image_type} format"
5
+ end
6
+ end
7
+ 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>
@@ -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