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 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
+