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