aptly 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/aptly.rb +102 -0
- data/lib/aptly/error.rb +9 -0
- data/lib/aptly/hash.rb +9 -0
- data/lib/aptly/mirror.rb +150 -0
- data/lib/aptly/mutex.rb +78 -0
- data/lib/aptly/publish.rb +147 -0
- data/lib/aptly/repo.rb +255 -0
- data/lib/aptly/snapshot.rb +270 -0
- data/lib/aptly/string.rb +6 -0
- data/lib/aptly/version.rb +3 -0
- data/spec/aptly.conf +3 -0
- data/spec/aptly/aptly_spec.rb +9 -0
- data/spec/aptly/error_spec.rb +11 -0
- data/spec/aptly/hash_spec.rb +13 -0
- data/spec/aptly/mirror_spec.rb +56 -0
- data/spec/aptly/mutex_spec.rb +25 -0
- data/spec/aptly/repo_spec.rb +148 -0
- data/spec/aptly/snapshot_spec.rb +19 -0
- data/spec/aptly/string_spec.rb +13 -0
- data/spec/bin/aptly +3 -0
- data/spec/pkgs/pkg1_1.0.1-1_amd64.deb +0 -0
- data/spec/pkgs/pkg2_1.0.2-2_amd64.deb +0 -0
- data/spec/setup.sh +21 -0
- data/spec/spec_helper.rb +22 -0
- metadata +138 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d28012fc713f236f2439309dddc1d30fdb66338e
|
4
|
+
data.tar.gz: ee1285d3545517e13dd2dff95b3222b8a081babf
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4d9e6934ea4a9b23b2c3028110d39b8bcbcd7e07dc25132e47854d2bdb570961e32e514147759dd7f5274c28f85ff6f041663960e405479329e5cf2960fb5953
|
7
|
+
data.tar.gz: 3b39146ff8e28f1af97c602c4415a48948a666c6a66d5660aa7d2fbc170f29812af166613188b19e180c3ad5a42d930d61496112b037b6132ef1a9349e4478f1
|
data/lib/aptly.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'aptly/version'
|
2
|
+
require 'aptly/mutex'
|
3
|
+
require 'aptly/error'
|
4
|
+
require 'aptly/mirror'
|
5
|
+
require 'aptly/repo'
|
6
|
+
require 'aptly/snapshot'
|
7
|
+
require 'aptly/publish'
|
8
|
+
require 'aptly/string'
|
9
|
+
require 'aptly/hash'
|
10
|
+
|
11
|
+
module Aptly
|
12
|
+
|
13
|
+
# runcmd handles running arbitrary commands. It is intended to run aptly-
|
14
|
+
# specific commands, and as such, it uses a mutex to take gurantee
|
15
|
+
# consistency and avoid multiple processes modifying aptly simultaneously.
|
16
|
+
#
|
17
|
+
# == Parameters:
|
18
|
+
# cmd::
|
19
|
+
# A string holding the command to run
|
20
|
+
#
|
21
|
+
# == Returns:
|
22
|
+
# The content of stdout
|
23
|
+
#
|
24
|
+
def runcmd cmd
|
25
|
+
Mutex.lock
|
26
|
+
at_exit { Mutex.unlock }
|
27
|
+
|
28
|
+
out = %x(#{cmd} 2>&1)
|
29
|
+
res = $?.exitstatus
|
30
|
+
|
31
|
+
Mutex.unlock
|
32
|
+
|
33
|
+
if res != 0
|
34
|
+
raise AptlyError.new "aptly: command failed: #{cmd}", out
|
35
|
+
end
|
36
|
+
|
37
|
+
return out
|
38
|
+
end
|
39
|
+
|
40
|
+
# Parses the output lines of aptly listing commands
|
41
|
+
#
|
42
|
+
# == Parameters:
|
43
|
+
# lines::
|
44
|
+
# An array of lines of string output from aptly list commands
|
45
|
+
#
|
46
|
+
# == Returns:
|
47
|
+
# An array of items
|
48
|
+
#
|
49
|
+
def parse_list lines
|
50
|
+
items = Array.new
|
51
|
+
lines.each do |line|
|
52
|
+
if line.start_with?(' * ')
|
53
|
+
parts = line.split(/\[|\]/, 3)
|
54
|
+
items << parts[1] if parts.length == 3
|
55
|
+
end
|
56
|
+
end
|
57
|
+
items
|
58
|
+
end
|
59
|
+
|
60
|
+
# Parses the output of aptly show commands.
|
61
|
+
#
|
62
|
+
# == Parameters:
|
63
|
+
# lines::
|
64
|
+
# An array of strings of aptly output
|
65
|
+
#
|
66
|
+
# == Returns:
|
67
|
+
# A hash of information
|
68
|
+
#
|
69
|
+
def parse_info lines
|
70
|
+
items = Hash.new
|
71
|
+
lines.reject{|l| l.empty?}.each do |line|
|
72
|
+
parts = line.split(/:\s/, 2)
|
73
|
+
items[parts[0]] = parts[1].strip if parts.length == 2
|
74
|
+
end
|
75
|
+
|
76
|
+
def multi items, key
|
77
|
+
items[key] = items[key].split(' ') if items.has_key? key
|
78
|
+
end
|
79
|
+
|
80
|
+
multi items, 'Components'
|
81
|
+
multi items, 'Architectures'
|
82
|
+
|
83
|
+
items
|
84
|
+
end
|
85
|
+
|
86
|
+
# Parses aptly output of double-space indented lists
|
87
|
+
#
|
88
|
+
# == Parameters:
|
89
|
+
# lines::
|
90
|
+
# Output lines from an aptly list command
|
91
|
+
#
|
92
|
+
# == Result:
|
93
|
+
# An array of items
|
94
|
+
#
|
95
|
+
def parse_indented_list lines
|
96
|
+
items = Array.new
|
97
|
+
lines.each do |line|
|
98
|
+
items << line.strip if line.start_with? ' '
|
99
|
+
end
|
100
|
+
items
|
101
|
+
end
|
102
|
+
end
|
data/lib/aptly/error.rb
ADDED
data/lib/aptly/hash.rb
ADDED
data/lib/aptly/mirror.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
module Aptly
|
2
|
+
extend self
|
3
|
+
|
4
|
+
# Creates a new mirror in aptly. This simply creates the necessary records in
|
5
|
+
# leveldb and doesn't do any heavy lifting.
|
6
|
+
#
|
7
|
+
# == Parameters:
|
8
|
+
# name::
|
9
|
+
# The name of the new repository
|
10
|
+
# baseurl::
|
11
|
+
# The URL to the repository content
|
12
|
+
# dist::
|
13
|
+
# The distribution (e.g. precise, quantal)
|
14
|
+
# components::
|
15
|
+
# The repository components (e.g. main, stable)
|
16
|
+
# archlist::
|
17
|
+
# A list of architecture types to mirror
|
18
|
+
# ignoresigs::
|
19
|
+
# Ignore package signature mismatches
|
20
|
+
# source::
|
21
|
+
# Optionally mirror in source packages
|
22
|
+
#
|
23
|
+
# == Returns:
|
24
|
+
# An Aptly::Mirror object
|
25
|
+
#
|
26
|
+
def create_mirror(
|
27
|
+
name,
|
28
|
+
baseurl,
|
29
|
+
dist,
|
30
|
+
kwargs={}
|
31
|
+
)
|
32
|
+
components = kwargs.arg :components, []
|
33
|
+
archlist = kwargs.arg :archlist, []
|
34
|
+
ignoresigs = kwargs.arg :ignoresigs, false
|
35
|
+
source = kwargs.arg :source, false
|
36
|
+
|
37
|
+
if list_mirrors.include? name
|
38
|
+
raise AptlyError.new "Mirror '#{name}' already exists"
|
39
|
+
end
|
40
|
+
|
41
|
+
if components.length < 1
|
42
|
+
raise AptlyError.new "1 or more components are required"
|
43
|
+
end
|
44
|
+
|
45
|
+
cmd = 'aptly mirror create'
|
46
|
+
cmd += " -architectures #{archlist.join(',')}" if !archlist.empty?
|
47
|
+
cmd += ' -ignore-signatures' if ignoresigs
|
48
|
+
cmd += ' -with-sources' if source
|
49
|
+
cmd += " #{name.quote} #{baseurl.quote} #{dist.quote}"
|
50
|
+
cmd += " #{components.join(' ')}"
|
51
|
+
|
52
|
+
runcmd cmd
|
53
|
+
return Mirror.new name
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns a list of existing mirrors in aptly
|
57
|
+
#
|
58
|
+
# == Returns
|
59
|
+
# An array of mirror names
|
60
|
+
#
|
61
|
+
def list_mirrors
|
62
|
+
out = runcmd 'aptly mirror list'
|
63
|
+
parse_list out.lines
|
64
|
+
end
|
65
|
+
|
66
|
+
# Retrieves information about a mirror
|
67
|
+
#
|
68
|
+
# == Parameters:
|
69
|
+
# name::
|
70
|
+
# The name of the mirror to retrieve info for
|
71
|
+
#
|
72
|
+
# == Returns:
|
73
|
+
# A hash of mirror information
|
74
|
+
#
|
75
|
+
def mirror_info name
|
76
|
+
out = runcmd "aptly mirror show #{name.quote}"
|
77
|
+
parse_info out.lines
|
78
|
+
end
|
79
|
+
|
80
|
+
class Mirror
|
81
|
+
attr_accessor :name, :baseurl, :dist, :components, :archlist
|
82
|
+
|
83
|
+
@name = ''
|
84
|
+
@baseurl = ''
|
85
|
+
@dist = ''
|
86
|
+
@components = []
|
87
|
+
@archlist = []
|
88
|
+
|
89
|
+
# Instantiates a new Mirror object
|
90
|
+
#
|
91
|
+
# == Parameters:
|
92
|
+
# name::
|
93
|
+
# Then name associated with the mirror
|
94
|
+
#
|
95
|
+
def initialize name
|
96
|
+
if !Aptly::list_mirrors.include? name
|
97
|
+
raise AptlyError.new("Mirror '#{name}' does not exist")
|
98
|
+
end
|
99
|
+
|
100
|
+
info = Aptly::mirror_info name
|
101
|
+
@name = info['Name']
|
102
|
+
@baseurl = info['Archive Root URL']
|
103
|
+
@dist = info['Distribution']
|
104
|
+
@components = info['Components']
|
105
|
+
@archlist = info['Architectures']
|
106
|
+
end
|
107
|
+
|
108
|
+
# Drops an existing mirror from aptly's configuration
|
109
|
+
def drop
|
110
|
+
Aptly::runcmd "aptly mirror drop #{@name.quote}"
|
111
|
+
end
|
112
|
+
|
113
|
+
# List all packages contained in a mirror
|
114
|
+
#
|
115
|
+
# == Returns:
|
116
|
+
# An array of packages
|
117
|
+
#
|
118
|
+
def list_packages
|
119
|
+
res = []
|
120
|
+
out = Aptly::runcmd "aptly mirror show -with-packages #{@name.quote}"
|
121
|
+
Aptly::parse_indented_list out.lines
|
122
|
+
end
|
123
|
+
|
124
|
+
# Shortcut method to snapshot an Aptly::Mirror object
|
125
|
+
def snapshot name
|
126
|
+
Aptly::create_mirror_snapshot name, @name
|
127
|
+
end
|
128
|
+
|
129
|
+
# Updates a repository, syncing in all packages which have not already been
|
130
|
+
# downloaded and caches them locally.
|
131
|
+
#
|
132
|
+
# == Parameters:
|
133
|
+
# ignore_cksum::
|
134
|
+
# Ignore checksum mismatches
|
135
|
+
# ignore_sigs::
|
136
|
+
# Ignore author signature mismatches
|
137
|
+
#
|
138
|
+
def update kwargs={}
|
139
|
+
ignore_cksum = kwargs.arg :ignore_cksum, false
|
140
|
+
ignore_sigs = kwargs.arg :ignore_sigs, false
|
141
|
+
|
142
|
+
cmd = 'aptly mirror update'
|
143
|
+
cmd += ' -ignore-checksums' if ignore_cksum
|
144
|
+
cmd += ' -ignore-signatures' if ignore_sigs
|
145
|
+
cmd += " #{@name.quote}"
|
146
|
+
|
147
|
+
Aptly::runcmd cmd
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
data/lib/aptly/mutex.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
module Aptly
|
2
|
+
extend self
|
3
|
+
|
4
|
+
class Mutex
|
5
|
+
@@mutex_path = '/tmp/aptly.lock'
|
6
|
+
|
7
|
+
# Static attribute accessor for mutex path
|
8
|
+
#
|
9
|
+
# == Returns:
|
10
|
+
# The path to the aptly mutex file
|
11
|
+
#
|
12
|
+
def self.mutex_path
|
13
|
+
@@mutex_path
|
14
|
+
end
|
15
|
+
|
16
|
+
# Alter the mutex path. This should be set to the same path in all places
|
17
|
+
# where ruby-aptly will be used on the same host, since the mutex is meant
|
18
|
+
# to be system-wide.
|
19
|
+
#
|
20
|
+
# == Parameters:
|
21
|
+
# path::
|
22
|
+
# The desired path for the mutex
|
23
|
+
#
|
24
|
+
def self.mutex_path= path
|
25
|
+
@@mutex_path = path
|
26
|
+
end
|
27
|
+
|
28
|
+
# Attempts to acquire the aptly mutex. This method will wait if the mutex
|
29
|
+
# is already locked elsewhere, and check back every 5 seconds to see if it
|
30
|
+
# has been freed. On each check where the mutex is determined to be in use,
|
31
|
+
# we check if the process which originally acquired the lock is still
|
32
|
+
# running so we can acquire a stale mutex should we need to.
|
33
|
+
#
|
34
|
+
# == Returns:
|
35
|
+
# True, once the lock is acquired.
|
36
|
+
#
|
37
|
+
def self.lock
|
38
|
+
while self.locked?
|
39
|
+
self.running? && sleep(5) || self.unlock
|
40
|
+
end
|
41
|
+
File.open(@@mutex_path, 'w') {|f| f.write Process.pid}
|
42
|
+
end
|
43
|
+
|
44
|
+
# Checks if the mutex is in use. This is done via a simple file check.
|
45
|
+
#
|
46
|
+
# == Returns:
|
47
|
+
# True if locked, else false
|
48
|
+
#
|
49
|
+
def self.locked?
|
50
|
+
File.exist? @@mutex_path
|
51
|
+
end
|
52
|
+
|
53
|
+
# Checks if the process which originally acquired the mutex is still
|
54
|
+
# running. This is useful to determine staleness of a locked mutex.
|
55
|
+
#
|
56
|
+
# == Returns:
|
57
|
+
# True if running, else false
|
58
|
+
#
|
59
|
+
def self.running?
|
60
|
+
begin
|
61
|
+
pid = File.open(@@mutex_path).read.to_i
|
62
|
+
Process.kill(0, pid)
|
63
|
+
return true
|
64
|
+
rescue Errno::ESRCH, Errno::ENOENT
|
65
|
+
return false
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Unlocks the mutex so that other processes may acquire it.
|
70
|
+
#
|
71
|
+
# == Returns:
|
72
|
+
# True if unlock happens, else false
|
73
|
+
#
|
74
|
+
def self.unlock
|
75
|
+
File.delete @@mutex_path if self.locked?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
module Aptly
|
2
|
+
extend self
|
3
|
+
|
4
|
+
# Publish a snapshot or repo resource.
|
5
|
+
#
|
6
|
+
# == Parameters:
|
7
|
+
# type::
|
8
|
+
# The type of resource to publish. 'snapshot' and 'repo' supported.
|
9
|
+
# name::
|
10
|
+
# The name of the resource to publish.
|
11
|
+
# prefix::
|
12
|
+
# An optional prefix to publish to.
|
13
|
+
# component::
|
14
|
+
# The component to publish to. If empty, aptly will attempt to guess
|
15
|
+
# from the source, or use 'main' as the default.
|
16
|
+
# dist::
|
17
|
+
# The distribution name. If empty, aptly will try to guess.
|
18
|
+
# gpg_key::
|
19
|
+
# The gpg key to sign the repository. Uses default if not specified.
|
20
|
+
# keyring::
|
21
|
+
# GPG keyring to use
|
22
|
+
# label::
|
23
|
+
# An optional label to give the published resource
|
24
|
+
# origin::
|
25
|
+
# The value of the 'Origin' field
|
26
|
+
# secret_keyring::
|
27
|
+
# GPG secret keyring to use
|
28
|
+
# sign::
|
29
|
+
# When false, don't sign Release files. Defaults to true.
|
30
|
+
#
|
31
|
+
def publish(
|
32
|
+
type,
|
33
|
+
name,
|
34
|
+
kwargs={}
|
35
|
+
)
|
36
|
+
prefix = kwargs.arg :prefix, ''
|
37
|
+
component = kwargs.arg :component, ''
|
38
|
+
dist = kwargs.arg :dist, ''
|
39
|
+
gpg_key = kwargs.arg :gpg_key, ''
|
40
|
+
keyring = kwargs.arg :keyring, ''
|
41
|
+
label = kwargs.arg :label, ''
|
42
|
+
origin = kwargs.arg :origin, ''
|
43
|
+
secret_keyring = kwargs.arg :secret_keyring, ''
|
44
|
+
sign = kwargs.arg :sign, true
|
45
|
+
|
46
|
+
if type != 'repo' && type != 'snapshot'
|
47
|
+
raise AptlyError "Invalid publish type: #{type}"
|
48
|
+
end
|
49
|
+
|
50
|
+
cmd = "aptly publish #{type}"
|
51
|
+
cmd += ' -skip-signing' if !sign
|
52
|
+
cmd += " -component #{component.quote}" if !component.empty?
|
53
|
+
cmd += " -distribution #{dist.quote}" if !dist.empty?
|
54
|
+
cmd += " -gpg-key #{gpg_key.quote}" if !gpg_key.empty?
|
55
|
+
cmd += " -keyring #{keyring.quote}" if !keyring.empty?
|
56
|
+
cmd += " -label #{label.quote}" if !label.empty?
|
57
|
+
cmd += " -origin #{origin.quote}" if !origin.empty?
|
58
|
+
cmd += " #{name.quote}"
|
59
|
+
cmd += " #{prefix.quote}" if !prefix.empty?
|
60
|
+
if !secret_keyring.empty?
|
61
|
+
cmd += " -secret-keyring #{secret_keyring.quote}"
|
62
|
+
end
|
63
|
+
|
64
|
+
runcmd cmd
|
65
|
+
end
|
66
|
+
|
67
|
+
# List existing published resources.
|
68
|
+
#
|
69
|
+
# == Returns
|
70
|
+
# A hash of published resource information. The outer hash key is the
|
71
|
+
# published resource path, and its value is the resource metadata.
|
72
|
+
#
|
73
|
+
def list_published
|
74
|
+
res = Hash.new
|
75
|
+
out = runcmd 'aptly publish list'
|
76
|
+
out.lines.each do |line|
|
77
|
+
if line.start_with? ' * '
|
78
|
+
resource = {}
|
79
|
+
parts = line[3..-1].split(/\[|\]|\(|\)/)
|
80
|
+
resource['path'] = parts[0].strip
|
81
|
+
resource['component'] = parts[1].strip
|
82
|
+
resource['archlist'] = parts[3].split(', ')
|
83
|
+
resource['from_name'] = parts[5].strip
|
84
|
+
if parts[6].include? 'Snapshot'
|
85
|
+
resource['from_type'] = 'snapshot'
|
86
|
+
elsif parts[6].include? 'Repo'
|
87
|
+
resource['from_type'] = 'repo'
|
88
|
+
else
|
89
|
+
next
|
90
|
+
end
|
91
|
+
dist = line.split.last
|
92
|
+
res[dist] = resource
|
93
|
+
end
|
94
|
+
end
|
95
|
+
res
|
96
|
+
end
|
97
|
+
|
98
|
+
class PublishedResource
|
99
|
+
attr_accessor :path, :from_name, :from_type, :component
|
100
|
+
attr_accessor :archlist, :dist
|
101
|
+
|
102
|
+
@path = ''
|
103
|
+
@from_name = ''
|
104
|
+
@from_type = ''
|
105
|
+
@component = ''
|
106
|
+
@archlist = []
|
107
|
+
@dist = ''
|
108
|
+
|
109
|
+
# Instantiates a new published resource object from an existing published
|
110
|
+
# resource.
|
111
|
+
#
|
112
|
+
# == Parameters:
|
113
|
+
# dist::
|
114
|
+
# The distribution name
|
115
|
+
# prefix::
|
116
|
+
# An optional prefix
|
117
|
+
#
|
118
|
+
# == Returns:
|
119
|
+
# An Aptly::PublishedResource object
|
120
|
+
#
|
121
|
+
def initialize dist, prefix=''
|
122
|
+
idx = "#{prefix}#{prefix.empty? ? '' : '/'}#{dist}"
|
123
|
+
|
124
|
+
published = Aptly::list_published
|
125
|
+
if !published.include? idx
|
126
|
+
raise AptlyError.new "Published resource #{idx} does not exist"
|
127
|
+
end
|
128
|
+
|
129
|
+
@dist = dist
|
130
|
+
@prefix = prefix
|
131
|
+
@path = published[idx]['path']
|
132
|
+
@from_name = published[idx]['from_name']
|
133
|
+
@from_type = published[idx]['from_type']
|
134
|
+
@component = published[idx]['component']
|
135
|
+
@archlist = published[idx]['archlist']
|
136
|
+
end
|
137
|
+
|
138
|
+
# Unpublish a currently published resource.
|
139
|
+
def drop
|
140
|
+
cmd = 'aptly publish drop'
|
141
|
+
cmd += " #{@dist.quote}"
|
142
|
+
cmd += " #{@prefix.quote}" if !@prefix.empty?
|
143
|
+
|
144
|
+
Aptly::runcmd cmd
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|