ssc 0.1

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