repomate 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/repomate ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # expand path if in development mode
4
+ if File.exists?(File.join(File.join(File.dirname(__FILE__), '..', '.git')))
5
+ $: << File.join(File.dirname(__FILE__), '..', 'lib')
6
+ end
7
+
8
+ require 'repomate'
9
+ require 'rubygems'
10
+ require 'slop'
11
+
12
+ options = Slop.parse do
13
+ banner "RepoMate (A simple debian repository management tool)
14
+
15
+ Usage: #{$0} add -s squeeze [-c main] <package>
16
+ #{$0} publish
17
+ #{$0} listpackages -r production
18
+
19
+ Actions:
20
+ add - Add a package to the staging area
21
+ publish - Move packages from staging area to production
22
+ save - Save a checkpoint
23
+ load - Load a checkpoint
24
+ listpackages - List packages
25
+ setup - Setup the pool
26
+
27
+ Options:"
28
+ on :s, :suitename=, "Set the name of the suite (lenny/squeeze...)", :argument => true
29
+ on :c, :component=, "Set the name of the component (main/contrib...)", :default => "main"
30
+ on :a, :architecture=, "Set the name of the component (main/contrib...)", :argument => true
31
+ on :r, :repodir, "Type of pool/category (stage/pool/production)", :argument => true
32
+ on :force, "Force action", :default => false
33
+ on :h, :help, 'Print this help message', :tail => true do
34
+ puts help
35
+ exit
36
+ end
37
+ end
38
+
39
+ cli = RepoMate::Cli.new
40
+
41
+ if ARGV.include?("add")
42
+ filename = nil
43
+ ARGV.each do |arg|
44
+ if arg =~ /\.deb/
45
+ filename = arg
46
+ end
47
+ end
48
+ if filename && File.exists?(filename)
49
+ cli.stage(options, filename)
50
+ else
51
+ STDERR.puts "File does not exist"
52
+ end
53
+ elsif ARGV.include?("publish")
54
+ cli.publish(options)
55
+ elsif ARGV.include?("save")
56
+ cli.save_checkpoint
57
+ elsif ARGV.include?("load")
58
+ cli.choose_checkpoint
59
+ elsif ARGV.include?("listpackages")
60
+ cli.list_packagelist(options)
61
+ elsif ARGV.include?("setup")
62
+ cli.setup(options)
63
+ else
64
+ puts options.help
65
+ end
data/etc/config.yml ADDED
@@ -0,0 +1,18 @@
1
+ ---
2
+ :rootdir: /var/lib/repomate/repository
3
+ :logdir: /var/log/repomate
4
+ :dpkg: /usr/bin/dpkg
5
+ :suites:
6
+ - lenny
7
+ - squeeze
8
+ :components:
9
+ - main
10
+ - contrib
11
+ :architectures:
12
+ - all
13
+ - amd64
14
+ :origin: Repository
15
+ :label: Repository
16
+ :gpg_enable: yes
17
+ :gpg_email: someone@example.net
18
+ :gpg_password: secret
@@ -0,0 +1,105 @@
1
+ # RepoMate module
2
+ module RepoMate
3
+
4
+ # Class for the architecture layer of the directory structure
5
+ class Architecture
6
+
7
+ # Init
8
+ def initialize(architecture, component, suitename, category)
9
+ @architecture = architecture
10
+ @component = component
11
+ @suitename = suitename
12
+ @category = category
13
+ end
14
+
15
+ # Returns the given architecture name (eg. all, amd64)
16
+ def name
17
+ @architecture
18
+ end
19
+
20
+ # Returns the directory strcuture of the architecture including all lower layers
21
+ def directory
22
+ File.join(Cfg.rootdir, @category, @suitename, @component, "binary-#{name}")
23
+ end
24
+
25
+ # Checks if the architecture directory exists
26
+ def exist?
27
+ Dir.exist?(directory)
28
+ end
29
+
30
+ # Checks if the architecture is allowed (See: configurationfile)
31
+ def is_allowed?
32
+ self.allowed.include?(@architecture)
33
+ end
34
+
35
+ # Checks if directory is unused
36
+ def is_unused?(dir)
37
+ status = true
38
+
39
+ path = Dir.glob(File.join(dir, "*"))
40
+ path.each do |dirorfile|
41
+ status = false if File.directory?(dirorfile)
42
+ status = false if File.basename(dirorfile) =~ /\.deb$/
43
+ end
44
+
45
+ status
46
+ end
47
+
48
+ # Creates the directory strcuture of the architecture including all lower layers
49
+ def create
50
+ FileUtils.mkdir_p(directory) unless exist?
51
+ end
52
+
53
+ # Deletes the architecture directory including all lower layers
54
+ def destroy
55
+ FileUtils.rm_r(directory) if exist?
56
+ end
57
+
58
+ # Returns a list of all debian files in the architecture directory
59
+ def files
60
+ Dir.glob(File.join(directory, "*.deb"))
61
+ end
62
+
63
+ # Returns a dataset including the name of the architecture and the fullpath recursive through all lower layers
64
+ def self.dataset(category=nil)
65
+ data = []
66
+ self.all.each do |entry|
67
+ parts = entry.split(/\//)
68
+ unless parts.length < 4
69
+ next unless parts[0].eql?(category) || category.eql?("all")
70
+ architecture = parts[3].split(/-/)
71
+ data << {
72
+ :category => parts[0],
73
+ :suitename => parts[1],
74
+ :component => parts[2],
75
+ :architecture_dir => parts[3],
76
+ :architecture => architecture[1],
77
+ :fullpath => File.join(Cfg.rootdir, entry)
78
+ }
79
+ end
80
+ end
81
+ data
82
+ end
83
+
84
+ # Returns all directories without @rootdir
85
+ def self.all
86
+ components = Component.all
87
+ dirs = []
88
+ rootdir = Cfg.rootdir
89
+ components.each do |component|
90
+ architectures = Dir.glob(File.join(rootdir, component, "*"))
91
+ architectures.each do |architecture|
92
+ dirs.push architecture.gsub(/#{rootdir}\//, '') if File.directory? architecture
93
+ end
94
+ end
95
+ return dirs
96
+ end
97
+
98
+ # Gets all configured architectures
99
+ def self.allowed
100
+ Cfg.architectures.uniq
101
+ end
102
+
103
+ end
104
+ end
105
+
@@ -0,0 +1,335 @@
1
+ require 'repomate'
2
+ require 'date'
3
+ require 'time'
4
+ require 'colors'
5
+
6
+ # RepoMate module
7
+ module RepoMate
8
+
9
+ # Class containing the main logic
10
+ class Base
11
+
12
+ # Init
13
+ def initialize
14
+ FileUtils.mkdir_p(Cfg.rootdir)
15
+
16
+ @repository = Repository.new
17
+ @metafile = Metafile.new
18
+ @cpdbfile = File.join(Cfg.rootdir, "checkpoints.db")
19
+ @cpdb = Database.new(@cpdbfile)
20
+
21
+ unless Dir.exists?(Cfg.logdir)
22
+ puts
23
+ puts "\tPlease run \"repomate setup\" first!".hl(:red)
24
+ puts
25
+ end
26
+
27
+ create_checkpoints_table
28
+
29
+ end
30
+
31
+ # Add's a package to the staging area
32
+ def stage(workload)
33
+ workload.each do |entry|
34
+ @repository.create(entry[:suitename], entry[:component])
35
+
36
+ package = Package.new(entry[:package_fullname], entry[:suitename], entry[:component])
37
+ destination = Component.new(entry[:component], entry[:suitename], "stage")
38
+
39
+ FileUtils.copy(entry[:package_fullname], File.join(destination.directory, package.newbasename))
40
+ end
41
+ end
42
+
43
+ # Returns a list of staged packages for cli confirmation packed as array of hashes
44
+ def prepare_publish
45
+ workload = []
46
+
47
+ source_category = "stage"
48
+ destination_category = "pool"
49
+
50
+ Component.dataset(source_category).each do |entry|
51
+ source = Component.new(entry[:component], entry[:suitename], source_category)
52
+ source.files.each do |fullname|
53
+ package = Package.new(fullname, entry[:suitename], entry[:component])
54
+ destination = Architecture.new(package.architecture, entry[:component], entry[:suitename], destination_category)
55
+
56
+ workload << {
57
+ :source_fullname => fullname,
58
+ :destination_fullname => File.join(destination.directory, package.newbasename),
59
+ :component => entry[:component],
60
+ :suitename => entry[:suitename],
61
+ :architecture => package.architecture
62
+ }
63
+ end
64
+ end
65
+ workload
66
+ end
67
+
68
+ # Publish all staged packages. Packages will be moved from stage to pool and linked to dists
69
+ def publish(workload)
70
+ newworkload = []
71
+ workload.each do |entry|
72
+ destination = Architecture.new(entry[:architecture], entry[:component], entry[:suitename], "dists")
73
+ basename = File.basename(entry[:source_fullname])
74
+
75
+ @repository.create(entry[:suitename], entry[:component], entry[:architecture])
76
+
77
+ newworkload << {
78
+ :source_fullname => entry[:destination_fullname],
79
+ :destination_dir => destination.directory,
80
+ :component => entry[:component],
81
+ :suitename => entry[:suitename],
82
+ :architecture => entry[:architecture]
83
+ }
84
+ FileUtils.move(entry[:source_fullname], entry[:destination_fullname])
85
+ end
86
+ workload = newworkload
87
+
88
+ save_checkpoint
89
+ check_versions(workload)
90
+ end
91
+
92
+ # Does the link job after checking versions through dpkg
93
+ def check_versions(workload)
94
+ dpkg = Cfg.dpkg
95
+
96
+ raise "dpkg is not installed" unless File.exists?(dpkg)
97
+
98
+ link_workload = []
99
+ unlink_workload = []
100
+
101
+ workload.each do |entry|
102
+ source_package = Package.new(entry[:source_fullname], entry[:suitename], entry[:component])
103
+ destination_fullname = File.join(entry[:destination_dir], source_package.newbasename)
104
+
105
+ Dir.glob("#{entry[:destination_dir]}/#{source_package.name}*.deb") do |target_fullname|
106
+ target_package = Package.new(target_fullname, entry[:suitename], entry[:component] )
107
+
108
+ if system("#{dpkg} --compare-versions #{source_package.version} gt #{target_package.version}")
109
+ puts "Package: #{target_package.newbasename} will be replaced with #{source_package.newbasename}"
110
+ unlink_workload << {
111
+ :destination_fullname => target_fullname,
112
+ :newbasename => target_package.newbasename
113
+ }
114
+ elsif system("#{dpkg} --compare-versions #{source_package.version} eq #{target_package.version}")
115
+ puts "Package: #{source_package.newbasename} already exists with same version"
116
+ return
117
+ elsif system("#{dpkg} --compare-versions #{source_package.version} lt #{target_package.version}")
118
+ puts "Package: #{source_package.newbasename} already exists with higher version"
119
+ return
120
+ end
121
+ end
122
+
123
+ link_workload << {
124
+ :source_fullname => source_package.fullname,
125
+ :destination_fullname => destination_fullname,
126
+ :suitename => source_package.suitename,
127
+ :component => source_package.component,
128
+ :newbasename => source_package.newbasename
129
+ }
130
+ end
131
+
132
+ unlink(unlink_workload)
133
+ link(link_workload)
134
+ end
135
+
136
+ # links the workload
137
+ def link(workload)
138
+ action = false
139
+
140
+ workload.each do |entry|
141
+ @repository.create(entry[:suitename], entry[:component], entry[:architecture])
142
+ unless File.exists?(entry[:destination_fullname])
143
+ package = Package.new(entry[:source_fullname], entry[:suitename], entry[:component])
144
+ package.create_checksums
145
+
146
+ File.symlink(entry[:source_fullname], entry[:destination_fullname])
147
+ puts "Package: #{package.newbasename} linked to production => #{entry[:suitename]}/#{entry[:component]}"
148
+ action = true
149
+ end
150
+ end
151
+
152
+ if action
153
+ @metafile.create
154
+ end
155
+ end
156
+
157
+ # unlinks workload
158
+ def unlink(workload)
159
+ action = false
160
+
161
+ workload.each do |entry|
162
+ package = Package.new(entry[:destination_fullname], entry[:suitename], entry[:component])
163
+ package.delete_checksums
164
+
165
+ File.unlink(entry[:destination_fullname])
166
+ puts "Package: #{package.newbasename} unlinked"
167
+ action = true
168
+ end
169
+
170
+ if action
171
+ cleandirs
172
+ @metafile.create
173
+ end
174
+ end
175
+
176
+ # Create the checkpoint table
177
+ def create_checkpoints_table
178
+ sql = "create table if not exists checkpoints (
179
+ date varchar(25),
180
+ suitename varchar(10),
181
+ component varchar(10),
182
+ architecture varchar(10),
183
+ basename varchar(70)
184
+ )"
185
+ @cpdb.query(sql)
186
+ end
187
+
188
+ # Saves a checkpoint
189
+ def save_checkpoint
190
+ datetime = DateTime.now
191
+ source_category = "dists"
192
+
193
+ Architecture.dataset(source_category).each do |entry|
194
+ source = Architecture.new(entry[:architecture], entry[:component], entry[:suitename], source_category)
195
+ source.files.each do |fullname|
196
+ basename = File.basename(fullname)
197
+ @cpdb.query("insert into checkpoints values ( '#{datetime}', '#{entry[:suitename]}', '#{entry[:component]}', '#{entry[:architecture]}', '#{basename}' )")
198
+ end
199
+ end
200
+
201
+ puts "Checkpoint (#{datetime.strftime("%F %T")}) saved"
202
+ end
203
+
204
+ # Loads a checkpoint
205
+ def load_checkpoint(number)
206
+ list = get_checkpoints
207
+ link_workload = []
208
+ unlink_workload = []
209
+ source_category = "dists"
210
+
211
+ Architecture.dataset(source_category).each do |entry|
212
+ destination = Architecture.new(entry[:architecture], entry[:component], entry[:suitename], source_category)
213
+ destination.files.each do |fullname|
214
+ unlink_workload << {
215
+ :destination_fullname => fullname,
216
+ :component => entry[:component],
217
+ :suitename => entry[:suitename],
218
+ :architecture => entry[:architecture]
219
+ }
220
+ end
221
+ end
222
+
223
+ @cpdb.query("select date, suitename, component, architecture, basename from checkpoints").each do |row|
224
+ if row[0] == list[number]
225
+ suitename = row[1]
226
+ component = row[2]
227
+ architecture = row[3]
228
+ basename = row[4]
229
+ source = Architecture.new(architecture, component, suitename, "pool")
230
+ destination = Architecture.new(architecture, component, suitename, "dists")
231
+
232
+ link_workload << {
233
+ :source_fullname => File.join(source.directory, basename),
234
+ :destination_fullname => File.join(destination.directory, basename),
235
+ :component => component,
236
+ :suitename => suitename,
237
+ :architecture => architecture
238
+ }
239
+ end
240
+ end
241
+
242
+ unlink(unlink_workload)
243
+ link(link_workload)
244
+ end
245
+
246
+ # Returns a list of checkpoints for the cli
247
+ def get_checkpoints
248
+ order = 0
249
+ dates = []
250
+ list = {}
251
+
252
+ @cpdb.query("select date from checkpoints group by date order by date asc").each do |row|
253
+ dates << row.first
254
+ end
255
+
256
+ dates.each do |date|
257
+ order += 1
258
+ list[order] = date
259
+ end
260
+
261
+ list
262
+ end
263
+
264
+ # Returns a list of packages
265
+ def get_packagelist(category)
266
+ packages = []
267
+ if category.eql?("stage")
268
+ Component.dataset(category).each do |entry|
269
+ source = Component.new(entry[:component], entry[:suitename], category)
270
+ source.files.each do |fullname|
271
+ package = Package.new(fullname, entry[:suitename], entry[:component])
272
+
273
+ packages << {
274
+ :fullname => fullname,
275
+ :controlfile => package.controlfile,
276
+ :component => entry[:component],
277
+ :suitename => entry[:suitename]
278
+ }
279
+ end
280
+ end
281
+ else
282
+ Architecture.dataset(category).each do |entry|
283
+ source = Architecture.new(entry[:architecture], entry[:component], entry[:suitename], category)
284
+ source.files.each do |fullname|
285
+ package = Package.new(fullname, entry[:suitename], entry[:component])
286
+
287
+ packages << {
288
+ :fullname => fullname,
289
+ :controlfile => package.controlfile,
290
+ :component => entry[:component],
291
+ :suitename => entry[:suitename],
292
+ :architecture => entry[:architecture]
293
+ }
294
+ end
295
+ end
296
+ end
297
+ packages
298
+ end
299
+
300
+ # cleans up unused directories
301
+ def cleandirs
302
+ action = false
303
+
304
+ @repository.categories.each do |category|
305
+ next if category.eql?("stage")
306
+ Architecture.dataset(category).each do |entry|
307
+ directory = Architecture.new(entry[:architecture], entry[:component], entry[:suitename], category)
308
+ if directory.is_unused?(entry[:fullpath])
309
+ action = true
310
+ directory.destroy
311
+ end
312
+ end
313
+ Component.dataset(category).each do |entry|
314
+ directory = Component.new(entry[:component], entry[:suitename], category)
315
+ if directory.is_unused?(entry[:fullpath])
316
+ action = true
317
+ directory.destroy
318
+ end
319
+ end
320
+ Suite.dataset(category).each do |entry|
321
+ directory = Suite.new(entry[:suitename], category)
322
+ if directory.is_unused?(entry[:fullpath])
323
+ action = true
324
+ directory.destroy
325
+ end
326
+ end
327
+ end
328
+ if action
329
+ puts "Cleaning structure"
330
+ @metafile.create
331
+ end
332
+ end
333
+ end
334
+ end
335
+
@@ -0,0 +1,60 @@
1
+ # RepoMate module
2
+ module RepoMate
3
+
4
+ # Class for the category layer of the directory structure
5
+ class Category
6
+
7
+ # Init
8
+ def initialize(category)
9
+ @category = category
10
+ end
11
+
12
+ # Returns the name of the category (eg. pool, dists)
13
+ def name
14
+ @category
15
+ end
16
+
17
+ # Returns the full path of the categories directory
18
+ def directory
19
+ File.join(Cfg.rootdir, @category)
20
+ end
21
+
22
+ # Checks if the category directory exists
23
+ def exist?
24
+ Dir.exist?(directory)
25
+ end
26
+
27
+ # Creates the directory strcuture of the category
28
+ def create
29
+ FileUtils.mkdir_p(directory) unless exist?
30
+ end
31
+
32
+ # Deletes a categories directory
33
+ def destroy
34
+ FileUtils.rm_r(directory) if exist?
35
+ end
36
+
37
+ # Returns a dataset including the name of the category and the fullpath
38
+ def self.dataset(category=nil)
39
+ data = []
40
+ self.all.each do |entry|
41
+ unless entry.nil?
42
+ next unless entry.eql?(category) || category.eql?("all")
43
+ data << {
44
+ :category => entry,
45
+ :fullpath => File.join(Cfg.rootdir, entry)
46
+ }
47
+ end
48
+ end
49
+ data
50
+ end
51
+
52
+ # Returns all directories
53
+ def self.all
54
+ dirs = Dir.glob(File.join(Cfg.rootdir, "*"))
55
+ dirs.map{ |dir| File.basename(dir) unless dirs.include?(File.basename(dir)) }
56
+ end
57
+
58
+ end
59
+ end
60
+
@@ -0,0 +1,123 @@
1
+ require 'date'
2
+ require 'time'
3
+
4
+ # RepoMate module
5
+ module RepoMate
6
+
7
+ # Class for the commandline interface
8
+ class Cli
9
+
10
+ # Init
11
+ def initialize
12
+ @repomate = Base.new
13
+ @repository = Repository.new
14
+ end
15
+
16
+ # Sets up the base directory structure by calling the repository class
17
+ def setup(options)
18
+ if options.suitename?
19
+ @repository.create(options[:suitename], options[:component], options[:architecture])
20
+ else
21
+ STDERR.puts "Specify a suitename with [-s|--suitname]"
22
+ exit 1
23
+ end
24
+ end
25
+
26
+ # Adds a given package to the staging area by calling the base class
27
+ def stage(options, filename)
28
+ if options.suitename?
29
+ workload = []
30
+ workload << {:package_fullname => filename, :suitename => options[:suitename], :component => options[:component]}
31
+
32
+ puts "Package: #{filename} moving to stage => #{options[:suitename]}/#{options[:component]}"
33
+
34
+ @repomate.stage(workload)
35
+ else
36
+ STDERR.puts "Specify a suitename with [-s|--suitname]"
37
+ exit 1
38
+ end
39
+ end
40
+
41
+ # Get's all packages from the staging area. Packages need to be confirmed here.
42
+ def publish(options)
43
+ @repomate.prepare_publish.each do |entry|
44
+ workload = []
45
+ basename = File.basename(entry[:source_fullname])
46
+ suitename = entry[:suitename]
47
+ component = entry[:component]
48
+
49
+ unless options.force?
50
+ printf "\n%s", "Link #{basename} to production => #{suitename}/#{component}? [y|yes|n|no]: "
51
+ input = STDIN.gets
52
+ end
53
+
54
+ if options.force? || input =~ /(y|yes)/
55
+ workload << {
56
+ :source_fullname => entry[:source_fullname],
57
+ :destination_fullname => entry[:destination_fullname],
58
+ :component => entry[:component],
59
+ :suitename => entry[:suitename],
60
+ :architecture => entry[:architecture]
61
+ }
62
+ end
63
+ @repomate.publish(workload) unless workload.empty?
64
+ end
65
+ end
66
+
67
+ # Save a checkpoint
68
+ def save_checkpoint
69
+ # Add verification and some output here
70
+ @repomate.save_checkpoint
71
+ end
72
+
73
+ # List all packages, see cli output
74
+ def list_packagelist(options)
75
+ if options.repodir?
76
+ architecture = "unknown"
77
+
78
+ packages = @repomate.get_packagelist(options[:repodir])
79
+ packages.each do |package|
80
+ architecture = package[:architecture] if package[:architecture]
81
+ printf "%-50s%-20s%s\n", package[:controlfile]['Package'], package[:controlfile]['Version'], "#{package[:suitename]}/#{package[:component]}/#{architecture}"
82
+ end
83
+ else
84
+ STDERR.puts "Specify a category with [-r|--repodir]"
85
+ exit 1
86
+ end
87
+ end
88
+
89
+ # Choose a checkpoint to restore.
90
+ def choose_checkpoint
91
+ list = @repomate.get_checkpoints
92
+
93
+ if list.empty?
94
+ STDERR.puts "We can't restore because we don't have checkpoints"
95
+ exit 1
96
+ end
97
+
98
+ puts "\n*** Restore production links to a date below. ***
99
+ Remember: If you need to restore, the last entry might be the one you want!
100
+ Everything between the last two \"publish (-P) commands\" will be lost if you proceed!\n\n"
101
+
102
+ list.each do |num, date|
103
+ datetime = DateTime.parse(date)
104
+ puts "#{num}) #{datetime.strftime("%F %T")}"
105
+ end
106
+
107
+ printf "\n%s", "Enter number or [q|quit] to abord: "
108
+ input = STDIN.gets
109
+ number = input.to_i
110
+
111
+ if input =~ /(q|quit)/
112
+ puts "Aborting..."
113
+ exit 0
114
+ elsif list[number].nil?
115
+ STDERR.puts "Invalid number"
116
+ exit 1
117
+ else
118
+ @repomate.load_checkpoint(number)
119
+ end
120
+ end
121
+ end
122
+ end
123
+