]> git.mxchange.org Git - quix0rs-apt-p2p.git/blob - AptPackages.py
Add tracking of index file hashes from Release files.
[quix0rs-apt-p2p.git] / AptPackages.py
1 #
2 # Copyright (C) 2002 Manuel Estrada Sainz <ranty@debian.org>
3 #
4 # This library is free software; you can redistribute it and/or
5 # modify it under the terms of version 2.1 of the GNU Lesser General Public
6 # License as published by the Free Software Foundation.
7 #
8 # This library is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11 # Lesser General Public License for more details.
12 #
13 # You should have received a copy of the GNU Lesser General Public
14 # License along with this library; if not, write to the Free Software
15 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
17 import apt_pkg, apt_inst, sys, os, stat
18 from os.path import dirname, basename
19 import re, shelve, shutil, fcntl
20 from twisted.internet import process
21 from twisted.python import log
22 import copy, UserDict
23 from twisted.trial import unittest
24
25 aptpkg_dir='.apt-dht'
26 apt_pkg.init()
27
28 class AptDpkgInfo(UserDict.UserDict):
29     """
30     Gets control fields from a .deb file.
31
32     And then behaves like a regular python dictionary.
33
34     See AptPackages.get_mirror_path
35     """
36
37     def __init__(self, filename):
38         UserDict.UserDict.__init__(self)
39         try:
40             filehandle = open(filename);
41             try:
42                 self.control = apt_inst.debExtractControl(filehandle)
43             finally:
44                 # Make sure that file is always closed.
45                 filehandle.close()
46         except SystemError:
47             log.msg("Had problems reading: %s"%(filename))
48             raise
49         for line in self.control.split('\n'):
50             if line.find(': ') != -1:
51                 key, value = line.split(': ', 1)
52                 self.data[key] = value
53
54 class PackageFileList(UserDict.DictMixin):
55     """
56     Manages a list of package files belonging to a backend
57     """
58     def __init__(self, backendName, cache_dir):
59         self.cache_dir = cache_dir
60         self.packagedb_dir = cache_dir+'/'+ aptpkg_dir + \
61                            '/backends/' + backendName
62         if not os.path.exists(self.packagedb_dir):
63             os.makedirs(self.packagedb_dir)
64         self.packages = None
65         self.open()
66
67     def open(self):
68         if self.packages is None:
69             self.packages = shelve.open(self.packagedb_dir+'/packages.db')
70
71     def close(self):
72         if self.packages is not None:
73             self.packages.close()
74
75     def update_file(self, filename, cache_path, file_path):
76         """
77         Called from apt_proxy.py when files get updated so we can update our
78         fake lists/ directory and sources.list.
79         """
80         if filename=="Packages" or filename=="Release" or filename=="Sources":
81             log.msg("Registering package file: "+cache_path)
82             self.packages[cache_path] = file_path
83             return True
84         return False
85
86     def check_files(self):
87         """
88         Check all files in the database to make sure it exists.
89         """
90         files = self.packages.keys()
91         #print self.packages.keys()
92         for f in files:
93             if not os.path.exists(self.packages[f]):
94                 log.msg("File in packages database has been deleted: "+f)
95                 del self.packages[f]
96                 
97     def __getitem__(self, key): return self.packages[key]
98     def __setitem__(self, key, item): self.packages[key] = item
99     def __delitem__(self, key): del self.packages[key]
100     def keys(self): return self.packages.keys()
101
102 class AptPackages:
103     """
104     Uses AptPackagesServer to answer queries about packages.
105
106     Makes a fake configuration for python-apt for each backend.
107     """
108     DEFAULT_APT_CONFIG = {
109         #'APT' : '',
110         #'APT::Architecture' : 'amd64',  # TODO: Fix this, see bug #436011 and #285360
111         #'APT::Default-Release' : 'unstable',
112    
113         'Dir':'.', # /
114         'Dir::State' : 'apt/', # var/lib/apt/
115         'Dir::State::Lists': 'lists/', # lists/
116         #'Dir::State::cdroms' : 'cdroms.list',
117         'Dir::State::userstatus' : 'status.user',
118         'Dir::State::status': 'dpkg/status', # '/var/lib/dpkg/status'
119         'Dir::Cache' : '.apt/cache/', # var/cache/apt/
120         #'Dir::Cache::archives' : 'archives/',
121         'Dir::Cache::srcpkgcache' : 'srcpkgcache.bin',
122         'Dir::Cache::pkgcache' : 'pkgcache.bin',
123         'Dir::Etc' : 'apt/etc/', # etc/apt/
124         'Dir::Etc::sourcelist' : 'sources.list',
125         'Dir::Etc::vendorlist' : 'vendors.list',
126         'Dir::Etc::vendorparts' : 'vendors.list.d',
127         #'Dir::Etc::main' : 'apt.conf',
128         #'Dir::Etc::parts' : 'apt.conf.d',
129         #'Dir::Etc::preferences' : 'preferences',
130         'Dir::Bin' : '',
131         #'Dir::Bin::methods' : '', #'/usr/lib/apt/methods'
132         'Dir::Bin::dpkg' : '/usr/bin/dpkg',
133         #'DPkg' : '',
134         #'DPkg::Pre-Install-Pkgs' : '',
135         #'DPkg::Tools' : '',
136         #'DPkg::Tools::Options' : '',
137         #'DPkg::Tools::Options::/usr/bin/apt-listchanges' : '',
138         #'DPkg::Tools::Options::/usr/bin/apt-listchanges::Version' : '2',
139         #'DPkg::Post-Invoke' : '',
140         }
141     essential_dirs = ('apt', 'apt/cache', 'apt/dpkg', 'apt/etc', 'apt/lists',
142                       'apt/lists/partial')
143     essential_files = ('apt/dpkg/status', 'apt/etc/sources.list',)
144         
145     def __init__(self, backendName, cache_dir):
146         """
147         Construct new packages manager
148         backend: Name of backend associated with this packages file
149         cache_dir: cache directory from config file
150         """
151         self.backendName = backendName
152         self.cache_dir = cache_dir
153         self.apt_config = copy.deepcopy(self.DEFAULT_APT_CONFIG)
154
155         self.status_dir = (cache_dir+'/'+ aptpkg_dir
156                            +'/backends/'+backendName)
157         for dir in self.essential_dirs:
158             path = self.status_dir+'/'+dir
159             if not os.path.exists(path):
160                 os.makedirs(path)
161         for file in self.essential_files:
162             path = self.status_dir+'/'+file
163             if not os.path.exists(path):
164                 f = open(path,'w')
165                 f.close()
166                 del f
167                 
168         self.apt_config['Dir'] = self.status_dir
169         self.apt_config['Dir::State::status'] = self.status_dir + '/apt/dpkg/status'
170         #os.system('find '+self.status_dir+' -ls ')
171         #print "status:"+self.apt_config['Dir::State::status']
172         self.packages = PackageFileList(backendName, cache_dir)
173         self.indexrecords = {}
174         self.loaded = 0
175         #print "Loaded aptPackages [%s] %s " % (self.backendName, self.cache_dir)
176         
177     def __del__(self):
178         self.cleanup()
179         #print "start aptPackages [%s] %s " % (self.backendName, self.cache_dir)
180         self.packages.close()
181         #print "Deleted aptPackages [%s] %s " % (self.backendName, self.cache_dir)
182         
183     def addRelease(self, cache_path, file_path):
184         """
185         Dirty hack until python-apt supports apt-pkg/indexrecords.h
186         (see Bug #456141)
187         """
188         self.indexrecords[cache_path] = {}
189
190         read_packages = False
191         f = open(file_path, 'r')
192         
193         for line in f:
194             line = line.rstrip()
195     
196             if line[:1] != " ":
197                 read_packages = False
198                 try:
199                     # Read the various headers from the file
200                     h, v = line.split(":", 1)
201                     if h == "MD5Sum" or h == "SHA1" or h == "SHA256":
202                         read_packages = True
203                         hash_type = h
204                 except:
205                     # Bad header line, just ignore it
206                     log.msg("WARNING: Ignoring badly formatted Release line: %s" % line)
207     
208                 # Skip to the next line
209                 continue
210             
211             # Read file names from the multiple hash sections of the file
212             if read_packages:
213                 p = line.split()
214                 self.indexrecords[cache_path].setdefault(p[2], {})[hash_type] = (p[0], p[1])
215         
216         f.close()
217
218     def file_updated(self, filename, cache_path, file_path):
219         """
220         A file in the backend has changed.  If this affects us, unload our apt database
221         """
222         if filename == "Release":
223             self.addRelease(cache_path, file_path)
224         if self.packages.update_file(filename, cache_path, file_path):
225             self.unload()
226
227     def __save_stdout(self):
228         self.real_stdout_fd = os.dup(1)
229         os.close(1)
230                 
231     def __restore_stdout(self):
232         os.dup2(self.real_stdout_fd, 1)
233         os.close(self.real_stdout_fd)
234         del self.real_stdout_fd
235
236     def load(self):
237         """
238         Regenerates the fake configuration and load the packages server.
239         """
240         if self.loaded: return True
241         apt_pkg.InitSystem()
242         #print "Load:", self.status_dir
243         shutil.rmtree(self.status_dir+'/apt/lists/')
244         os.makedirs(self.status_dir+'/apt/lists/partial')
245         sources_filename = self.status_dir+'/'+'apt/etc/sources.list'
246         sources = open(sources_filename, 'w')
247         sources_count = 0
248         self.packages.check_files()
249         for f in self.packages:
250             # we should probably clear old entries from self.packages and
251             # take into account the recorded mtime as optimization
252             filepath = self.packages[f]
253             fake_uri='http://apt-dht/'+f
254             if f.endswith('Sources'):
255                 source_line='deb-src '+dirname(fake_uri)+'/ /'
256             else:
257                 source_line='deb '+dirname(fake_uri)+'/ /'
258             listpath=(self.status_dir+'/apt/lists/'
259                     +apt_pkg.URItoFileName(fake_uri))
260             sources.write(source_line+'\n')
261             log.msg("Sources line: " + source_line)
262             sources_count = sources_count + 1
263
264             try:
265                 #we should empty the directory instead
266                 os.unlink(listpath)
267             except:
268                 pass
269             os.symlink(self.packages[f], listpath)
270         sources.close()
271
272         if sources_count == 0:
273             log.msg("No Packages files available for %s backend"%(self.backendName))
274             return False
275
276         log.msg("Loading Packages database for "+self.status_dir)
277         #apt_pkg.Config = apt_pkg.newConfiguration(); #-- this causes unit tests to fail!
278         for key, value in self.apt_config.items():
279             apt_pkg.Config[key] = value
280 #         print "apt_pkg config:"
281 #         for I in apt_pkg.Config.keys():
282 #            print "%s \"%s\";"%(I,apt_pkg.Config[I]);
283
284         # apt_pkg prints progress messages to stdout, disable
285         self.__save_stdout()
286         try:
287             self.cache = apt_pkg.GetCache()
288         finally:
289             self.__restore_stdout()
290
291         self.records = apt_pkg.GetPkgRecords(self.cache)
292         self.srcrecords = apt_pkg.GetPkgSrcRecords()
293         #for p in self.cache.Packages:
294         #    print p
295         #log.debug("%s packages found" % (len(self.cache)),'apt_pkg')
296         self.loaded = 1
297         return True
298
299     def unload(self):
300         "Tries to make the packages server quit."
301         if self.loaded:
302             del self.cache
303             del self.records
304             del self.srcrecords
305             self.loaded = 0
306
307     def cleanup(self):
308         self.unload()
309         self.packages.close()
310
311     def get_mirror_path(self, name, version):
312         "Find the path for version 'version' of package 'name'"
313         if not self.load(): return None
314         try:
315             for pack_vers in self.cache[name].VersionList:
316                 if(pack_vers.VerStr == version):
317                     file, index = pack_vers.FileList[0]
318                     self.records.Lookup((file,index))
319                     path = self.records.FileName
320                     if len(path)>2 and path[0:2] == './': 
321                         path = path[2:] # Remove any leading './'
322                     return path
323
324         except KeyError:
325             pass
326         return None
327       
328
329     def get_mirror_versions(self, package_name):
330         """
331         Find the available versions of the package name given
332         @type package_name: string
333         @param package_name: package name to search for e.g. ;apt'
334         @return: A list of mirror versions available
335
336         """
337         vers = []
338         if not self.load(): return vers
339         try:
340             for pack_vers in self.cache[package_name].VersionList:
341                 vers.append(pack_vers.VerStr)
342         except KeyError:
343             pass
344         return vers
345
346
347 def cleanup(factory):
348     for backend in factory.backends.values():
349         backend.get_packages_db().cleanup()
350
351 def get_mirror_path(factory, file):
352     """
353     Look for the path of 'file' in all backends.
354     """
355     info = AptDpkgInfo(file)
356     paths = []
357     for backend in factory.backends.values():
358         path = backend.get_packages_db().get_mirror_path(info['Package'],
359                                                 info['Version'])
360         if path:
361             paths.append('/'+backend.base+'/'+path)
362     return paths
363
364 def get_mirror_versions(factory, package):
365     """
366     Look for the available version of a package in all backends, given
367     an existing package name
368     """
369     all_vers = []
370     for backend in factory.backends.values():
371         vers = backend.get_packages_db().get_mirror_versions(package)
372         for ver in vers:
373             path = backend.get_packages_db().get_mirror_path(package, ver)
374             all_vers.append((ver, "%s/%s"%(backend.base,path)))
375     return all_vers
376
377 def closest_match(info, others):
378     def compare(a, b):
379         return apt_pkg.VersionCompare(a[0], b[0])
380
381     others.sort(compare)
382     version = info['Version']
383     match = None
384     for ver,path in others:
385         if version <= ver:
386             match = path
387             break
388     if not match:
389         if not others:
390             return None
391         match = others[-1][1]
392
393     dirname=re.sub(r'/[^/]*$', '', match)
394     version=re.sub(r'^[^:]*:', '', info['Version'])
395     if dirname.find('/pool/') != -1:
396         return "/%s/%s_%s_%s.deb"%(dirname, info['Package'],
397                                   version, info['Architecture'])
398     else:
399         return "/%s/%s_%s.deb"%(dirname, info['Package'], version)
400
401 def import_directory(factory, dir, recursive=0):
402     """
403     Import all files in a given directory into the cache
404     This is used by apt-proxy-import to import new files
405     into the cache
406     """
407     imported_count  = 0
408
409     if not os.path.exists(dir):
410         log.err('Directory ' + dir + ' does not exist')
411         return
412
413     if recursive:    
414         log.msg("Importing packages from directory tree: " + dir)
415         for root, dirs, files in os.walk(dir):
416             for file in files:
417                 imported_count += import_file(factory, root, file)
418     else:
419         log.msg("Importing packages from directory: " + dir)
420         for file in os.listdir(dir):
421             mode = os.stat(dir + '/' + file)[stat.ST_MODE]
422             if not stat.S_ISDIR(mode):
423                 imported_count += import_file(factory, dir, file)
424
425     for backend in factory.backends.values():
426         backend.get_packages_db().unload()
427
428     log.msg("Imported %s files" % (imported_count))
429     return imported_count
430
431 def import_file(factory, dir, file):
432     """
433     Import a .deb or .udeb into cache from given filename
434     """
435     if file[-4:]!='.deb' and file[-5:]!='.udeb':
436         log.msg("Ignoring (unknown file type):"+ file)
437         return 0
438     
439     log.msg("considering: " + dir + '/' + file)
440     try:
441         paths = get_mirror_path(factory, dir+'/'+file)
442     except SystemError:
443         log.msg(file + ' skipped - wrong format or corrupted')
444         return 0
445     if paths:
446         if len(paths) != 1:
447             log.msg("WARNING: multiple ocurrences")
448             log.msg(str(paths), 'import')
449         cache_path = paths[0]
450     else:
451         log.msg("Not found, trying to guess")
452         info = AptDpkgInfo(dir+'/'+file)
453         cache_path = closest_match(info,
454                                 get_mirror_versions(factory, info['Package']))
455     if cache_path:
456         log.msg("MIRROR_PATH:"+ cache_path)
457         src_path = dir+'/'+file
458         dest_path = factory.config.cache_dir+cache_path
459         
460         if not os.path.exists(dest_path):
461             log.msg("IMPORTING:" + src_path)
462             dest_path = re.sub(r'/\./', '/', dest_path)
463             if not os.path.exists(dirname(dest_path)):
464                 os.makedirs(dirname(dest_path))
465             f = open(dest_path, 'w')
466             fcntl.lockf(f.fileno(), fcntl.LOCK_EX)
467             f.truncate(0)
468             shutil.copy2(src_path, dest_path)
469             f.close()
470             if hasattr(factory, 'access_times'):
471                 atime = os.stat(src_path)[stat.ST_ATIME]
472                 factory.access_times[cache_path] = atime
473             log.msg(file + ' imported')
474             return 1
475         else:
476             log.msg(file + ' skipped - already in cache')
477             return 0
478
479     else:
480         log.msg(file + ' skipped - no suitable backend found')
481         return 0
482             
483 class TestAptPackages(unittest.TestCase):
484     """Unit tests for the AptPackages cache."""
485     
486     pending_calls = []
487     client = None
488     packagesFile = ''
489     sourcesFile = ''
490     releaseFile = ''
491     
492     def setUp(self):
493         self.client = AptPackages('whatever', '/tmp')
494     
495         self.packagesFile = os.popen('ls -Sr /var/lib/apt/lists/ | grep -E "Packages$" | tail -n 1').read().rstrip('\n')
496         self.sourcesFile = os.popen('ls -Sr /var/lib/apt/lists/ | grep -E "Sources$" | tail -n 1').read().rstrip('\n')
497         for f in os.walk('/var/lib/apt/lists').next()[2]:
498             if f[-7:] == "Release" and self.packagesFile.startswith(f[:-7]):
499                 self.releaseFile = f
500                 break
501         
502         self.client.file_updated('Release', 
503                                  self.releaseFile[self.releaseFile.find('_debian_')+1:].replace('_','/'), 
504                                  '/var/lib/apt/lists/' + self.releaseFile)
505         self.client.file_updated('Packages', 
506                                  self.packagesFile[self.packagesFile.find('_debian_')+1:].replace('_','/'), 
507                                  '/var/lib/apt/lists/' + self.packagesFile)
508         self.client.file_updated('Sources', 
509                                  self.sourcesFile[self.sourcesFile.find('_debian_')+1:].replace('_','/'), 
510                                  '/var/lib/apt/lists/' + self.sourcesFile)
511     
512         self.client.load()
513
514     def test_pkg_hash(self):
515         self.client.records.Lookup(self.client.cache['dpkg'].VersionList[0].FileList[0])
516         
517         pkg_hash = os.popen('grep -A 30 -E "^Package: dpkg$" ' + 
518                             '/var/lib/apt/lists/' + self.packagesFile + 
519                             ' | grep -E "^SHA1:" | head -n 1' + 
520                             ' | cut -d\  -f 2').read().rstrip('\n')
521
522         self.failUnless(self.client.records.SHA1Hash == pkg_hash, 
523                         "Hashes don't match: %s != %s" % (self.client.records.SHA1Hash, pkg_hash))
524
525     def test_src_hash(self):
526         self.client.srcrecords.Lookup('dpkg')
527
528         src_hashes = os.popen('grep -A 20 -E "^Package: dpkg$" ' + 
529                             '/var/lib/apt/lists/' + self.sourcesFile + 
530                             ' | grep -A 4 -E "^Files:" | grep -E "^ " ' + 
531                             ' | cut -d\  -f 2').read().split('\n')[:-1]
532
533         for f in self.client.srcrecords.Files:
534             self.failUnless(f[0] in src_hashes, "Couldn't find %s in: %r" % (f[0], src_hashes))
535
536     def test_index_hash(self):
537         indexhash = self.client.indexrecords[self.releaseFile[self.releaseFile.find('_debian_')+1:].replace('_','/')]['main/binary-i386/Packages.bz2']['SHA1'][0]
538
539         idx_hash = os.popen('grep -A 3000 -E "^SHA1:" ' + 
540                             '/var/lib/apt/lists/' + self.releaseFile + 
541                             ' | grep -E " main/binary-i386/Packages.bz2$"'
542                             ' | head -n 1 | cut -d\  -f 2').read().rstrip('\n')
543
544         self.failUnless(indexhash == idx_hash, "Hashes don't match: %s != %s" % (indexhash, idx_hash))
545
546     def tearDown(self):
547         for p in self.pending_calls:
548             if p.active():
549                 p.cancel()
550         self.pending_calls = []
551         self.client.cleanup()
552         self.client = None