ssc 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +1 -1
- data/README.rdoc +83 -3
- data/VERSION +1 -1
- data/bin/ssc +1 -1
- data/lib/directory_manager.rb +107 -155
- data/lib/handlers/all.rb +8 -1
- data/lib/handlers/appliance.rb +16 -12
- data/lib/handlers/build.rb +56 -0
- data/lib/handlers/file.rb +38 -19
- data/lib/handlers/helper.rb +32 -1
- data/lib/handlers/package.rb +19 -11
- data/lib/handlers/repository.rb +17 -9
- data/lib/ssc.rb +124 -1
- data/ssc.gemspec +82 -0
- data/test/helper.rb +12 -1
- data/test/integration/test_appliance.rb +82 -0
- data/test/integration/test_file.rb +86 -0
- data/test/integration/test_package.rb +75 -0
- data/test/integration/test_repository.rb +78 -0
- data/test/unit/test_directory_manager.rb +100 -0
- metadata +12 -22
- data/test/handlers/test_appliance.rb +0 -20
- data/test/handlers/test_helper.rb +0 -45
- data/test/handlers/test_repository.rb +0 -20
- data/test/handlers/test_template.rb +0 -27
- data/test/test_argument_parser.rb +0 -55
- data/test/test_directory_manager.rb +0 -40
- data/test/test_ssc.rb +0 -39
data/Gemfile.lock
CHANGED
data/README.rdoc
CHANGED
@@ -4,15 +4,95 @@ This is the new version of the the Suse Studio command line client. Built as a p
|
|
4
4
|
|
5
5
|
== Installing ssc
|
6
6
|
|
7
|
+
=== The easy way
|
8
|
+
gem install ssc
|
9
|
+
|
7
10
|
=== Straight from the code
|
8
11
|
* Checkout the code
|
9
12
|
* In the checked out directory do `rake install`
|
10
13
|
|
11
|
-
|
12
|
-
|
14
|
+
== Using ssc
|
15
|
+
|
16
|
+
=== Command Listing
|
17
|
+
|
18
|
+
ssc checkout --appliance-id=N --password=PASSWORD --username=USERNAME # checkout the latest changes to an appliance
|
19
|
+
ssc commit --appliance-id=N --password=PASSWORD --username=USERNAME # commit changes to studio
|
20
|
+
ssc status --appliance-id=N --password=PASSWORD --username=USERNAME # show status of the appliance
|
21
|
+
|
22
|
+
ssc appliance # manage appliances
|
23
|
+
|
24
|
+
ssc appliance create APPLIANCE_NAME --password=PASSWORD --source-id=N --username=USERNAME # Create an appliance
|
25
|
+
ssc appliance destroy --appliance-id=N --password=PASSWORD --username=USERNAME # destroy the current appliance
|
26
|
+
ssc appliance info --appliance-id=N --password=PASSWORD --username=USERNAME # show details of a specific appliance
|
27
|
+
ssc appliance list --password=PASSWORD --username=USERNAME # list all appliances
|
28
|
+
ssc appliance status --appliance-id=N --password=PASSWORD --username=USERNAME # gives status of the appliance
|
29
|
+
|
30
|
+
ssc package # manage packages
|
31
|
+
|
32
|
+
ssc package add NAME --appliance-id=N --password=PASSWORD --username=USERNAME
|
33
|
+
# add a package to the appliance
|
34
|
+
ssc package ban NAME --appliance-id=N --password=PASSWORD --username=USERNAME
|
35
|
+
# ban a package from the appliance
|
36
|
+
ssc package list [selected|installed] --appliance-id=N --password=PASSWORD --username=USERNAME
|
37
|
+
# list all selected or installed packages
|
38
|
+
ssc package remove NAME --appliance-id=N --password=PASSWORD --username=USERNAME
|
39
|
+
# remove a package from the appliance
|
40
|
+
ssc package search SEARCH_STRING --appliance-id=N --password=PASSWORD --username=USERNAME
|
41
|
+
# search available packages and patterns
|
42
|
+
ssc package unban NAME --appliance-id=N --password=PASSWORD --username=USERNAME
|
43
|
+
# unban a package for the appliance
|
44
|
+
|
45
|
+
ssc repository # manage repositories
|
46
|
+
ssc repository add REPO_IDS --appliance-id=N --password=PASSWORD --username=USERNAME
|
47
|
+
# add existing repositories to the appliance
|
48
|
+
ssc repository import URL NAME --password=PASSWORD --username=USERNAME
|
49
|
+
# import a 3rd party repository into appliance
|
50
|
+
ssc repository list --appliance-id=N --password=PASSWORD --username=USERNAME
|
51
|
+
# list all repositories in a given appliance
|
52
|
+
ssc repository remove REPO_IDS --appliance-id=N --password=PASSWORD --username=USERNAME
|
53
|
+
# remove existing repositories from appliance
|
54
|
+
ssc repository search SEARCH_STRING --password=PASSWORD --username=USERNAME
|
55
|
+
# search all available repositories
|
56
|
+
|
57
|
+
|
58
|
+
ssc file # manage files
|
59
|
+
ssc file add PATH --appliance-id=N --password=PASSWORD --username=USERNAME # create a new overlay file
|
60
|
+
ssc file diff FILE_NAME --appliance-id=N --password=PASSWORD --username=USERNAME
|
61
|
+
# show the diff of the remote file and the local one
|
62
|
+
ssc file list --appliance-id=N --password=PASSWORD --username=USERNAME # show all overlay files
|
63
|
+
ssc file remove FILE_NAME --appliance-id=N --password=PASSWORD --username=USERNAME # removes existing overlay file
|
64
|
+
ssc file show FILE_NAME --appliance-id=N --password=PASSWORD --username=USERNAME # show the contents of the file
|
65
|
+
|
66
|
+
|
67
|
+
ssc build # manage builds
|
68
|
+
ssc build --appliance-id=N --password=PASSWORD --username=USERNAME # build an appliance
|
69
|
+
ssc list --appliance-id=N --password=PASSWORD --username=USERNAME # list builds (running or completed)
|
70
|
+
ssc status --build-id=N --password=PASSWORD --username=USERNAME # find the build status of an appliance
|
71
|
+
|
72
|
+
ssc template # manage templates
|
73
|
+
ssc template list SET_NAME --password=PASSWORD --username=USERNAME # show details of a particular template set
|
74
|
+
ssc template list_sets --password=PASSWORD --username=USERNAME # list all available template sets
|
75
|
+
|
76
|
+
ssc help [TASK] # Describe available tasks or one specific task
|
77
|
+
|
78
|
+
=== Examples
|
79
|
+
|
80
|
+
* Creating and modifying a new appliance:
|
81
|
+
$ ssc appliance create web_server --source-id=SOURCE_APPLIANCE_ID --username=USERNAME --password=PASSWORD
|
82
|
+
$ cd web_server
|
83
|
+
$ ssc package list
|
84
|
+
$ ssc package add apache
|
85
|
+
$ ssc file add /etc/apache2/apache2.conf
|
86
|
+
$ ssc commit
|
87
|
+
|
88
|
+
* Checking out an existing appliance and starting a build
|
89
|
+
$ ssc checkout --appliance-id 59836 --username=USERNAME --password=PASSWORD
|
90
|
+
$ cd APPLIANCE_NAME
|
91
|
+
$ ssc build
|
92
|
+
$ ssc build status
|
13
93
|
|
14
94
|
== Contributing to ssc
|
15
|
-
|
95
|
+
|
16
96
|
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
17
97
|
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
18
98
|
* Fork the project
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.3.0
|
data/bin/ssc
CHANGED
data/lib/directory_manager.rb
CHANGED
@@ -1,194 +1,146 @@
|
|
1
1
|
module SSC
|
2
2
|
module DirectoryManager
|
3
|
+
class LocalStorageFile
|
3
4
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
end
|
8
|
-
|
9
|
-
module ClassMethods
|
10
|
-
def create_appliance_directory(appliance_dir, username, password, appliance_id)
|
11
|
-
FileUtils.mkdir_p(appliance_dir)
|
12
|
-
FileUtils.mkdir_p(File.join(appliance_dir, 'files'))
|
13
|
-
FileUtils.touch(File.join(appliance_dir, 'repositories'))
|
14
|
-
FileUtils.touch(File.join(appliance_dir, 'software'))
|
15
|
-
FileUtils.touch(File.join(appliance_dir, 'files/.file_list'))
|
16
|
-
File.open(File.join(appliance_dir, '.sscrc'), 'w') do |file|
|
17
|
-
file.write("username: #{username}\n"+
|
18
|
-
"password: #{password}\n"+
|
19
|
-
"appliance_id: #{appliance_id}")
|
20
|
-
end
|
21
|
-
File.join(Dir.pwd, appliance_dir)
|
5
|
+
def initialize(file_name, path= nil)
|
6
|
+
path = path ? path : Dir.pwd
|
7
|
+
@location= File.join(path, file_name)
|
22
8
|
end
|
23
9
|
|
24
|
-
def
|
25
|
-
|
26
|
-
if appliance_directory_valid?(Dir.pwd)
|
27
|
-
file= File.join(Dir.pwd, local_source)
|
28
|
-
self.class.class_variable_set('@@local_source', file) if File.exist?(file)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
private
|
33
|
-
|
34
|
-
def appliance_directory_valid?(dir)
|
35
|
-
config_file= File.join(dir, '.sscrc')
|
36
|
-
File.exist?(config_file) && File.read(config_file).match(/appliance_id:\ *\d+/)
|
10
|
+
def valid?
|
11
|
+
File.exist?(@location)
|
37
12
|
end
|
38
13
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
include Thor::Actions
|
43
|
-
|
44
|
-
# Save data to local storage file
|
45
|
-
# @param [String] section The section of the document that is to be saved
|
46
|
-
# @param [Array] list The data in Array format which will be merged with existing data
|
47
|
-
def save(section, list)
|
48
|
-
safe_get_source_file do |source|
|
49
|
-
parsed_file= YAML::load(File.read(source))
|
50
|
-
# YAML::load returns false if file is empty
|
51
|
-
parsed_file= {} unless parsed_file
|
52
|
-
final_list= list
|
53
|
-
if parsed_file[section]
|
54
|
-
current_list= parsed_file[section]
|
55
|
-
final_list= current_list | final_list
|
56
|
-
end
|
57
|
-
parsed_file[section]= final_list
|
58
|
-
File.open(source, 'w') {|f| f.write parsed_file.to_yaml}
|
59
|
-
end
|
14
|
+
def read
|
15
|
+
# default error is informative enough if the file is not found
|
16
|
+
@parsed_file = @parsed_file || YAML::load(File.read(@location)) || {}
|
60
17
|
end
|
61
18
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
safe_get_source_file do |source|
|
69
|
-
if section
|
70
|
-
parsed_file= YAML::load(File.read(source))
|
71
|
-
parsed_file[section]
|
72
|
-
else
|
73
|
-
File.read(source)
|
74
|
-
end
|
19
|
+
def [](section)
|
20
|
+
read
|
21
|
+
if @parsed_file[section]
|
22
|
+
@parsed_file[section]
|
23
|
+
else
|
24
|
+
[]
|
75
25
|
end
|
76
26
|
end
|
77
27
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
# It takes a block with one argument - the path of the source file
|
83
|
-
def safe_get_source_file
|
84
|
-
source= self.class.class_variable_get('@@local_source')
|
85
|
-
source= File.join(Dir.pwd, source)
|
86
|
-
if File.exist?(source)
|
87
|
-
yield source
|
28
|
+
def pop(section)
|
29
|
+
read
|
30
|
+
if @parsed_file[section].is_a?(Array)
|
31
|
+
@parsed_file[section].pop
|
88
32
|
else
|
89
|
-
|
33
|
+
nil
|
90
34
|
end
|
91
35
|
end
|
92
|
-
|
93
|
-
def
|
94
|
-
|
95
|
-
parsed_file
|
96
|
-
|
97
|
-
files= parsed_file["list"].select{|i| i.keys[0] == file_name}
|
98
|
-
if files.length < 1
|
99
|
-
raise ArgumentError, "file not found"
|
100
|
-
else
|
101
|
-
files[0][file_name]["id"]
|
102
|
-
end
|
36
|
+
|
37
|
+
def push(section, item)
|
38
|
+
read
|
39
|
+
if @parsed_file[section].is_a?(Array)
|
40
|
+
@parsed_file[section] |= [ item ]
|
103
41
|
else
|
104
|
-
|
42
|
+
@parsed_file[section] = [ item ]
|
105
43
|
end
|
44
|
+
item
|
106
45
|
end
|
107
46
|
|
108
|
-
def full_local_file_path(file)
|
109
|
-
full_path= File.join(self.class.class_variable_get('@@local_source'), file)
|
110
|
-
end
|
111
47
|
|
112
|
-
def
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
raise ArgumentError, "file not found"
|
118
|
-
end
|
48
|
+
def save
|
49
|
+
contents= @parsed_file.to_yaml
|
50
|
+
@parsed_file= nil
|
51
|
+
File.open(@location, 'w') {|f| f.write contents}
|
52
|
+
contents
|
119
53
|
end
|
120
54
|
|
121
|
-
def
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
def initiate_file(file_dir, file_name, id)
|
126
|
-
source_file= File.join(file_dir, file_name)
|
127
|
-
destination_file= full_local_file_path(file_name)
|
128
|
-
file_list= File.join(self.class.class_variable_get('@@local_source'), '.file_list')
|
129
|
-
if File.exist?(source_file)
|
130
|
-
FileUtils.cp(source_file, destination_file)
|
131
|
-
parsed_file= YAML::load(File.read(file_list)) || {}
|
132
|
-
File.open(file_list, 'w') do |f|
|
133
|
-
if id # if the file has been uploaded
|
134
|
-
parsed_file['list']= [] unless parsed_file['list']
|
135
|
-
parsed_file['list'] |= [{file_name => {
|
136
|
-
"id" => id,
|
137
|
-
"path" => file_dir}}]
|
138
|
-
else
|
139
|
-
parsed_file['add']= [] unless parsed_file['add']
|
140
|
-
|
141
|
-
parsed_file['add'] |= [{file_name => {
|
142
|
-
"path" => file_dir}}]
|
143
|
-
end
|
144
|
-
f.write(parsed_file.to_yaml)
|
145
|
-
end
|
146
|
-
destination_file
|
147
|
-
else
|
148
|
-
raise ArgumentError, "File does not exist"
|
149
|
-
end
|
55
|
+
def empty_list?
|
56
|
+
read
|
57
|
+
(!@parsed_file['list']) || (@parsed_file['list'] == []) || (@parsed_file['list'] == {})
|
150
58
|
end
|
59
|
+
end
|
151
60
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
parsed_file = {} unless parsed_file
|
156
|
-
parsed_file["list"]
|
61
|
+
class PackageFile < LocalStorageFile
|
62
|
+
def initialize(path= nil)
|
63
|
+
super("software", path)
|
157
64
|
end
|
65
|
+
end
|
158
66
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
YAML::load(file_list)
|
67
|
+
class RepositoryFile < LocalStorageFile
|
68
|
+
def initialize(path= nil)
|
69
|
+
super("repositories", path)
|
163
70
|
end
|
71
|
+
end
|
164
72
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
73
|
+
class FileListFile < LocalStorageFile
|
74
|
+
def initialize(path= nil)
|
75
|
+
super("files/.file_list", path)
|
76
|
+
end
|
77
|
+
|
78
|
+
def pop(section)
|
79
|
+
file_hash= super(section)
|
80
|
+
file_name= file_hash.keys[0]
|
81
|
+
file_hash= file_hash[file_name]
|
82
|
+
{ :name => file_name,
|
83
|
+
:full_path => File.join(file_hash["path"]),
|
84
|
+
:params => file_hash.slice("owner", "group", "permissions")}
|
85
|
+
end
|
86
|
+
|
87
|
+
def initiate_file(path, options)
|
88
|
+
raise "Unknown file #{path}" unless File.exists?(path)
|
89
|
+
file_path, file_name= File.split(path)
|
90
|
+
file_path ||= options[:path]
|
91
|
+
destination_path= File.join(File.split(@location)[0], file_name)
|
92
|
+
FileUtils.cp(path, destination_path)
|
93
|
+
if options[:id]
|
94
|
+
push("list", {file_name => {
|
95
|
+
"id" => options[:id],
|
96
|
+
"path" => file_path }})
|
97
|
+
else
|
98
|
+
file_params= options.slice(:permissions, :group, :owner).merge(:path => file_path)
|
99
|
+
push("add", {file_name => file_params})
|
174
100
|
end
|
101
|
+
destination_file
|
175
102
|
end
|
176
103
|
|
177
|
-
def
|
178
|
-
|
179
|
-
|
180
|
-
|
104
|
+
def is_uploaded?(file_name)
|
105
|
+
read
|
106
|
+
list= @parsed_file["list"].select{|i| i.keys[0] == file_name}
|
107
|
+
list.length > 0 ? list[0]["id"].to_i : nil
|
181
108
|
end
|
182
109
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
110
|
+
end
|
111
|
+
|
112
|
+
class ApplianceDirectory
|
113
|
+
attr_reader :path
|
114
|
+
attr_reader :files
|
115
|
+
|
116
|
+
def initialize(name= '', options = {})
|
117
|
+
@name= name
|
118
|
+
@path= File.join(Dir.pwd, name)
|
119
|
+
@files = if Dir.exist?(@path)
|
120
|
+
{:package => PackageFile.new(@path),
|
121
|
+
:repository => RepositoryFile.new(@path),
|
122
|
+
:file_list => FileListFile.new(@path)}
|
123
|
+
else
|
124
|
+
{}
|
125
|
+
end
|
126
|
+
@options= options
|
127
|
+
end
|
128
|
+
|
129
|
+
def create
|
130
|
+
FileUtils.mkdir_p(@name)
|
131
|
+
FileUtils.mkdir_p(File.join(@name, 'files'))
|
132
|
+
@files[:repository] = FileUtils.touch(File.join(@name, 'repositories'))[0]
|
133
|
+
@files[:package] = FileUtils.touch(File.join(@name, 'software'))[0]
|
134
|
+
@files[:file_list] = FileUtils.touch(File.join(@name, 'files/.file_list'))[0]
|
135
|
+
File.open(File.join(@name, '.sscrc'), 'w') do |file|
|
136
|
+
file.write(@options.stringify_keys.to_yaml)
|
189
137
|
end
|
138
|
+
File.join(Dir.pwd, @name)
|
190
139
|
end
|
191
140
|
|
141
|
+
def valid?
|
142
|
+
Dir.exists?(@path) && File.exists?(File.join(@path, '.sscrc'))
|
143
|
+
end
|
192
144
|
end
|
193
145
|
end
|
194
146
|
end
|
data/lib/handlers/all.rb
CHANGED
@@ -11,7 +11,6 @@ module SSC
|
|
11
11
|
class Base < Thor
|
12
12
|
|
13
13
|
include Helper
|
14
|
-
include DirectoryManager
|
15
14
|
|
16
15
|
API_URL= 'https://susestudio.com/api/v1/user'
|
17
16
|
|
@@ -22,10 +21,18 @@ module SSC
|
|
22
21
|
connect(options.username, options.password, optional_connection_options)
|
23
22
|
@not_local= true if options.remote?
|
24
23
|
end
|
24
|
+
|
25
|
+
no_tasks do
|
26
|
+
def say(*args)
|
27
|
+
super(*args)
|
28
|
+
args[0]
|
29
|
+
end
|
30
|
+
end
|
25
31
|
end
|
26
32
|
end
|
27
33
|
end
|
28
34
|
|
35
|
+
require 'build'
|
29
36
|
require 'appliance'
|
30
37
|
require 'repository'
|
31
38
|
require 'package'
|
data/lib/handlers/appliance.rb
CHANGED
@@ -11,18 +11,20 @@ module SSC
|
|
11
11
|
params= {:name => appliance_name}
|
12
12
|
params.merge!(:arch => options.arch) if options.arch
|
13
13
|
appliance= StudioApi::Appliance.clone(options.source_id, params)
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
14
|
+
appliance_params= {
|
15
|
+
:username => options.username,
|
16
|
+
:password => options.password,
|
17
|
+
:appliance_id => appliance.id }
|
18
|
+
appliance_dir= ApplianceDirectory.new(appliance_name, appliance_params)
|
19
|
+
appliance_dir.create
|
20
|
+
say_array(["Created: ", appliance_dir.path] + appliance_dir.files.values)
|
19
21
|
end
|
20
22
|
|
21
23
|
desc "appliance list", "list all appliances"
|
22
24
|
require_authorization
|
23
25
|
def list
|
24
26
|
appliances= StudioApi::Appliance.find(:all)
|
25
|
-
print_table appliances.collect{|i| [i.id, i.name]}
|
27
|
+
print_table([["id", "name"]]+appliances.collect{|i| [i.id, i.name]})
|
26
28
|
end
|
27
29
|
|
28
30
|
desc "appliance info", "show details of a specific appliance"
|
@@ -37,12 +39,14 @@ module SSC
|
|
37
39
|
desc "appliance destroy", "destroy the current appliance (within appliance directory only)"
|
38
40
|
require_appliance_id
|
39
41
|
def destroy
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
42
|
+
require_appliance do |appliance|
|
43
|
+
if appliance.destroy.code_type == Net::HTTPOK
|
44
|
+
say 'Appliance Successfully Destroyed', :red
|
45
|
+
else
|
46
|
+
say_array ['There was a problem with destroying the appliance.',
|
47
|
+
'Make sure that you\'re in the appliance directory OR',
|
48
|
+
'Have provided the --appliance_id option']
|
49
|
+
end
|
46
50
|
end
|
47
51
|
end
|
48
52
|
|