ssc 0.1 → 0.2.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,102 @@
1
+ require 'tempfile'
2
+
3
+ module SSC
4
+ module Handler
5
+ class OverlayFile < Base
6
+
7
+
8
+ no_tasks do
9
+ cattr_reader :local_source
10
+ @@local_source= 'files/'
11
+ end
12
+
13
+ # must be run in appliance directory
14
+ # takes the following argument:
15
+ # file_path => (relative positions("." and "..") allowed and ~ for home directory allowed)
16
+ # takes the following options:
17
+ # --path="/path/to/file_directory/" => optional (by default it is the path of the file on the local system)
18
+ # --name="file_name" => optional (by default it is the name of the file on the local system)
19
+ # --permissions="0766" => optional (default: 0755)
20
+ # --owner="user" => optional (default: root)
21
+ desc 'file create PATH', 'create a new overlay file'
22
+ require_appliance_id
23
+ allow_remote_option
24
+ method_option :path, :type => :string, :default => ''
25
+ method_option :name, :type => :string, :default => ''
26
+ method_option :permissions, :type => :string, :default => '0755'
27
+ method_option :owner, :type => :string, :default => 'root'
28
+ def create(path)
29
+ absolute_path= File.expand_path(path)
30
+ optional_file_params= {:permissions => options.permissions,
31
+ :owner => options.owner}
32
+ file_dir, file_name= File.split(absolute_path)
33
+ file_dir = options.path == '' ? file_dir : options.path
34
+ file_name = options.name == '' ? file_name : options.name
35
+ id= nil
36
+ if options.remote?
37
+ require_appliance do |appliance|
38
+ file_params= ({:path => file_dir, :filename => file_name})
39
+ file_params.merge!(optional_file_params)
40
+ File.open(absolute_path) do |file|
41
+ file= StudioApi::File.upload(file, appliance.id, file_params)
42
+ id= file.id.to_i
43
+ end
44
+ say "Overlay file saved. Id: #{id}"
45
+ end
46
+ end
47
+ local_copy= initiate_file(file_dir, file_name, id)
48
+ say "Created #{local_copy}"
49
+ end
50
+
51
+ desc 'file show FILE_NAME', 'show the contents of the file'
52
+ require_appliance_id
53
+ allow_remote_option
54
+ def show(file_name)
55
+ if options.remote?
56
+ id= find_file_id(file_name)
57
+ response= StudioApi::File.find(id)
58
+ say response.content
59
+ else
60
+ say show_file(file_name)
61
+ end
62
+ end
63
+
64
+ desc 'file diff FILE_NAME', 'show the diff of the remote file and the local one'
65
+ require_appliance_id
66
+ def diff(file_name)
67
+ begin
68
+ id= find_file_id(file_name)
69
+ file_content= StudioApi::File.find(id).content
70
+ rescue
71
+ say "unable to connect or not in appliance directory", :red
72
+ end
73
+
74
+ begin
75
+ tempfile=Tempfile.new('ssc_file')
76
+ tempfile.write(file_content)
77
+ say find_diff(tempfile.path, full_local_file_path(file_name))
78
+ tempfile.close; tempfile.unlink
79
+ rescue Errno::ENOENT
80
+ say "diff not installed", :red
81
+ end
82
+ end
83
+
84
+ desc 'file list', 'show all overlay files'
85
+ require_appliance_id
86
+ allow_remote_option
87
+ def list
88
+ require_appliance do |appliance|
89
+ out= if options.remote? || file_list_empty?
90
+ response= StudioApi::File.find(:all, :params => {:appliance_id => appliance.id})
91
+ response.collect do |file|
92
+ {file.filename => {"id" => id, "path" => file.path}}
93
+ end
94
+ else
95
+ list_local_files
96
+ end
97
+ say out.to_yaml
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,79 @@
1
+ require 'yaml'
2
+
3
+ module SSC
4
+ module Handler
5
+ module Helper
6
+
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+ base.class_eval do
10
+
11
+ end
12
+ base.send :include, InstanceMethods
13
+ end
14
+
15
+ module ClassMethods
16
+ def require_authorization
17
+ config= get_config
18
+ method_option :username, :type => :string, :required => true,
19
+ :default => config["username"]
20
+ method_option :password, :type => :string, :required => true,
21
+ :default => config["password"]
22
+ method_option :proxy, :type => :string
23
+ method_option :timeout, :type => :string
24
+ end
25
+
26
+ def require_appliance_id
27
+ require_authorization
28
+ config= get_config
29
+ method_option :appliance_id, :type => :numeric, :required => true,
30
+ :default => config["appliance_id"]
31
+ end
32
+
33
+ def allow_remote_option
34
+ method_option :remote, :type => :boolean, :default => false
35
+ end
36
+
37
+ def get_config
38
+ begin
39
+ YAML::load File.read(File.join('.', '.sscrc'))
40
+ rescue Errno::ENOENT
41
+ return {'username' => nil, 'password' => nil, 'appliance_id' => nil}
42
+ end
43
+ end
44
+ end
45
+
46
+ module InstanceMethods
47
+
48
+ # Establish connection to Suse Studio with username, password
49
+ def connect(user, pass, connection_options)
50
+ @connection= StudioApi::Connection.new(user, pass, self.class::API_URL, connection_options)
51
+ StudioApi::Util.configure_studio_connection @connection
52
+ end
53
+
54
+ def filter_options(options, keys)
55
+ keys.inject({}) do |out, key|
56
+ (options.respond_to?(key) && options.send(key)) ? out.merge({ key => options.send(key) }) : out
57
+ end
58
+ end
59
+
60
+ def say_array(array, color= nil)
61
+ # NOTE
62
+ # Included for those methods that still return arrays for printing
63
+ # Can be removed eventually
64
+ # Still seems to be a nice way to format the text output of methods
65
+ say array.join("\n"), color
66
+ end
67
+
68
+ def require_appliance
69
+ if options.appliance_id
70
+ yield(StudioApi::Appliance.find(options.appliance_id))
71
+ else
72
+ raise "Unable to find the appliance"
73
+ end
74
+ end
75
+ end
76
+
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,153 @@
1
+ module SSC
2
+ module Handler
3
+ class Package < Base
4
+
5
+ # Structure of the 'software' file:
6
+ #
7
+ # ---
8
+ # list:
9
+ # installed:
10
+ # <name>:
11
+ # version: <package.version>
12
+ # .
13
+ # .
14
+ # .
15
+ # selected:
16
+ # <name>:
17
+ # .
18
+ # .
19
+ # .
20
+ # add:
21
+ # <name>
22
+ # .
23
+ # .
24
+ # .
25
+ # remove:
26
+ # <name>
27
+ # .
28
+ # .
29
+ # .
30
+ # ban:
31
+ # <name>
32
+ # .
33
+ # .
34
+ # .
35
+ # unban:
36
+ # <name>
37
+ # .
38
+ # .
39
+ # .
40
+
41
+ no_tasks do
42
+ cattr_reader :local_source
43
+ @@local_source= 'software'
44
+ end
45
+
46
+ desc 'package search SEARCH_STRING', 'search available packages and patterns'
47
+ require_appliance_id
48
+ method_option :all_repos, :type => :boolean, :default => true
49
+ def search(search_string)
50
+ require_appliance_id(@options) do |appliance|
51
+ params= {:all_repos => options.all_repos} if options.all_repos
52
+ software= appliance.search_software(search_string, params)
53
+ say_array software.collect do |software|
54
+ "#{software.name} v#{software.version}. Repo Id: #{software.repository_id}"
55
+ end
56
+ end
57
+ end
58
+
59
+ desc 'package list [selected|installed]', 'list all selected or installed packages'
60
+ require_appliance_id
61
+ allow_remote_option
62
+ method_option :build_id, :type => :numeric
63
+ def list(type)
64
+ say("installed | selected package only", :red) unless ['installed', 'selected'].include?(type)
65
+ out= if options.remote? || no_local_list?
66
+ require_appliance do |appliance|
67
+ params= {:build_id => options.build_id} if options.build_id
68
+ software= appliance.send("#{type}_software")
69
+ formatted_list= software.collect do |package|
70
+ version= package.version ? { "version" => package.version } : nil
71
+ {package.name => version}
72
+ end
73
+ save(type, formatted_list)
74
+ formatted_list
75
+ end
76
+ else
77
+ read(type)
78
+ end
79
+ say out.to_yaml
80
+ end
81
+
82
+
83
+ desc 'package add NAME', 'add a package to the appliance'
84
+ require_appliance_id
85
+ allow_remote_option
86
+ def add(name)
87
+ if options.remote?
88
+ require_appliance do |appliance|
89
+ response= appliance.add_package(name)
90
+ say case response['state']
91
+ when "fixed"
92
+ "Package Added. State: #{response['state']}"
93
+ when "equal"
94
+ "Package Not Added."
95
+ when "broken"
96
+ "Package Added. State: #{response['state']}. Please resolve dependencies"
97
+ else
98
+ "unknown code"
99
+ end
100
+ end
101
+ else
102
+ save("add", [ name ])
103
+ say "#{name} marked for addition"
104
+ end
105
+ end
106
+
107
+ desc 'package remove NAME', 'remove a package from the appliance'
108
+ require_appliance_id
109
+ allow_remote_option
110
+ def remove(name)
111
+ if options.remote?
112
+ require_appliance do |appliance|
113
+ response= appliance.remove_package(name)
114
+ say "State: #{response['state']}"
115
+ end
116
+ else
117
+ save("remove", [ name ])
118
+ say "#{name} marked for removal"
119
+ end
120
+ end
121
+
122
+ desc 'package ban NAME', 'ban a package from the appliance'
123
+ require_appliance_id
124
+ allow_remote_option
125
+ def ban(name)
126
+ if options.remote?
127
+ require_appliance do |appliance|
128
+ response= appliance.ban_package(name)
129
+ response.collect{|key, val| "#{key}: #{val}"}
130
+ end
131
+ else
132
+ save("ban", [ name ])
133
+ say "#{name} marked to be banned"
134
+ end
135
+ end
136
+
137
+ desc 'package unban NAME', 'unban a package for the appliance'
138
+ require_appliance_id
139
+ allow_remote_option
140
+ def unban(name)
141
+ if options.remote?
142
+ require_appliance do |appliance|
143
+ response= appliance.unban_package(name)
144
+ response.collect{|key, val| "#{key}: #{val}"}
145
+ end
146
+ else
147
+ save("unban", [ name ])
148
+ say "#{name} marked to be unbanned"
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,109 @@
1
+ module SSC
2
+ module Handler
3
+ class Repository < Base
4
+
5
+ # Structure of the 'repositories' file:
6
+ #
7
+ # ---
8
+ # list:
9
+ # <id>:
10
+ # name: <repo.name>
11
+ # type: <repo.type>
12
+ # base_system: <repo.base_url>
13
+ # .
14
+ # .
15
+ # .
16
+ # add:
17
+ # <id>
18
+ # .
19
+ # .
20
+ # .
21
+ # remove:
22
+ # <id>
23
+ # .
24
+ # .
25
+ # .
26
+ # import:
27
+ # name: <name>
28
+ # url: <url>
29
+ no_tasks do
30
+ cattr_reader :local_source
31
+ @@local_source= 'repositories'
32
+ end
33
+
34
+ desc "repository search SEARCH_STRING", "search all available repositories"
35
+ require_authorization
36
+ method_option :base_system, :type => :string
37
+ def search(search_string)
38
+ params= {:filter => search_string}
39
+ params= params.merge({:base_system => options.base_system}) if options.base_system
40
+ repos= StudioApi::Repository.find(:all, :params => params)
41
+ say_array(repos.collect do |repo|
42
+ "#{repo.id}.) #{repo.name}: #{repo.base_url}
43
+ #{[repo.matches.software_name].flatten.join(', ')}\n"
44
+ end)
45
+ end
46
+
47
+ desc "repository list", "list all repositories in a given appliance"
48
+ require_appliance_id
49
+ allow_remote_option
50
+ def list
51
+ list= if options.remote? || no_local_list?
52
+ require_appliance do |appliance|
53
+ appliance.repositories.collect do |repo|
54
+ { repo.id => { 'name' => repo.name,
55
+ 'type' => repo.type,
56
+ 'base_system' => repo.base_system}}
57
+ end
58
+ end
59
+ else
60
+ read('list')
61
+ end
62
+ save('list', list) unless options.remote?
63
+ say list.to_yaml
64
+ end
65
+
66
+ desc 'repository add REPO_IDS', 'add existing repositories to the appliance'
67
+ require_appliance_id
68
+ allow_remote_option
69
+ def add(*repo_ids)
70
+ if options.remote?
71
+ require_appliance do |appliance|
72
+ response= appliance.add_repository(repo_ids)
73
+ say "Added"+( response.collect{|repos| repos.name} ).join(", ")
74
+ end
75
+ else
76
+ save('add', repo_ids)
77
+ say "Marked the following for addition #{repo_ids.join(", ")}"
78
+ end
79
+ end
80
+
81
+ desc 'repository remove REPO_IDS', 'remove existing repositories from appliance'
82
+ require_appliance_id
83
+ allow_remote_option
84
+ def remove(*repo_ids)
85
+ if options.remote?
86
+ require_appliance do |appliance|
87
+ response= appliance.remove_repository(repo_ids)
88
+ say "Removed #{repo_ids.join(", ")}"
89
+ end
90
+ else
91
+ save('remove', repo_ids)
92
+ say "Marked the following for removal #{repo_ids.join(", ")}"
93
+ end
94
+ end
95
+
96
+ desc 'repository import URL NAME', 'import a 3rd party repository into appliance'
97
+ allow_remote_option
98
+ def import(url, name)
99
+ if options.remote?
100
+ repository= StudioApi::Repository.import(url, name)
101
+ say "Added #{repository.name} at #{url}"
102
+ else
103
+ save("import", [{"name" => name, "url" => url}])
104
+ say "Marked #{name} for import"
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,25 @@
1
+ module SSC
2
+ module Handler
3
+ class Template < Base
4
+
5
+ desc 'template list_sets', 'list all available template sets'
6
+ require_authorization
7
+ def list_sets
8
+ templates= StudioApi::TemplateSet.find(:all)
9
+ say_array templates.collect {|template| template.name}
10
+ end
11
+
12
+ desc 'template list SET_NAME', 'show details of a particular template set'
13
+ require_authorization
14
+ def list(name)
15
+ template_set= StudioApi::TemplateSet.find(name)
16
+ out = [template_set.name+' : '+template_set.description]
17
+ out += template_set.template.collect do |appliance|
18
+ "#{appliance.appliance_id}: #{appliance.name}"
19
+ end
20
+ say_array out
21
+ end
22
+ end
23
+ end
24
+
25
+ end
data/lib/ssc.rb ADDED
@@ -0,0 +1,18 @@
1
+ module SSC
2
+ end
3
+
4
+ require 'thor'
5
+ require 'thor/group'
6
+ require 'directory_manager'
7
+ require 'handlers/all'
8
+ require 'yaml'
9
+
10
+ module SSC
11
+ class Base < Thor
12
+ register Handler::Appliance, :appliance, "appliance", "manage appliances"
13
+ register Handler::Repository, :repository, "repository","manage repositories"
14
+ register Handler::Package, :package, "package", "manage packages"
15
+ register Handler::Template, :template, "template", "manage templates"
16
+ register Handler::OverlayFile, :file, "file", "manage files"
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ require 'helper'
2
+
3
+ class TestHandlerAppliance < Test::Unit::TestCase
4
+ context "SSC::Handler::Template" do
5
+ context "#list" do
6
+ setup do
7
+ @handler= SSC::Handler::Appliance.new()
8
+ @handler.stubs(:connect)
9
+ end
10
+
11
+ should "call find(:all) on StudioApi::Appliance" do
12
+ mock_app_list= mock('appliance list')
13
+ mock_app_list.stubs(:collect)
14
+ mock_app_list.stubs(:empty?)
15
+ StudioApi::Appliance.expects(:find).with(:all).returns(mock_app_list)
16
+ @handler.list
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,45 @@
1
+ require 'helper'
2
+
3
+ class TestHandlerHelper < Test::Unit::TestCase
4
+ context "SSC::Handler::Helper" do
5
+ setup do
6
+ class TestObject
7
+ include SSC::Handler::Helper
8
+ end
9
+
10
+ @objekt= TestObject.new
11
+ end
12
+
13
+ context "#connect" do
14
+ should "create connection and configure StudioApi to use it" do
15
+ mock_connection= mock('connection')
16
+ StudioApi::Connection.expects(:new)
17
+ .with('user', 'pass', 'https://susestudio.com/api/v1/user',
18
+ {:proxy => 'proxy'})
19
+ .returns(mock_connection)
20
+ StudioApi::Util.expects(:configure_studio_connection).with(mock_connection)
21
+ @objekt.connect('user', 'pass', {:proxy => 'proxy', :another_option => 'value'})
22
+ end
23
+ end
24
+
25
+ context "#filter_options" do
26
+ should "return a hash of only the specified keys" do
27
+ out= @objekt.filter_options({:a => 'a', :b => 'b'}, [:a])
28
+ assert_equal({:a => 'a'}, out)
29
+ end
30
+ end
31
+
32
+ context "#require_appliance_id" do
33
+ should "raise and error if the appliance id option is not passed" do
34
+ assert_raise(RuntimeError) { @objekt.require_appliance_id({}) }
35
+ end
36
+
37
+ should "not raise error if appliance id is provided" do
38
+ StudioApi::Appliance.expects(:find).with(1).returns(nil)
39
+ assert_nothing_raised do
40
+ @objekt.require_appliance_id(:appliance_id=>1) {|i| i}
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,20 @@
1
+ require 'helper'
2
+
3
+ class TestHandlerRepository < Test::Unit::TestCase
4
+ context "SSC::Handler::Repository" do
5
+ setup do
6
+ @handler= SSC::Handler::Repository.new()
7
+ @handler.stubs(:connect)
8
+ end
9
+ context "#search" do
10
+
11
+ should "call .find(:all, params_hash) on StudioApi::Repository" do
12
+ mock_collection= mock('collection')
13
+ mock_collection.stubs(:collect)
14
+ StudioApi::Repository.expects(:find).with(:all, :params => {:filter => 'chess', :base_system => '11.1'}).returns(mock_collection)
15
+ @handler.instance_variable_set('@options', {:base_system => '11.1'})
16
+ @handler.search('chess')
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ require 'helper'
2
+
3
+ class TestTemplateHandler < Test::Unit::TestCase
4
+ context "SSC::Handler::Template" do
5
+ context "#list" do
6
+ setup do
7
+ @handler= SSC::Handler::Template.new()
8
+ @handler.stubs(:connect)
9
+ end
10
+
11
+ should "call .find(:all) on StudioApi::TemplateSet" do
12
+ mock_template= mock('template_list')
13
+ mock_template.stubs(:collect)
14
+ StudioApi::TemplateSet.expects(:find).with(:all).returns(mock_template)
15
+ @handler.list
16
+ end
17
+
18
+ should "return a list of strings of type 'template.id: template.name'" do
19
+ mock_template= StudioApi::TemplateSet.new(:name => 'Template Name')
20
+ StudioApi::TemplateSet.stubs(:find).with(:all).returns([mock_template])
21
+ assert_equal ["Template Name"],
22
+ @handler.list
23
+ end
24
+
25
+ end
26
+ end
27
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,19 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+ require 'shoulda'
12
+ require 'mocha'
13
+
14
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
15
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
16
+ require 'ssc'
17
+
18
+ class Test::Unit::TestCase
19
+ end
@@ -0,0 +1,55 @@
1
+ require 'helper'
2
+
3
+ class TestArgumentParser < Test::Unit::TestCase
4
+ context "SSC::ArgumentParser" do
5
+
6
+ context "when arguments are good" do
7
+ setup do
8
+ @parser= SSC::ArgumentParser.new(['appliance', 'create', 'act_arg1', 'act_arg2', '--option', 'value', '-o', 'v', '--flag', '-f'])
9
+ end
10
+
11
+ should "set @klass to Appliance" do
12
+ assert_equal SSC::Handler::Appliance, @parser.klass
13
+ end
14
+
15
+ should "set @action to create" do
16
+ assert_equal 'create', @parser.action
17
+ end
18
+
19
+ should "set @options to option hash" do
20
+ assert_equal({:option => 'value',
21
+ :o => 'v',
22
+ :flag => true,
23
+ :f => true }, @parser.options)
24
+ end
25
+
26
+ should "set @action_arguments to argument array" do
27
+ #only one of the arguments must be taken since create take only one argument
28
+ assert_equal(['act_arg1'], @parser.action_arguments)
29
+ end
30
+
31
+ should "have the entire list if arity of the method is -1(splat)" do
32
+ parser= SSC::ArgumentParser.new(['repository', 'add', 'act_arg1', 'act_arg2'])
33
+ assert_equal(['act_arg1', 'act_arg2'], parser.action_arguments )
34
+ end
35
+
36
+ end
37
+ end
38
+
39
+ context "when handler is unknown" do
40
+ should "raise UnkownOptionError" do
41
+ assert_raise(SSC::UnkownOptionError) do
42
+ SSC::ArgumentParser.new(['apliance', 'create'])
43
+ end
44
+ end
45
+ end
46
+
47
+ context "when handler method is unknown" do
48
+ should "raise UnkownOptionError" do
49
+ assert_raise(SSC::UnkownOptionError) do
50
+ SSC::ArgumentParser.new(['appliance', 'unkown_method'])
51
+ end
52
+ end
53
+ end
54
+
55
+ end