ssc 0.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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