ssc 0.1
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/README +1 -0
- data/bin/ssc +233 -0
- data/lib/appliancehandler.rb +69 -0
- data/lib/buildhandler.rb +125 -0
- data/lib/checkouthandler.rb +322 -0
- data/lib/commandhandler.rb +38 -0
- data/lib/request.rb +29 -0
- metadata +78 -0
data/README
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
A commandline client for SUSE Studio.
|
data/bin/ssc
ADDED
@@ -0,0 +1,233 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'net/http'
|
5
|
+
require 'net/netrc'
|
6
|
+
require 'xml/smart'
|
7
|
+
require 'cgi'
|
8
|
+
require 'fileutils'
|
9
|
+
require 'optparse'
|
10
|
+
|
11
|
+
$LOAD_PATH << "#{File.dirname(__FILE__)}/../lib"
|
12
|
+
require 'appliancehandler.rb'
|
13
|
+
require 'buildhandler.rb'
|
14
|
+
require 'checkouthandler.rb'
|
15
|
+
|
16
|
+
|
17
|
+
$server_name="susestudio.com"
|
18
|
+
$api_prefix="api/v1/user"
|
19
|
+
$username=""
|
20
|
+
$password=""
|
21
|
+
force = false
|
22
|
+
follow = false
|
23
|
+
images = false
|
24
|
+
version="0.1"
|
25
|
+
|
26
|
+
def get_appliance_from_args_or_config args
|
27
|
+
if args
|
28
|
+
appliance = args[1]
|
29
|
+
end
|
30
|
+
unless appliance
|
31
|
+
if File.exists?(".ssc/appliance.config")
|
32
|
+
appliance_config = XML::Smart.open(".ssc/appliance.config")
|
33
|
+
appliance = appliance_config.find("/checkout/appliance_id").first.to_s if appliance_config.find("/checkout/appliance_id").length > 0
|
34
|
+
end
|
35
|
+
end
|
36
|
+
if appliance.nil? || appliance.empty?
|
37
|
+
STDERR.puts "You need to specify an appliance."
|
38
|
+
exit 1
|
39
|
+
end
|
40
|
+
appliance
|
41
|
+
end
|
42
|
+
|
43
|
+
def base_url
|
44
|
+
"http://#{$server_name}/#{$api_prefix}"
|
45
|
+
end
|
46
|
+
|
47
|
+
opt = OptionParser.new
|
48
|
+
opt.separator("Options")
|
49
|
+
opt.on( "-s", "--server", "=HOST",
|
50
|
+
"The Studio hostname" ) do |v|
|
51
|
+
$server_name = v
|
52
|
+
end
|
53
|
+
opt.on( "-u", "--username", "=USER_NAME", "User name") do |v|
|
54
|
+
$username = v
|
55
|
+
end
|
56
|
+
opt.on( "-p", "--password", "=PASSWORD", "Password") do |v|
|
57
|
+
$password = v
|
58
|
+
end
|
59
|
+
opt.on( "-h", "--help", "Print this message" ) do
|
60
|
+
puts opt
|
61
|
+
exit
|
62
|
+
end
|
63
|
+
opt.on( "-v", "--version", "Print the version") do |v|
|
64
|
+
puts "ssc #{version} - A command line interface to SUSE Studio"
|
65
|
+
exit 0
|
66
|
+
end
|
67
|
+
|
68
|
+
# Try to get credentials from .netrc
|
69
|
+
rc = Net::Netrc.locate($server_name)
|
70
|
+
if $username.empty? and $password.empty? and rc
|
71
|
+
$username = rc.login
|
72
|
+
$password = rc.password
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
if ARGV.include?("ba") || ARGV.include?("buildappliance")
|
77
|
+
opt.banner = "Usage: ssc [options] buildappliance APPLIANCE [command-options]"
|
78
|
+
opt.separator("Trigger a build of an appliance.")
|
79
|
+
opt.separator("\n")
|
80
|
+
opt.separator("Command options:")
|
81
|
+
opt.on( "-f", "--force","Force building the appliance even if it overwrites a build") do |v|
|
82
|
+
force = v
|
83
|
+
end
|
84
|
+
elsif ARGV.include?("la") || ARGV.include?("listappliances")
|
85
|
+
opt.banner = "Usage: ssc [options] listappliances"
|
86
|
+
opt.separator("Show a list of your appliances.")
|
87
|
+
elsif ARGV.include?("ca") || ARGV.include?("cloneappliance")
|
88
|
+
opt.banner = "Usage: ssc [options] cloneappliance APPLIANCE"
|
89
|
+
opt.separator("Create a new appliance by cloning a template.")
|
90
|
+
elsif ARGV.include?("da") || ARGV.include?("deleteappliance")
|
91
|
+
opt.banner = "Usage: ssc [options] deleteappliance APPLIANCE"
|
92
|
+
opt.separator("Delete an appliance.")
|
93
|
+
elsif ARGV.include?("lt") || ARGV.include?("listtemplates")
|
94
|
+
opt.banner = "Usage: ssc [options] listtemplates"
|
95
|
+
opt.separator("Get a list of available templates.")
|
96
|
+
elsif ARGV.include?("lrb") || ARGV.include?("listrunningbuilds")
|
97
|
+
opt.banner = "Usage: ssc [options] listrunningbuilds APPLIANCE"
|
98
|
+
opt.separator("List all running builds of an appliance.")
|
99
|
+
elsif ARGV.include?("srb") || ARGV.include?("showrunningbuild")
|
100
|
+
opt.banner = "Usage: ssc [options] showrunningbuild ID"
|
101
|
+
opt.separator("Show the status of a running build.")
|
102
|
+
opt.on( "-f", "--follow","Follow the progress of the build") do |f|
|
103
|
+
follow = f
|
104
|
+
end
|
105
|
+
elsif ARGV.include?("lb") || ARGV.include?("listbuilds")
|
106
|
+
opt.banner = "Usage: ssc [options] listbuilds APPLIANCE"
|
107
|
+
opt.separator("List builds of an appliance.")
|
108
|
+
elsif ARGV.include?("sb") || ARGV.include?("showbuild")
|
109
|
+
opt.banner = "Usage: ssc [options] showbuild ID"
|
110
|
+
opt.separator("Show information on a build.")
|
111
|
+
elsif ARGV.include?("cb") || ARGV.include?("cancelbuild")
|
112
|
+
opt.banner = "Usage: ssc [options] cancelbuild ID"
|
113
|
+
opt.separator("Cancel a running build.")
|
114
|
+
elsif ARGV.include?("db") || ARGV.include?("deletebuild")
|
115
|
+
opt.banner = "Usage: ssc [options] deletebuild ID"
|
116
|
+
opt.separator("Delete a finished build.")
|
117
|
+
elsif ARGV.include?("co") || ARGV.include?("checkout")
|
118
|
+
opt.banner = "Usage: ssc [options] checkout APPLIANCE"
|
119
|
+
opt.separator("Checkout an appliance.")
|
120
|
+
opt.on( "-i", "--download-images","Download images of the appliance") do |i|
|
121
|
+
images = i
|
122
|
+
end
|
123
|
+
elsif ARGV.include?("st") || ARGV.include?("status")
|
124
|
+
opt.banner = "Usage: ssc [options] status"
|
125
|
+
opt.separator("Show the status of the checkout.")
|
126
|
+
elsif ARGV.include?("ci") || ARGV.include?("commit")
|
127
|
+
opt.banner = "Usage: ssc [options] commit"
|
128
|
+
opt.separator("Commit changes to the appliance.")
|
129
|
+
elsif ARGV.include?("add")
|
130
|
+
opt.banner = "Usage: ssc [options] add FILE"
|
131
|
+
opt.separator("Add a file to the checkout.")
|
132
|
+
elsif ARGV.include?("rm") || ARGV.include?("remove")
|
133
|
+
opt.banner = "Usage: ssc [options] remove FILE"
|
134
|
+
opt.separator("Remove a file from the checkout.")
|
135
|
+
else
|
136
|
+
opt.banner = "Usage: ssc [options] COMMAND [command-options]"
|
137
|
+
opt.separator("SUSE Studio command line client.")
|
138
|
+
opt.separator("Type 'ssc COMMAND --help' for help on a specific command.")
|
139
|
+
|
140
|
+
opt.separator("\n")
|
141
|
+
opt.separator("Commands:")
|
142
|
+
opt.separator(" Managing your appliances:")
|
143
|
+
opt.separator(" listappliances,la\t\t Get a list of your appliances")
|
144
|
+
opt.separator(" cloneappliance,ca\t\t Create a new appliance by cloning a template")
|
145
|
+
opt.separator(" deleteappliance,da\t Delete an appliance")
|
146
|
+
opt.separator(" listtemplates,lt\t\t Get a list of available templates")
|
147
|
+
opt.separator("\n")
|
148
|
+
opt.separator(" Managing builds:")
|
149
|
+
opt.separator(" buildappliance,ba\t\t Trigger a build of an appliance")
|
150
|
+
opt.separator(" listrunningbuilds,lrb\t List all running builds of an appliance")
|
151
|
+
opt.separator(" showrunningbuild,srb\t Show the status of a running build")
|
152
|
+
opt.separator(" listbuilds,lb\t\t List builds of an appliance")
|
153
|
+
opt.separator(" showbuild,sb\t\t Show information on a build")
|
154
|
+
opt.separator(" cancelbuild,cb\t\t Cancel a running build")
|
155
|
+
opt.separator(" deletebuild,db\t\t Delete a finished build")
|
156
|
+
opt.separator("\n")
|
157
|
+
opt.separator(" Managing checkouts:")
|
158
|
+
opt.separator(" checkout,co\t\t Checkout an appliance")
|
159
|
+
opt.separator(" status,st\t\t\t Show the status of the checkout")
|
160
|
+
opt.separator(" commit,ci\t\t\t Commit changes to the appliance")
|
161
|
+
opt.separator(" add\t\t\t Add a file to the checkout")
|
162
|
+
opt.separator(" rm\t\t\t Remove a file from the checkout")
|
163
|
+
opt.separator("\n")
|
164
|
+
end
|
165
|
+
|
166
|
+
begin
|
167
|
+
opt.parse!( ARGV )
|
168
|
+
rescue OptionParser::InvalidOption
|
169
|
+
STDERR.puts $!
|
170
|
+
STDERR.puts opt
|
171
|
+
exit 1
|
172
|
+
end
|
173
|
+
|
174
|
+
if ARGV.size == 0
|
175
|
+
STDERR.puts opt
|
176
|
+
exit 1
|
177
|
+
end
|
178
|
+
|
179
|
+
cmd = ARGV[0]
|
180
|
+
|
181
|
+
if cmd == "listappliances" or cmd =="la"
|
182
|
+
ApplianceHandler.list_appliances
|
183
|
+
|
184
|
+
elsif cmd == "cloneappliance" or cmd =="ca"
|
185
|
+
ApplianceHandler.clone_appliance ARGV
|
186
|
+
|
187
|
+
elsif cmd == "deleteappliance" or cmd == "da"
|
188
|
+
ApplianceHandler.delete_appliance ARGV
|
189
|
+
|
190
|
+
elsif cmd == "listtemplates" or cmd =="lt"
|
191
|
+
ApplianceHandler.template_sets
|
192
|
+
|
193
|
+
elsif cmd == "buildappliance" or cmd =="ba"
|
194
|
+
BuildHandler.build_appliance ARGV, force
|
195
|
+
|
196
|
+
elsif cmd == "listrunningbuilds" or cmd =="lrb"
|
197
|
+
BuildHandler.list_running_builds ARGV
|
198
|
+
|
199
|
+
elsif cmd == "showrunningbuild" or cmd =="srb"
|
200
|
+
BuildHandler.show_running_build ARGV, follow
|
201
|
+
|
202
|
+
elsif cmd == "listbuilds" or cmd =="lb"
|
203
|
+
BuildHandler.list_builds ARGV
|
204
|
+
|
205
|
+
elsif cmd == "showbuild" or cmd =="sb"
|
206
|
+
BuildHandler.show_build ARGV
|
207
|
+
|
208
|
+
elsif cmd == "cancelbuild" or cmd =="cb"
|
209
|
+
BuildHandler.cancel_build ARGV
|
210
|
+
|
211
|
+
elsif cmd == "deletebuild" or cmd =="db"
|
212
|
+
BuildHandler.delete_build ARGV
|
213
|
+
|
214
|
+
elsif cmd == "checkout" or cmd =="co"
|
215
|
+
CheckoutHandler.checkout ARGV, images
|
216
|
+
|
217
|
+
elsif cmd == "status" or cmd =="st"
|
218
|
+
CheckoutHandler.status
|
219
|
+
|
220
|
+
elsif cmd == "commit" or cmd =="ci"
|
221
|
+
CheckoutHandler.commit
|
222
|
+
|
223
|
+
elsif cmd == "add"
|
224
|
+
CheckoutHandler.add ARGV
|
225
|
+
|
226
|
+
elsif cmd == "remove" or cmd == "rm"
|
227
|
+
CheckoutHandler.remove ARGV
|
228
|
+
|
229
|
+
else
|
230
|
+
STDERR.puts "Unknown command: #{cmd}\n"
|
231
|
+
STDERR.puts opt
|
232
|
+
exit 1
|
233
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'commandhandler'
|
4
|
+
|
5
|
+
class ApplianceHandler < CommandHandler
|
6
|
+
def self.list_appliances
|
7
|
+
r = Request.new
|
8
|
+
r.method = "GET"
|
9
|
+
r.call = "appliances"
|
10
|
+
s = doRequest(r)
|
11
|
+
|
12
|
+
xml = XML::Smart.string( s )
|
13
|
+
res = String.new
|
14
|
+
xml.find("/appliances/appliance").each do |a|
|
15
|
+
res << "#{a.find("id").first.to_s}: #{a.find("name").first.to_s} (based on #{a.find("basesystem").first.to_s})\n"
|
16
|
+
res << " Cloned from: #{a.find("parent/name").first.to_s} (#{a.find("parent/id").first.to_s})\n" unless a.find("parent/name").length == 0
|
17
|
+
res << " Builds: #{a.find("builds/build").length} (#{a.find("builds/build/compressed_image_size").inject(0){|sum,item| sum + item.to_i}})\n"
|
18
|
+
res << "\n"
|
19
|
+
end
|
20
|
+
puts res
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.clone_appliance args
|
24
|
+
clonefrom = args[1]
|
25
|
+
if clonefrom.nil? || clonefrom.empty?
|
26
|
+
STDERR.puts "You need to specify a template."
|
27
|
+
exit 1
|
28
|
+
end
|
29
|
+
r = Request.new
|
30
|
+
r.method = "POST"
|
31
|
+
r.call = "appliances?clone_from=#{clonefrom}"
|
32
|
+
s = doRequest(r)
|
33
|
+
|
34
|
+
xml = XML::Smart.string( s )
|
35
|
+
res = String.new
|
36
|
+
res << "Created Appliance: #{xml.find("/appliance/name").first.to_s}\n"
|
37
|
+
res << " Id: " + xml.find("/appliance/id").first.to_s + "\n"
|
38
|
+
res << " Based on: " + xml.find("/appliance/basesystem").first.to_s + "\n"
|
39
|
+
res << " Cloned from: #{xml.find("/appliance/parent/name").first.to_s} (#{xml.find("/appliance/parent/id").first.to_s})\n" unless xml.find("/appliance/parent/name").length == 0
|
40
|
+
puts res
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.delete_appliance args
|
44
|
+
appliance = get_appliance_from_args_or_config args
|
45
|
+
r = Request.new
|
46
|
+
r.method = "DELETE"
|
47
|
+
r.call = "appliances/#{appliance}"
|
48
|
+
doRequest(r)
|
49
|
+
puts "Success."
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.template_sets
|
53
|
+
r = Request.new
|
54
|
+
r.method = "GET"
|
55
|
+
r.call = "template_sets"
|
56
|
+
s = doRequest(r)
|
57
|
+
|
58
|
+
xml = XML::Smart.string( s )
|
59
|
+
res = String.new
|
60
|
+
xml.find("/template_sets/template_set").each do |ts|
|
61
|
+
res << "'#{ts.find("name").first.to_s}' Templates (#{ts.find("description").first.to_s}):\n"
|
62
|
+
ts.find("template").each do |t|
|
63
|
+
res << " #{t.find("appliance_id").first.to_s}: #{t.find("name").first.to_s} (based on #{t.find("basesystem").first.to_s})\n"
|
64
|
+
res << " Description: #{t.find("description").first.to_s}\n\n"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
puts res
|
68
|
+
end
|
69
|
+
end
|
data/lib/buildhandler.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'commandhandler'
|
4
|
+
|
5
|
+
class BuildHandler < CommandHandler
|
6
|
+
def self.build_appliance args, force
|
7
|
+
appliance = get_appliance_from_args_or_config args
|
8
|
+
r = Request.new
|
9
|
+
r.method = "POST"
|
10
|
+
r.call = "running_builds?appliance_id=#{appliance}"
|
11
|
+
r.call += "&force=1" if force
|
12
|
+
s = doRequest(r)
|
13
|
+
|
14
|
+
xml = XML::Smart.string( s )
|
15
|
+
res = String.new
|
16
|
+
res << "Triggered build: #{xml.find("/build/id").first.to_s}" unless xml.find("/build/id").length == 0
|
17
|
+
puts res
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.list_running_builds args
|
21
|
+
appliance = get_appliance_from_args_or_config args
|
22
|
+
r = Request.new
|
23
|
+
r.method = "GET"
|
24
|
+
r.call = "running_builds/?appliance_id=#{appliance}"
|
25
|
+
s = doRequest(r)
|
26
|
+
|
27
|
+
xml = XML::Smart.string( s )
|
28
|
+
res = String.new
|
29
|
+
xml.find("/running_builds/running_build").each do |rb|
|
30
|
+
res << "#{rb.find("id").first.to_s}: #{rb.find("state").first.to_s}"
|
31
|
+
res << ", #{rb.find("percent").first.to_s}% done - #{rb.find("message").first.to_s} (#{rb.find("time_elapsed").first.to_s}s elapsed)" unless xml.find("state").first.to_s == "error"
|
32
|
+
end
|
33
|
+
puts res unless res.empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.show_running_build args, follow
|
37
|
+
build = args[1]
|
38
|
+
if build.nil? || build.empty?
|
39
|
+
STDERR.puts "You need to specify a running build."
|
40
|
+
exit 1
|
41
|
+
end
|
42
|
+
r = Request.new
|
43
|
+
r.method = "GET"
|
44
|
+
r.call = "running_builds/#{build}"
|
45
|
+
while 1
|
46
|
+
s = doRequest(r)
|
47
|
+
|
48
|
+
xml = XML::Smart.string( s )
|
49
|
+
res = String.new
|
50
|
+
return unless xml.find("/running_build/id").length > 0
|
51
|
+
res << xml.find("/running_build/state").first.to_s
|
52
|
+
res << ", #{xml.find("/running_build/percent").first.to_s}% done - #{xml.find("/running_build/message").first.to_s} (#{xml.find("/running_build/time_elapsed").first.to_s}s elapsed)" unless xml.find("/running_build/state").first.to_s == "error"
|
53
|
+
puts res
|
54
|
+
exit 0 unless follow
|
55
|
+
sleep 5
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.list_builds args
|
60
|
+
appliance = get_appliance_from_args_or_config args
|
61
|
+
r = Request.new
|
62
|
+
r.method = "GET"
|
63
|
+
r.call = "builds/?appliance_id=#{appliance}"
|
64
|
+
s = doRequest(r)
|
65
|
+
|
66
|
+
xml = XML::Smart.string( s )
|
67
|
+
res = String.new
|
68
|
+
xml.find("/builds/build").each do |rb|
|
69
|
+
res << "#{rb.find("id").first.to_s}: #{rb.find("state").first.to_s}"
|
70
|
+
res << ", v#{rb.find("version").first.to_s} (#{rb.find("image_type").first.to_s})"
|
71
|
+
res << " (#{rb.find("compressed_image_size").first.to_s} MB)" if rb.find("compressed_image_size").length > 0
|
72
|
+
res << " #{rb.find("download_url").first.to_s}" if rb.find("download_url").length > 0
|
73
|
+
res << "\n"
|
74
|
+
end
|
75
|
+
puts res unless res.empty?
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.show_build args
|
79
|
+
build = args[1]
|
80
|
+
if build.nil? || build.empty?
|
81
|
+
STDERR.puts "You need to specify a build."
|
82
|
+
exit 1
|
83
|
+
end
|
84
|
+
r = Request.new
|
85
|
+
r.method = "GET"
|
86
|
+
r.call = "builds/#{build}"
|
87
|
+
s = doRequest(r)
|
88
|
+
|
89
|
+
xml = XML::Smart.string( s )
|
90
|
+
res = String.new
|
91
|
+
xml.find("/build").each do |rb|
|
92
|
+
res << "#{rb.find("id").first.to_s}: #{rb.find("state").first.to_s}"
|
93
|
+
res << ", v#{rb.find("version").first.to_s} (#{rb.find("image_type").first.to_s})"
|
94
|
+
res << " (#{rb.find("size").first.to_s}/#{rb.find("compressed_image_size").first.to_s} MB)" if rb.find("size").length > 0
|
95
|
+
res << " #{rb.find("download_url").first.to_s}" if rb.find("download_url").length > 0
|
96
|
+
end
|
97
|
+
puts res unless res.empty?
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.cancel_build args
|
101
|
+
build = args[1]
|
102
|
+
if build.nil? || build.empty?
|
103
|
+
STDERR.puts "You need to specify a build."
|
104
|
+
exit 1
|
105
|
+
end
|
106
|
+
r = Request.new
|
107
|
+
r.method = "DELETE"
|
108
|
+
r.call = "running_builds/#{build}"
|
109
|
+
doRequest(r)
|
110
|
+
puts "Success."
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.delete_build args
|
114
|
+
build = args[1]
|
115
|
+
if build.nil? || build.empty?
|
116
|
+
STDERR.puts "You need to specify a build."
|
117
|
+
exit 1
|
118
|
+
end
|
119
|
+
r = Request.new
|
120
|
+
r.method = "DELETE"
|
121
|
+
r.call = "builds/#{build}"
|
122
|
+
doRequest(r)
|
123
|
+
puts "Success."
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,322 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'commandhandler'
|
4
|
+
|
5
|
+
class CheckoutHandler < CommandHandler
|
6
|
+
def self.checkout args, images
|
7
|
+
appliance = get_appliance_from_args_or_config args
|
8
|
+
# Get appliance
|
9
|
+
r = Request.new
|
10
|
+
r.method = "GET"
|
11
|
+
r.call = "appliances/#{appliance}"
|
12
|
+
s = doRequest(r)
|
13
|
+
appliancexml =XML::Smart.string( s )
|
14
|
+
id = appliancexml.find("appliance/id").first.to_s
|
15
|
+
base_system = appliancexml.find("appliance/basesystem").first.to_s
|
16
|
+
puts "Checkout '#{appliancexml.find("appliance/name").first.to_s}'\n"
|
17
|
+
|
18
|
+
FileUtils.mkdir_p id
|
19
|
+
[ ".ssc", ".ssc/files", ".ssc/rpms", "files", "rpms", "images"].each do |d|
|
20
|
+
FileUtils.mkdir_p id + "/" + d
|
21
|
+
end
|
22
|
+
|
23
|
+
XML::Smart.modify("#{id}/.ssc/appliance.config","<checkout/>") { |doc|
|
24
|
+
node = doc.root.add("appliance_id", id)
|
25
|
+
node = doc.root.add("appliance_name", appliancexml.find("appliance/name").first.to_s)
|
26
|
+
node = doc.root.add("base_system", appliancexml.find("appliance/basesystem").first.to_s)
|
27
|
+
}
|
28
|
+
|
29
|
+
# Get files
|
30
|
+
r = Request.new
|
31
|
+
r.method = "GET"
|
32
|
+
r.call = "files/?appliance_id=#{appliance}"
|
33
|
+
s = doRequest(r)
|
34
|
+
|
35
|
+
filesxml = XML::Smart.string( s )
|
36
|
+
filesxml.find("files/file").each do |f|
|
37
|
+
filename = f.find("filename").first.to_s
|
38
|
+
fileid = f.find("id").first.to_s
|
39
|
+
path = "#{id}/files/#{filename}"
|
40
|
+
puts " Downloading '#{filename}'"
|
41
|
+
|
42
|
+
download_file "#{base_url}/files/#{fileid}/data", path
|
43
|
+
FileUtils.cp path, "#{id}/.ssc/files/#{filename}.orig"
|
44
|
+
download_file "#{base_url}/files/#{fileid}", "#{id}/.ssc/files/#{filename}.config"
|
45
|
+
XML::Smart.modify("#{id}/.ssc/files/#{filename}.config") do |doc|
|
46
|
+
node = doc.find("/file").first
|
47
|
+
node.add("state", "synched")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Get rpms
|
52
|
+
r = Request.new
|
53
|
+
r.method = "GET"
|
54
|
+
r.call = "rpms?base_system=#{base_system}"
|
55
|
+
s = doRequest(r)
|
56
|
+
|
57
|
+
rpmsxml = XML::Smart.string( s )
|
58
|
+
rpmsxml.find("rpms/rpm").each do |rpm|
|
59
|
+
filename = rpm.find("filename").first.to_s
|
60
|
+
fileid = rpm.find("id").first.to_s
|
61
|
+
path = "#{id}/rpms/#{filename}"
|
62
|
+
puts " Downloading '#{filename}'"
|
63
|
+
|
64
|
+
download_file "#{base_url}/rpms/#{fileid}/data", path
|
65
|
+
FileUtils.cp path, "#{id}/.ssc/rpms/#{filename}.orig"
|
66
|
+
download_file "#{base_url}/rpms/#{fileid}", "#{id}/.ssc/rpms/#{filename}.config"
|
67
|
+
XML::Smart.modify("#{id}/.ssc/rpms/#{filename}.config") do |doc|
|
68
|
+
node = doc.find("/rpm").first
|
69
|
+
node.add("state", "synched")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
if images
|
74
|
+
appliancexml.find("appliance/builds/build").each do |build|
|
75
|
+
url = build.find("download_url").first.to_s
|
76
|
+
puts " Downloading image '#{File.basename(url)}'"
|
77
|
+
download_file url, "#{id}/images/#{File.basename(url)}"
|
78
|
+
end
|
79
|
+
else
|
80
|
+
puts " Skipped downloading images (change with -i)'"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.status
|
85
|
+
appliance_config = XML::Smart.open(".ssc/appliance.config")
|
86
|
+
id = appliance_config.find("/checkout/appliance_id").first.to_s
|
87
|
+
name = appliance_config.find("/checkout/appliance_name").first.to_s
|
88
|
+
|
89
|
+
show_files = false
|
90
|
+
unknown_files = Array.new
|
91
|
+
added_files = Array.new
|
92
|
+
modified_files = Array.new
|
93
|
+
removed_files = Array.new
|
94
|
+
Dir.entries("files").each do |file|
|
95
|
+
if [".", ".."].include?(file) then next end
|
96
|
+
if File.exists?(".ssc/files/#{file}.config")
|
97
|
+
xml = XML::Smart.open(".ssc/files/#{file}.config")
|
98
|
+
status = xml.find("file/state").first.to_s
|
99
|
+
if status == "added"
|
100
|
+
added_files << file
|
101
|
+
show_files = true
|
102
|
+
elsif status == "synched"
|
103
|
+
oldmd5 = `md5sum .ssc/files/#{file}.orig`.split[0]
|
104
|
+
md5 = `md5sum files/#{file}`.split[0]
|
105
|
+
if md5 == oldmd5
|
106
|
+
next
|
107
|
+
elsif md5 != oldmd5
|
108
|
+
modified_files << file
|
109
|
+
show_files = true
|
110
|
+
end
|
111
|
+
end
|
112
|
+
else
|
113
|
+
unknown_files << file
|
114
|
+
show_files = true
|
115
|
+
end
|
116
|
+
end
|
117
|
+
Dir.entries(".ssc/files").each do |file|
|
118
|
+
if [".", ".."].include?(file) then next end
|
119
|
+
unless file =~ /.*.config$/ then next end
|
120
|
+
xml = XML::Smart.open(".ssc/files/#{file}")
|
121
|
+
if xml.find("file/state").first.to_s == "removed"
|
122
|
+
removed_files << file.sub(/(.*).config$/, "\\1")
|
123
|
+
show_files = true
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
show_rpms = false
|
128
|
+
unknown_rpms = Array.new
|
129
|
+
added_rpms = Array.new
|
130
|
+
modified_rpms = Array.new
|
131
|
+
removed_rpms = Array.new
|
132
|
+
Dir.entries("rpms").each do |file|
|
133
|
+
if [".", ".."].include?(file) then next end
|
134
|
+
if File.exists?(".ssc/rpms/#{file}.config")
|
135
|
+
xml = XML::Smart.open(".ssc/rpms/#{file}.config")
|
136
|
+
status = xml.find("rpm/state").first.to_s
|
137
|
+
if status == "added"
|
138
|
+
added_rpms << file
|
139
|
+
show_rpms = true
|
140
|
+
elsif status == "synched"
|
141
|
+
oldmd5 = `md5sum .ssc/rpms/#{file}.orig`.split[0]
|
142
|
+
md5 = `md5sum rpms/#{file}`.split[0]
|
143
|
+
if md5 == oldmd5
|
144
|
+
next
|
145
|
+
elsif md5 != oldmd5
|
146
|
+
modified_rpms << file
|
147
|
+
show_rpms = true
|
148
|
+
end
|
149
|
+
end
|
150
|
+
else
|
151
|
+
unknown_rpms << file
|
152
|
+
show_rpms = true
|
153
|
+
end
|
154
|
+
end
|
155
|
+
Dir.entries(".ssc/rpms").each do |file|
|
156
|
+
if [".", ".."].include?(file) then next end
|
157
|
+
unless file =~ /.*.config$/ then next end
|
158
|
+
xml = XML::Smart.open(".ssc/rpms/#{file}")
|
159
|
+
if xml.find("rpm/state").first.to_s == "removed"
|
160
|
+
removed_rpms << file.sub(/(.*).config$/, "\\1")
|
161
|
+
show_rpms = true
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
puts "Status of #{name} (#{id}):"
|
166
|
+
if show_files
|
167
|
+
puts " Overlay files:"
|
168
|
+
unknown_files.each {|f| puts " ? #{f}"}
|
169
|
+
modified_files.each {|f| puts " M #{f}"}
|
170
|
+
added_files.each {|f| puts " A #{f}"}
|
171
|
+
removed_files.each {|f| puts " D #{f}"}
|
172
|
+
end
|
173
|
+
if show_rpms
|
174
|
+
puts " RPMs:"
|
175
|
+
unknown_rpms.each {|f| puts " ? #{f}"}
|
176
|
+
modified_rpms.each {|f| puts " M #{f}"}
|
177
|
+
added_rpms.each {|f| puts " A #{f}"}
|
178
|
+
removed_rpms.each {|f| puts " D #{f}"}
|
179
|
+
end
|
180
|
+
unless show_files or show_rpms
|
181
|
+
puts "Nothing changed."
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def self.commit
|
186
|
+
appliance = get_appliance_from_args_or_config nil
|
187
|
+
appliance_config = XML::Smart.open(".ssc/appliance.config")
|
188
|
+
base = appliance_config.find("checkout/base_system").first.to_s
|
189
|
+
|
190
|
+
Dir.entries(".ssc/rpms").each do |file|
|
191
|
+
next unless file =~ /.config$/
|
192
|
+
config = XML::Smart.open(".ssc/rpms/" + file)
|
193
|
+
filename = file.gsub(/(.*).config$/, "\\1")
|
194
|
+
status = config.find("rpm/state").first.to_s
|
195
|
+
|
196
|
+
if (status == "added")
|
197
|
+
puts "Uploading #{filename}"
|
198
|
+
s = `curl -u #{$username}:#{$password} -XPOST -F\"file=@#{"rpms/" + filename}\" http://#{$username}:#{$password}@#{$server_name}/#{$api_prefix}/rpms?base_system=#{base} 2> /dev/null`
|
199
|
+
xml = XML::Smart.string(s)
|
200
|
+
id = xml.find("rpm/id").first.to_s unless xml.find("rpm/id").length == 0
|
201
|
+
if id
|
202
|
+
FileUtils.cp "rpms/#{filename}", ".ssc/rpms/#{filename}.orig"
|
203
|
+
download_file "#{base_url}/rpms/#{id}", ".ssc/rpms/#{filename}.config"
|
204
|
+
end
|
205
|
+
elsif (status == "removed")
|
206
|
+
puts "Removing #{filename}"
|
207
|
+
id = config.find("rpm/id").first.to_s
|
208
|
+
r = Request.new
|
209
|
+
r.method = "DELETE"
|
210
|
+
r.call = "rpms/#{id}"
|
211
|
+
doRequest(r)
|
212
|
+
FileUtils.rm ".ssc/rpms/#{filename}.orig"
|
213
|
+
FileUtils.rm ".ssc/rpms/#{filename}.config"
|
214
|
+
else
|
215
|
+
oldmd5 = `md5sum .ssc/rpms/#{filename}.orig`.split[0]
|
216
|
+
md5 = `md5sum rpms/#{filename}`.split[0]
|
217
|
+
if (status == "synched" and md5 != oldmd5)
|
218
|
+
id = config.find("rpm/id").first.to_s
|
219
|
+
puts "Updating #{filename}"
|
220
|
+
`curl -u #{$username}:#{$password} -XPUT -F\"file=@#{"rpms/" + filename}\" http://#{$username}:#{$password}@#{$server_name}/#{$api_prefix}/rpms/#{id}/data 2> /dev/null`
|
221
|
+
download_file "#{base_url}/rpms/#{id}", ".ssc/rpms/#{filename}.config"
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
Dir.entries(".ssc/files").each do |file|
|
226
|
+
next unless file =~ /.config$/
|
227
|
+
config = XML::Smart.open(".ssc/files/" + file)
|
228
|
+
filename = file.gsub(/(.*).config$/, "\\1")
|
229
|
+
status = config.find("file/state").first.to_s
|
230
|
+
|
231
|
+
if (status == "added")
|
232
|
+
puts "Uploading #{filename}"
|
233
|
+
s = `curl -u #{$username}:#{$password} -XPOST -F\"file=@#{"files/" + filename}\" http://#{$username}:#{$password}@#{$server_name}/#{$api_prefix}/files?appliance_id=#{appliance} 2> /dev/null`
|
234
|
+
xml = XML::Smart.string(s)
|
235
|
+
id = xml.find("file/id").first.to_s unless xml.find("file/id").length == 0
|
236
|
+
if id
|
237
|
+
FileUtils.cp "files/#{filename}", ".ssc/files/#{filename}.orig"
|
238
|
+
download_file "#{base_url}/files/#{id}", ".ssc/files/#{filename}.config"
|
239
|
+
end
|
240
|
+
elsif (status == "removed")
|
241
|
+
puts "Removing #{filename}"
|
242
|
+
id = config.find("file/id").first.to_s
|
243
|
+
r = Request.new
|
244
|
+
r.method = "DELETE"
|
245
|
+
r.call = "files/#{id}"
|
246
|
+
doRequest(r)
|
247
|
+
FileUtils.rm ".ssc/files/#{filename}.orig"
|
248
|
+
FileUtils.rm ".ssc/files/#{filename}.config"
|
249
|
+
else
|
250
|
+
oldmd5 = `md5sum .ssc/files/#{filename}.orig`.split[0]
|
251
|
+
md5 = `md5sum files/#{filename}`.split[0]
|
252
|
+
if ((status == "synched" or status == "modified") and md5 != oldmd5)
|
253
|
+
id = config.find("file/id").first.to_s
|
254
|
+
puts "Updating #{filename}"
|
255
|
+
`curl -u #{$username}:#{$password} -XPUT -F\"file=@#{"files/" + filename}\" http://#{$username}:#{$password}@#{$server_name}/#{$api_prefix}/files/#{id}/data 2> /dev/null`
|
256
|
+
download_file "#{base_url}/files/#{id}", ".ssc/files/#{filename}.config"
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def self.add args
|
263
|
+
filename = args[1]
|
264
|
+
unless File.exists?(filename)
|
265
|
+
STDERR.puts "File '#{filename}' does not exist."
|
266
|
+
return 1
|
267
|
+
end
|
268
|
+
path = File.expand_path(filename)
|
269
|
+
basename = File.basename(path)
|
270
|
+
if path =~ /.*\/rpms\/#{basename}/
|
271
|
+
if File.exists?(".ssc/rpms/#{basename}.config")
|
272
|
+
STDERR.puts "File '#{filename}' already belongs to the checkout."
|
273
|
+
return 1
|
274
|
+
end
|
275
|
+
XML::Smart.modify(".ssc/rpms/#{basename}.config","<rpm/>") { |doc|
|
276
|
+
node = doc.root.add("state", "added")
|
277
|
+
}
|
278
|
+
elsif path =~ /.*\/files\/#{basename}/
|
279
|
+
if File.exists?(".ssc/files/#{basename}.config")
|
280
|
+
STDERR.puts "File '#{filename}' already belongs to the checkout."
|
281
|
+
return 1
|
282
|
+
end
|
283
|
+
XML::Smart.modify(".ssc/files/#{basename}.config","<file/>") { |doc|
|
284
|
+
node = doc.root.add("state", "added")
|
285
|
+
}
|
286
|
+
else
|
287
|
+
STDERR.puts "Only files in /rpms and /files can be added."
|
288
|
+
return 1
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def self.remove args
|
293
|
+
filename = args[1]
|
294
|
+
path = File.expand_path(filename)
|
295
|
+
basename = File.basename(path)
|
296
|
+
if path =~ /.*\/rpms\/#{basename}/
|
297
|
+
type = "rpm"
|
298
|
+
elsif path =~ /.*\/files\/#{basename}/
|
299
|
+
type = "file"
|
300
|
+
else
|
301
|
+
STDERR.puts "Only files in /rpms and /files can be removed."
|
302
|
+
return 1
|
303
|
+
end
|
304
|
+
unless File.exists?(".ssc/#{type}s/#{basename}.config")
|
305
|
+
STDERR.puts "The file does not belong to the checkout and can not be removed."
|
306
|
+
return 1
|
307
|
+
end
|
308
|
+
|
309
|
+
XML::Smart.modify(".ssc/#{type}s/#{basename}.config") { |doc|
|
310
|
+
nodes = doc.find("#{type}/state")
|
311
|
+
if( nodes.first.to_s == "added" )
|
312
|
+
FileUtils.rm "#{type}s/#{basename}"
|
313
|
+
FileUtils.rm ".ssc/#{type}s/#{basename}.config"
|
314
|
+
else
|
315
|
+
nodes.delete_at!(0)
|
316
|
+
doc.root.add("state", "removed")
|
317
|
+
end
|
318
|
+
}
|
319
|
+
FileUtils.rm filename
|
320
|
+
end
|
321
|
+
|
322
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'request.rb'
|
4
|
+
|
5
|
+
|
6
|
+
class CommandHandler
|
7
|
+
def self.doRequest r
|
8
|
+
xml, success = r.go
|
9
|
+
if success
|
10
|
+
return xml
|
11
|
+
else
|
12
|
+
handle_error xml
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.handle_error s
|
17
|
+
xml = XML::Smart.string(s)
|
18
|
+
if xml.find("/error/code").length > 0
|
19
|
+
STDERR.puts "Error '#{xml.find("/error/code").first.to_s}' occured.\nMessage: #{xml.find("/error/message").first.to_s}"
|
20
|
+
else
|
21
|
+
STDERR.puts "Server returned: #{s}"
|
22
|
+
end
|
23
|
+
exit 1
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.download_file url, target
|
27
|
+
uri = URI.parse(url)
|
28
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
29
|
+
request.basic_auth($username, $password)
|
30
|
+
Net::HTTP.start(uri.host, uri.port) do |http|
|
31
|
+
http.read_timeout = 45
|
32
|
+
response = http.request(request)
|
33
|
+
open(target, "wb") do |file|
|
34
|
+
file.write(response.body)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/request.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
class Request
|
2
|
+
attr_accessor :method, :call, :data
|
3
|
+
|
4
|
+
def go
|
5
|
+
uri = URI.parse("#{base_url}/#{call}")
|
6
|
+
request = nil
|
7
|
+
if method == "GET"
|
8
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
9
|
+
elsif method == "POST"
|
10
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
11
|
+
request.set_form_data(data, ";") unless data.nil?
|
12
|
+
elsif method == "DELETE"
|
13
|
+
request = Net::HTTP::Delete.new(uri.request_uri)
|
14
|
+
end
|
15
|
+
request.basic_auth($username, $password)
|
16
|
+
begin
|
17
|
+
Net::HTTP.start(uri.host, uri.port) do |http|
|
18
|
+
http.read_timeout = 45
|
19
|
+
response = http.request(request)
|
20
|
+
unless( response.kind_of? Net::HTTPSuccess )
|
21
|
+
return [response.body, false]
|
22
|
+
end
|
23
|
+
return [response.body, true]
|
24
|
+
end
|
25
|
+
rescue => e
|
26
|
+
return ["Error: #{e.to_s}", false]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ssc
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "0.1"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andre Duffeck
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-06-14 00:00:00 +02:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: ruby-xml-smart
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0.2"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: net-netrc
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.2.1
|
34
|
+
version:
|
35
|
+
description:
|
36
|
+
email: aduffeck@suse.de
|
37
|
+
executables:
|
38
|
+
- ssc
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files:
|
42
|
+
- README
|
43
|
+
files:
|
44
|
+
- bin/ssc
|
45
|
+
- lib/request.rb
|
46
|
+
- lib/commandhandler.rb
|
47
|
+
- lib/checkouthandler.rb
|
48
|
+
- lib/appliancehandler.rb
|
49
|
+
- lib/buildhandler.rb
|
50
|
+
- README
|
51
|
+
has_rdoc: false
|
52
|
+
homepage: http://git.opensuse.org/?p=projects/ssc.git;a=summary
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options: []
|
55
|
+
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: "0"
|
63
|
+
version:
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: "0"
|
69
|
+
version:
|
70
|
+
requirements: []
|
71
|
+
|
72
|
+
rubyforge_project:
|
73
|
+
rubygems_version: 1.3.1
|
74
|
+
signing_key:
|
75
|
+
specification_version: 2
|
76
|
+
summary: A commandline client for SUSE Studio
|
77
|
+
test_files: []
|
78
|
+
|