utils.py 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342
  1. #!/usr/bin/env python
  2. # vim:set et ts=4 sw=4:
  3. """Utility functions
  4. @contact: Debian FTP Master <ftpmaster@debian.org>
  5. @copyright: 2000, 2001, 2002, 2003, 2004, 2005, 2006 James Troup <james@nocrew.org>
  6. @license: GNU General Public License version 2 or later
  7. """
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation; either version 2 of the License, or
  11. # (at your option) any later version.
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program; if not, write to the Free Software
  18. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  19. import commands
  20. import codecs
  21. import datetime
  22. import email.Header
  23. import os
  24. import pwd
  25. import grp
  26. import select
  27. import socket
  28. import shutil
  29. import sys
  30. import tempfile
  31. import traceback
  32. import stat
  33. import apt_inst
  34. import apt_pkg
  35. import time
  36. import re
  37. import email as modemail
  38. import subprocess
  39. import ldap
  40. import errno
  41. import daklib.config as config
  42. import daklib.daksubprocess
  43. from dbconn import DBConn, get_architecture, get_component, get_suite, \
  44. get_override_type, Keyring, session_wrapper, \
  45. get_active_keyring_paths, \
  46. get_suite_architectures, get_or_set_metadatakey, DBSource, \
  47. Component, Override, OverrideType
  48. from sqlalchemy import desc
  49. from dak_exceptions import *
  50. from gpg import SignedFile
  51. from textutils import fix_maintainer
  52. from regexes import re_html_escaping, html_escaping, re_single_line_field, \
  53. re_multi_line_field, re_srchasver, re_taint_free, \
  54. re_re_mark, re_whitespace_comment, re_issource, \
  55. re_build_dep_arch, re_parse_maintainer
  56. from formats import parse_format, validate_changes_format
  57. from srcformats import get_format_from_string
  58. from collections import defaultdict
  59. ################################################################################
  60. default_config = "/etc/dak/dak.conf" #: default dak config, defines host properties
  61. alias_cache = None #: Cache for email alias checks
  62. key_uid_email_cache = {} #: Cache for email addresses from gpg key uids
  63. # (hashname, function, earliest_changes_version)
  64. known_hashes = [("sha1", apt_pkg.sha1sum, (1, 8)),
  65. ("sha256", apt_pkg.sha256sum, (1, 8))] #: hashes we accept for entries in .changes/.dsc
  66. # Monkeypatch commands.getstatusoutput as it may not return the correct exit
  67. # code in lenny's Python. This also affects commands.getoutput and
  68. # commands.getstatus.
  69. def dak_getstatusoutput(cmd):
  70. pipe = daklib.daksubprocess.Popen(cmd, shell=True, universal_newlines=True,
  71. stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  72. output = pipe.stdout.read()
  73. pipe.wait()
  74. if output[-1:] == '\n':
  75. output = output[:-1]
  76. ret = pipe.wait()
  77. if ret is None:
  78. ret = 0
  79. return ret, output
  80. commands.getstatusoutput = dak_getstatusoutput
  81. ################################################################################
  82. def html_escape(s):
  83. """ Escape html chars """
  84. return re_html_escaping.sub(lambda x: html_escaping.get(x.group(0)), s)
  85. ################################################################################
  86. def open_file(filename, mode='r'):
  87. """
  88. Open C{file}, return fileobject.
  89. @type filename: string
  90. @param filename: path/filename to open
  91. @type mode: string
  92. @param mode: open mode
  93. @rtype: fileobject
  94. @return: open fileobject
  95. @raise CantOpenError: If IOError is raised by open, reraise it as CantOpenError.
  96. """
  97. try:
  98. f = open(filename, mode)
  99. except IOError:
  100. raise CantOpenError(filename)
  101. return f
  102. ################################################################################
  103. def our_raw_input(prompt=""):
  104. if prompt:
  105. while 1:
  106. try:
  107. sys.stdout.write(prompt)
  108. break
  109. except IOError:
  110. pass
  111. sys.stdout.flush()
  112. try:
  113. ret = raw_input()
  114. return ret
  115. except EOFError:
  116. sys.stderr.write("\nUser interrupt (^D).\n")
  117. raise SystemExit
  118. ################################################################################
  119. def extract_component_from_section(section, session=None):
  120. component = ""
  121. if section.find('/') != -1:
  122. component = section.split('/')[0]
  123. # Expand default component
  124. if component == "":
  125. component = "main"
  126. return (section, component)
  127. ################################################################################
  128. def parse_deb822(armored_contents, signing_rules=0, keyrings=None, session=None):
  129. require_signature = True
  130. if keyrings == None:
  131. keyrings = []
  132. require_signature = False
  133. signed_file = SignedFile(armored_contents, keyrings=keyrings, require_signature=require_signature)
  134. contents = signed_file.contents
  135. error = ""
  136. changes = {}
  137. # Split the lines in the input, keeping the linebreaks.
  138. lines = contents.splitlines(True)
  139. if len(lines) == 0:
  140. raise ParseChangesError("[Empty changes file]")
  141. # Reindex by line number so we can easily verify the format of
  142. # .dsc files...
  143. index = 0
  144. indexed_lines = {}
  145. for line in lines:
  146. index += 1
  147. indexed_lines[index] = line[:-1]
  148. num_of_lines = len(indexed_lines.keys())
  149. index = 0
  150. first = -1
  151. while index < num_of_lines:
  152. index += 1
  153. line = indexed_lines[index]
  154. if line == "" and signing_rules == 1:
  155. if index != num_of_lines:
  156. raise InvalidDscError(index)
  157. break
  158. slf = re_single_line_field.match(line)
  159. if slf:
  160. field = slf.groups()[0].lower()
  161. changes[field] = slf.groups()[1]
  162. first = 1
  163. continue
  164. if line == " .":
  165. changes[field] += '\n'
  166. continue
  167. mlf = re_multi_line_field.match(line)
  168. if mlf:
  169. if first == -1:
  170. raise ParseChangesError("'%s'\n [Multi-line field continuing on from nothing?]" % (line))
  171. if first == 1 and changes[field] != "":
  172. changes[field] += '\n'
  173. first = 0
  174. changes[field] += mlf.groups()[0] + '\n'
  175. continue
  176. error += line
  177. changes["filecontents"] = armored_contents
  178. if changes.has_key("source"):
  179. # Strip the source version in brackets from the source field,
  180. # put it in the "source-version" field instead.
  181. srcver = re_srchasver.search(changes["source"])
  182. if srcver:
  183. changes["source"] = srcver.group(1)
  184. changes["source-version"] = srcver.group(2)
  185. if error:
  186. raise ParseChangesError(error)
  187. return changes
  188. ################################################################################
  189. def parse_changes(filename, signing_rules=0, dsc_file=0, keyrings=None):
  190. """
  191. Parses a changes file and returns a dictionary where each field is a
  192. key. The mandatory first argument is the filename of the .changes
  193. file.
  194. signing_rules is an optional argument:
  195. - If signing_rules == -1, no signature is required.
  196. - If signing_rules == 0 (the default), a signature is required.
  197. - If signing_rules == 1, it turns on the same strict format checking
  198. as dpkg-source.
  199. The rules for (signing_rules == 1)-mode are:
  200. - The PGP header consists of "-----BEGIN PGP SIGNED MESSAGE-----"
  201. followed by any PGP header data and must end with a blank line.
  202. - The data section must end with a blank line and must be followed by
  203. "-----BEGIN PGP SIGNATURE-----".
  204. """
  205. with open_file(filename) as changes_in:
  206. content = changes_in.read()
  207. try:
  208. unicode(content, 'utf-8')
  209. except UnicodeError:
  210. raise ChangesUnicodeError("Changes file not proper utf-8")
  211. changes = parse_deb822(content, signing_rules, keyrings=keyrings)
  212. if not dsc_file:
  213. # Finally ensure that everything needed for .changes is there
  214. must_keywords = ('Format', 'Date', 'Source', 'Binary', 'Architecture', 'Version',
  215. 'Distribution', 'Maintainer', 'Description', 'Changes', 'Files')
  216. missingfields=[]
  217. for keyword in must_keywords:
  218. if not changes.has_key(keyword.lower()):
  219. missingfields.append(keyword)
  220. if len(missingfields):
  221. raise ParseChangesError("Missing mandatory field(s) in changes file (policy 5.5): %s" % (missingfields))
  222. return changes
  223. ################################################################################
  224. def hash_key(hashname):
  225. return '%ssum' % hashname
  226. ################################################################################
  227. def check_dsc_files(dsc_filename, dsc, dsc_files):
  228. """
  229. Verify that the files listed in the Files field of the .dsc are
  230. those expected given the announced Format.
  231. @type dsc_filename: string
  232. @param dsc_filename: path of .dsc file
  233. @type dsc: dict
  234. @param dsc: the content of the .dsc parsed by C{parse_changes()}
  235. @type dsc_files: dict
  236. @param dsc_files: the file list returned by C{build_file_list()}
  237. @rtype: list
  238. @return: all errors detected
  239. """
  240. rejmsg = []
  241. # Ensure .dsc lists proper set of source files according to the format
  242. # announced
  243. has = defaultdict(lambda: 0)
  244. ftype_lookup = (
  245. (r'orig.tar.gz', ('orig_tar_gz', 'orig_tar')),
  246. (r'diff.gz', ('debian_diff',)),
  247. (r'tar.gz', ('native_tar_gz', 'native_tar')),
  248. (r'debian\.tar\.(gz|bz2|xz)', ('debian_tar',)),
  249. (r'orig\.tar\.(gz|bz2|xz)', ('orig_tar',)),
  250. (r'tar\.(gz|bz2|xz)', ('native_tar',)),
  251. (r'orig-.+\.tar\.(gz|bz2|xz)', ('more_orig_tar',)),
  252. )
  253. for f in dsc_files:
  254. m = re_issource.match(f)
  255. if not m:
  256. rejmsg.append("%s: %s in Files field not recognised as source."
  257. % (dsc_filename, f))
  258. continue
  259. # Populate 'has' dictionary by resolving keys in lookup table
  260. matched = False
  261. for regex, keys in ftype_lookup:
  262. if re.match(regex, m.group(3)):
  263. matched = True
  264. for key in keys:
  265. has[key] += 1
  266. break
  267. # File does not match anything in lookup table; reject
  268. if not matched:
  269. reject("%s: unexpected source file '%s'" % (dsc_filename, f))
  270. # Check for multiple files
  271. for file_type in ('orig_tar', 'native_tar', 'debian_tar', 'debian_diff'):
  272. if has[file_type] > 1:
  273. rejmsg.append("%s: lists multiple %s" % (dsc_filename, file_type))
  274. # Source format specific tests
  275. try:
  276. format = get_format_from_string(dsc['format'])
  277. rejmsg.extend([
  278. '%s: %s' % (dsc_filename, x) for x in format.reject_msgs(has)
  279. ])
  280. except UnknownFormatError:
  281. # Not an error here for now
  282. pass
  283. return rejmsg
  284. ################################################################################
  285. # Dropped support for 1.4 and ``buggy dchanges 3.4'' (?!) compared to di.pl
  286. def build_file_list(changes, is_a_dsc=0, field="files", hashname="md5sum"):
  287. files = {}
  288. # Make sure we have a Files: field to parse...
  289. if not changes.has_key(field):
  290. raise NoFilesFieldError
  291. # Validate .changes Format: field
  292. if not is_a_dsc:
  293. validate_changes_format(parse_format(changes['format']), field)
  294. includes_section = (not is_a_dsc) and field == "files"
  295. # Parse each entry/line:
  296. for i in changes[field].split('\n'):
  297. if not i:
  298. break
  299. s = i.split()
  300. section = priority = ""
  301. try:
  302. if includes_section:
  303. (md5, size, section, priority, name) = s
  304. else:
  305. (md5, size, name) = s
  306. except ValueError:
  307. raise ParseChangesError(i)
  308. if section == "":
  309. section = "-"
  310. if priority == "":
  311. priority = "-"
  312. (section, component) = extract_component_from_section(section)
  313. files[name] = dict(size=size, section=section,
  314. priority=priority, component=component)
  315. files[name][hashname] = md5
  316. return files
  317. ################################################################################
  318. def send_mail (message, filename="", whitelists=None):
  319. """sendmail wrapper, takes _either_ a message string or a file as arguments
  320. @type whitelists: list of (str or None)
  321. @param whitelists: path to whitelists. C{None} or an empty list whitelists
  322. everything, otherwise an address is whitelisted if it is
  323. included in any of the lists.
  324. In addition a global whitelist can be specified in
  325. Dinstall::MailWhiteList.
  326. """
  327. maildir = Cnf.get('Dir::Mail')
  328. if maildir:
  329. path = os.path.join(maildir, datetime.datetime.now().isoformat())
  330. path = find_next_free(path)
  331. with open(path, 'w') as fh:
  332. print >>fh, message,
  333. # Check whether we're supposed to be sending mail
  334. if Cnf.has_key("Dinstall::Options::No-Mail") and Cnf["Dinstall::Options::No-Mail"]:
  335. return
  336. # If we've been passed a string dump it into a temporary file
  337. if message:
  338. (fd, filename) = tempfile.mkstemp()
  339. os.write (fd, message)
  340. os.close (fd)
  341. if whitelists is None or None in whitelists:
  342. whitelists = []
  343. if Cnf.get('Dinstall::MailWhiteList', ''):
  344. whitelists.append(Cnf['Dinstall::MailWhiteList'])
  345. if len(whitelists) != 0:
  346. with open_file(filename) as message_in:
  347. message_raw = modemail.message_from_file(message_in)
  348. whitelist = [];
  349. for path in whitelists:
  350. with open_file(path, 'r') as whitelist_in:
  351. for line in whitelist_in:
  352. if not re_whitespace_comment.match(line):
  353. if re_re_mark.match(line):
  354. whitelist.append(re.compile(re_re_mark.sub("", line.strip(), 1)))
  355. else:
  356. whitelist.append(re.compile(re.escape(line.strip())))
  357. # Fields to check.
  358. fields = ["To", "Bcc", "Cc"]
  359. for field in fields:
  360. # Check each field
  361. value = message_raw.get(field, None)
  362. if value != None:
  363. match = [];
  364. for item in value.split(","):
  365. (rfc822_maint, rfc2047_maint, name, email) = fix_maintainer(item.strip())
  366. mail_whitelisted = 0
  367. for wr in whitelist:
  368. if wr.match(email):
  369. mail_whitelisted = 1
  370. break
  371. if not mail_whitelisted:
  372. print "Skipping {0} since it's not whitelisted".format(item)
  373. continue
  374. match.append(item)
  375. # Doesn't have any mail in whitelist so remove the header
  376. if len(match) == 0:
  377. del message_raw[field]
  378. else:
  379. message_raw.replace_header(field, ', '.join(match))
  380. # Change message fields in order if we don't have a To header
  381. if not message_raw.has_key("To"):
  382. fields.reverse()
  383. for field in fields:
  384. if message_raw.has_key(field):
  385. message_raw[fields[-1]] = message_raw[field]
  386. del message_raw[field]
  387. break
  388. else:
  389. # Clean up any temporary files
  390. # and return, as we removed all recipients.
  391. if message:
  392. os.unlink (filename);
  393. return;
  394. fd = os.open(filename, os.O_RDWR|os.O_EXCL, 0o700);
  395. os.write (fd, message_raw.as_string(True));
  396. os.close (fd);
  397. # Invoke sendmail
  398. (result, output) = commands.getstatusoutput("%s < %s" % (Cnf["Dinstall::SendmailCommand"], filename))
  399. if (result != 0):
  400. raise SendmailFailedError(output)
  401. # Clean up any temporary files
  402. if message:
  403. os.unlink (filename)
  404. ################################################################################
  405. def poolify (source, component=None):
  406. if source[:3] == "lib":
  407. return source[:4] + '/' + source + '/'
  408. else:
  409. return source[:1] + '/' + source + '/'
  410. ################################################################################
  411. def move (src, dest, overwrite = 0, perms = 0o664):
  412. if os.path.exists(dest) and os.path.isdir(dest):
  413. dest_dir = dest
  414. else:
  415. dest_dir = os.path.dirname(dest)
  416. if not os.path.lexists(dest_dir):
  417. umask = os.umask(00000)
  418. os.makedirs(dest_dir, 0o2775)
  419. os.umask(umask)
  420. #print "Moving %s to %s..." % (src, dest)
  421. if os.path.exists(dest) and os.path.isdir(dest):
  422. dest += '/' + os.path.basename(src)
  423. # Don't overwrite unless forced to
  424. if os.path.lexists(dest):
  425. if not overwrite:
  426. fubar("Can't move %s to %s - file already exists." % (src, dest))
  427. else:
  428. if not os.access(dest, os.W_OK):
  429. fubar("Can't move %s to %s - can't write to existing file." % (src, dest))
  430. shutil.copy2(src, dest)
  431. os.chmod(dest, perms)
  432. os.unlink(src)
  433. def copy (src, dest, overwrite = 0, perms = 0o664):
  434. if os.path.exists(dest) and os.path.isdir(dest):
  435. dest_dir = dest
  436. else:
  437. dest_dir = os.path.dirname(dest)
  438. if not os.path.exists(dest_dir):
  439. umask = os.umask(00000)
  440. os.makedirs(dest_dir, 0o2775)
  441. os.umask(umask)
  442. #print "Copying %s to %s..." % (src, dest)
  443. if os.path.exists(dest) and os.path.isdir(dest):
  444. dest += '/' + os.path.basename(src)
  445. # Don't overwrite unless forced to
  446. if os.path.lexists(dest):
  447. if not overwrite:
  448. raise FileExistsError
  449. else:
  450. if not os.access(dest, os.W_OK):
  451. raise CantOverwriteError
  452. shutil.copy2(src, dest)
  453. os.chmod(dest, perms)
  454. ################################################################################
  455. def which_conf_file ():
  456. if os.getenv('DAK_CONFIG'):
  457. return os.getenv('DAK_CONFIG')
  458. res = socket.getfqdn()
  459. # In case we allow local config files per user, try if one exists
  460. if Cnf.find_b("Config::" + res + "::AllowLocalConfig"):
  461. homedir = os.getenv("HOME")
  462. confpath = os.path.join(homedir, "/etc/dak.conf")
  463. if os.path.exists(confpath):
  464. apt_pkg.read_config_file_isc(Cnf,confpath)
  465. # We are still in here, so there is no local config file or we do
  466. # not allow local files. Do the normal stuff.
  467. if Cnf.get("Config::" + res + "::DakConfig"):
  468. return Cnf["Config::" + res + "::DakConfig"]
  469. return default_config
  470. ################################################################################
  471. def TemplateSubst(subst_map, filename):
  472. """ Perform a substition of template """
  473. with open_file(filename) as templatefile:
  474. template = templatefile.read()
  475. for k, v in subst_map.iteritems():
  476. template = template.replace(k, str(v))
  477. return template
  478. ################################################################################
  479. def fubar(msg, exit_code=1):
  480. sys.stderr.write("E: %s\n" % (msg))
  481. sys.exit(exit_code)
  482. def warn(msg):
  483. sys.stderr.write("W: %s\n" % (msg))
  484. ################################################################################
  485. # Returns the user name with a laughable attempt at rfc822 conformancy
  486. # (read: removing stray periods).
  487. def whoami ():
  488. return pwd.getpwuid(os.getuid())[4].split(',')[0].replace('.', '')
  489. def getusername ():
  490. return pwd.getpwuid(os.getuid())[0]
  491. ################################################################################
  492. def size_type (c):
  493. t = " B"
  494. if c > 10240:
  495. c = c / 1024
  496. t = " KB"
  497. if c > 10240:
  498. c = c / 1024
  499. t = " MB"
  500. return ("%d%s" % (c, t))
  501. ################################################################################
  502. def find_next_free (dest, too_many=100):
  503. extra = 0
  504. orig_dest = dest
  505. while os.path.lexists(dest) and extra < too_many:
  506. dest = orig_dest + '.' + repr(extra)
  507. extra += 1
  508. if extra >= too_many:
  509. raise NoFreeFilenameError
  510. return dest
  511. ################################################################################
  512. def result_join (original, sep = '\t'):
  513. resultlist = []
  514. for i in xrange(len(original)):
  515. if original[i] == None:
  516. resultlist.append("")
  517. else:
  518. resultlist.append(original[i])
  519. return sep.join(resultlist)
  520. ################################################################################
  521. def prefix_multi_line_string(str, prefix, include_blank_lines=0):
  522. out = ""
  523. for line in str.split('\n'):
  524. line = line.strip()
  525. if line or include_blank_lines:
  526. out += "%s%s\n" % (prefix, line)
  527. # Strip trailing new line
  528. if out:
  529. out = out[:-1]
  530. return out
  531. ################################################################################
  532. def join_with_commas_and(list):
  533. if len(list) == 0: return "nothing"
  534. if len(list) == 1: return list[0]
  535. return ", ".join(list[:-1]) + " and " + list[-1]
  536. ################################################################################
  537. def pp_deps (deps):
  538. pp_deps = []
  539. for atom in deps:
  540. (pkg, version, constraint) = atom
  541. if constraint:
  542. pp_dep = "%s (%s %s)" % (pkg, constraint, version)
  543. else:
  544. pp_dep = pkg
  545. pp_deps.append(pp_dep)
  546. return " |".join(pp_deps)
  547. ################################################################################
  548. def get_conf():
  549. return Cnf
  550. ################################################################################
  551. def parse_args(Options):
  552. """ Handle -a, -c and -s arguments; returns them as SQL constraints """
  553. # XXX: This should go away and everything which calls it be converted
  554. # to use SQLA properly. For now, we'll just fix it not to use
  555. # the old Pg interface though
  556. session = DBConn().session()
  557. # Process suite
  558. if Options["Suite"]:
  559. suite_ids_list = []
  560. for suitename in split_args(Options["Suite"]):
  561. suite = get_suite(suitename, session=session)
  562. if not suite or suite.suite_id is None:
  563. warn("suite '%s' not recognised." % (suite and suite.suite_name or suitename))
  564. else:
  565. suite_ids_list.append(suite.suite_id)
  566. if suite_ids_list:
  567. con_suites = "AND su.id IN (%s)" % ", ".join([ str(i) for i in suite_ids_list ])
  568. else:
  569. fubar("No valid suite given.")
  570. else:
  571. con_suites = ""
  572. # Process component
  573. if Options["Component"]:
  574. component_ids_list = []
  575. for componentname in split_args(Options["Component"]):
  576. component = get_component(componentname, session=session)
  577. if component is None:
  578. warn("component '%s' not recognised." % (componentname))
  579. else:
  580. component_ids_list.append(component.component_id)
  581. if component_ids_list:
  582. con_components = "AND c.id IN (%s)" % ", ".join([ str(i) for i in component_ids_list ])
  583. else:
  584. fubar("No valid component given.")
  585. else:
  586. con_components = ""
  587. # Process architecture
  588. con_architectures = ""
  589. check_source = 0
  590. if Options["Architecture"]:
  591. arch_ids_list = []
  592. for archname in split_args(Options["Architecture"]):
  593. if archname == "source":
  594. check_source = 1
  595. else:
  596. arch = get_architecture(archname, session=session)
  597. if arch is None:
  598. warn("architecture '%s' not recognised." % (archname))
  599. else:
  600. arch_ids_list.append(arch.arch_id)
  601. if arch_ids_list:
  602. con_architectures = "AND a.id IN (%s)" % ", ".join([ str(i) for i in arch_ids_list ])
  603. else:
  604. if not check_source:
  605. fubar("No valid architecture given.")
  606. else:
  607. check_source = 1
  608. return (con_suites, con_architectures, con_components, check_source)
  609. ################################################################################
  610. def arch_compare_sw (a, b):
  611. """
  612. Function for use in sorting lists of architectures.
  613. Sorts normally except that 'source' dominates all others.
  614. """
  615. if a == "source" and b == "source":
  616. return 0
  617. elif a == "source":
  618. return -1
  619. elif b == "source":
  620. return 1
  621. return cmp (a, b)
  622. ################################################################################
  623. def split_args (s, dwim=True):
  624. """
  625. Split command line arguments which can be separated by either commas
  626. or whitespace. If dwim is set, it will complain about string ending
  627. in comma since this usually means someone did 'dak ls -a i386, m68k
  628. foo' or something and the inevitable confusion resulting from 'm68k'
  629. being treated as an argument is undesirable.
  630. """
  631. if s.find(",") == -1:
  632. return s.split()
  633. else:
  634. if s[-1:] == "," and dwim:
  635. fubar("split_args: found trailing comma, spurious space maybe?")
  636. return s.split(",")
  637. ################################################################################
  638. def gpg_keyring_args(keyrings=None):
  639. if not keyrings:
  640. keyrings = get_active_keyring_paths()
  641. return " ".join(["--keyring %s" % x for x in keyrings])
  642. ################################################################################
  643. def gpg_get_key_addresses(fingerprint):
  644. """retreive email addresses from gpg key uids for a given fingerprint"""
  645. addresses = key_uid_email_cache.get(fingerprint)
  646. if addresses != None:
  647. return addresses
  648. addresses = list()
  649. try:
  650. with open(os.devnull, "wb") as devnull:
  651. output = daklib.daksubprocess.check_output(
  652. ["gpg", "--no-default-keyring"] + gpg_keyring_args().split() +
  653. ["--with-colons", "--list-keys", fingerprint], stderr=devnull)
  654. except subprocess.CalledProcessError:
  655. pass
  656. else:
  657. for l in output.split('\n'):
  658. parts = l.split(':')
  659. if parts[0] not in ("uid", "pub"):
  660. continue
  661. try:
  662. uid = parts[9]
  663. except IndexError:
  664. continue
  665. try:
  666. # Do not use unicode_escape, because it is locale-specific
  667. uid = codecs.decode(uid, "string_escape").decode("utf-8")
  668. except UnicodeDecodeError:
  669. uid = uid.decode("latin1") # does not fail
  670. m = re_parse_maintainer.match(uid)
  671. if not m:
  672. continue
  673. address = m.group(2)
  674. address = address.encode("utf8") # dak still uses bytes
  675. if address.endswith('@debian.org'):
  676. # prefer @debian.org addresses
  677. # TODO: maybe not hardcode the domain
  678. addresses.insert(0, address)
  679. else:
  680. addresses.append(address)
  681. key_uid_email_cache[fingerprint] = addresses
  682. return addresses
  683. ################################################################################
  684. def get_logins_from_ldap(fingerprint='*'):
  685. """retrieve login from LDAP linked to a given fingerprint"""
  686. LDAPDn = Cnf['Import-LDAP-Fingerprints::LDAPDn']
  687. LDAPServer = Cnf['Import-LDAP-Fingerprints::LDAPServer']
  688. l = ldap.open(LDAPServer)
  689. l.simple_bind_s('','')
  690. Attrs = l.search_s(LDAPDn, ldap.SCOPE_ONELEVEL,
  691. '(keyfingerprint=%s)' % fingerprint,
  692. ['uid', 'keyfingerprint'])
  693. login = {}
  694. for elem in Attrs:
  695. login[elem[1]['keyFingerPrint'][0]] = elem[1]['uid'][0]
  696. return login
  697. ################################################################################
  698. def get_users_from_ldap():
  699. """retrieve login and user names from LDAP"""
  700. LDAPDn = Cnf['Import-LDAP-Fingerprints::LDAPDn']
  701. LDAPServer = Cnf['Import-LDAP-Fingerprints::LDAPServer']
  702. l = ldap.open(LDAPServer)
  703. l.simple_bind_s('','')
  704. Attrs = l.search_s(LDAPDn, ldap.SCOPE_ONELEVEL,
  705. '(uid=*)', ['uid', 'cn', 'mn', 'sn'])
  706. users = {}
  707. for elem in Attrs:
  708. elem = elem[1]
  709. name = []
  710. for k in ('cn', 'mn', 'sn'):
  711. try:
  712. if elem[k][0] != '-':
  713. name.append(elem[k][0])
  714. except KeyError:
  715. pass
  716. users[' '.join(name)] = elem['uid'][0]
  717. return users
  718. ################################################################################
  719. def clean_symlink (src, dest, root):
  720. """
  721. Relativize an absolute symlink from 'src' -> 'dest' relative to 'root'.
  722. Returns fixed 'src'
  723. """
  724. src = src.replace(root, '', 1)
  725. dest = dest.replace(root, '', 1)
  726. dest = os.path.dirname(dest)
  727. new_src = '../' * len(dest.split('/'))
  728. return new_src + src
  729. ################################################################################
  730. def temp_filename(directory=None, prefix="dak", suffix="", mode=None, group=None):
  731. """
  732. Return a secure and unique filename by pre-creating it.
  733. @type directory: str
  734. @param directory: If non-null it will be the directory the file is pre-created in.
  735. @type prefix: str
  736. @param prefix: The filename will be prefixed with this string
  737. @type suffix: str
  738. @param suffix: The filename will end with this string
  739. @type mode: str
  740. @param mode: If set the file will get chmodded to those permissions
  741. @type group: str
  742. @param group: If set the file will get chgrped to the specified group.
  743. @rtype: list
  744. @return: Returns a pair (fd, name)
  745. """
  746. (tfd, tfname) = tempfile.mkstemp(suffix, prefix, directory)
  747. if mode:
  748. os.chmod(tfname, mode)
  749. if group:
  750. gid = grp.getgrnam(group).gr_gid
  751. os.chown(tfname, -1, gid)
  752. return (tfd, tfname)
  753. ################################################################################
  754. def temp_dirname(parent=None, prefix="dak", suffix="", mode=None, group=None):
  755. """
  756. Return a secure and unique directory by pre-creating it.
  757. @type parent: str
  758. @param parent: If non-null it will be the directory the directory is pre-created in.
  759. @type prefix: str
  760. @param prefix: The filename will be prefixed with this string
  761. @type suffix: str
  762. @param suffix: The filename will end with this string
  763. @type mode: str
  764. @param mode: If set the file will get chmodded to those permissions
  765. @type group: str
  766. @param group: If set the file will get chgrped to the specified group.
  767. @rtype: list
  768. @return: Returns a pair (fd, name)
  769. """
  770. tfname = tempfile.mkdtemp(suffix, prefix, parent)
  771. if mode:
  772. os.chmod(tfname, mode)
  773. if group:
  774. gid = grp.getgrnam(group).gr_gid
  775. os.chown(tfname, -1, gid)
  776. return tfname
  777. ################################################################################
  778. def is_email_alias(email):
  779. """ checks if the user part of the email is listed in the alias file """
  780. global alias_cache
  781. if alias_cache == None:
  782. aliasfn = which_alias_file()
  783. alias_cache = set()
  784. if aliasfn:
  785. for l in open(aliasfn):
  786. alias_cache.add(l.split(':')[0])
  787. uid = email.split('@')[0]
  788. return uid in alias_cache
  789. ################################################################################
  790. def get_changes_files(from_dir):
  791. """
  792. Takes a directory and lists all .changes files in it (as well as chdir'ing
  793. to the directory; this is due to broken behaviour on the part of p-u/p-a
  794. when you're not in the right place)
  795. Returns a list of filenames
  796. """
  797. try:
  798. # Much of the rest of p-u/p-a depends on being in the right place
  799. os.chdir(from_dir)
  800. changes_files = [x for x in os.listdir(from_dir) if x.endswith('.changes')]
  801. except OSError as e:
  802. fubar("Failed to read list from directory %s (%s)" % (from_dir, e))
  803. return changes_files
  804. ################################################################################
  805. Cnf = config.Config().Cnf
  806. ################################################################################
  807. def parse_wnpp_bug_file(file = "/srv/ftp-master.debian.org/scripts/masterfiles/wnpp_rm"):
  808. """
  809. Parses the wnpp bug list available at https://qa.debian.org/data/bts/wnpp_rm
  810. Well, actually it parsed a local copy, but let's document the source
  811. somewhere ;)
  812. returns a dict associating source package name with a list of open wnpp
  813. bugs (Yes, there might be more than one)
  814. """
  815. line = []
  816. try:
  817. f = open(file)
  818. lines = f.readlines()
  819. except IOError as e:
  820. print "Warning: Couldn't open %s; don't know about WNPP bugs, so won't close any." % file
  821. lines = []
  822. wnpp = {}
  823. for line in lines:
  824. splited_line = line.split(": ", 1)
  825. if len(splited_line) > 1:
  826. wnpp[splited_line[0]] = splited_line[1].split("|")
  827. for source in wnpp.keys():
  828. bugs = []
  829. for wnpp_bug in wnpp[source]:
  830. bug_no = re.search("(\d)+", wnpp_bug).group()
  831. if bug_no:
  832. bugs.append(bug_no)
  833. wnpp[source] = bugs
  834. return wnpp
  835. ################################################################################
  836. def get_packages_from_ftp(root, suite, component, architecture):
  837. """
  838. Returns an object containing apt_pkg-parseable data collected by
  839. aggregating Packages.gz files gathered for each architecture.
  840. @type root: string
  841. @param root: path to ftp archive root directory
  842. @type suite: string
  843. @param suite: suite to extract files from
  844. @type component: string
  845. @param component: component to extract files from
  846. @type architecture: string
  847. @param architecture: architecture to extract files from
  848. @rtype: TagFile
  849. @return: apt_pkg class containing package data
  850. """
  851. filename = "%s/dists/%s/%s/binary-%s/Packages.gz" % (root, suite, component, architecture)
  852. (fd, temp_file) = temp_filename()
  853. (result, output) = commands.getstatusoutput("gunzip -c %s > %s" % (filename, temp_file))
  854. if (result != 0):
  855. fubar("Gunzip invocation failed!\n%s\n" % (output), result)
  856. filename = "%s/dists/%s/%s/debian-installer/binary-%s/Packages.gz" % (root, suite, component, architecture)
  857. if os.path.exists(filename):
  858. (result, output) = commands.getstatusoutput("gunzip -c %s >> %s" % (filename, temp_file))
  859. if (result != 0):
  860. fubar("Gunzip invocation failed!\n%s\n" % (output), result)
  861. packages = open_file(temp_file)
  862. Packages = apt_pkg.TagFile(packages)
  863. os.unlink(temp_file)
  864. return Packages
  865. ################################################################################
  866. def deb_extract_control(fh):
  867. """extract DEBIAN/control from a binary package"""
  868. return apt_inst.DebFile(fh).control.extractdata("control")
  869. ################################################################################
  870. def mail_addresses_for_upload(maintainer, changed_by, fingerprint):
  871. """mail addresses to contact for an upload
  872. @type maintainer: str
  873. @param maintainer: Maintainer field of the .changes file
  874. @type changed_by: str
  875. @param changed_by: Changed-By field of the .changes file
  876. @type fingerprint: str
  877. @param fingerprint: fingerprint of the key used to sign the upload
  878. @rtype: list of str
  879. @return: list of RFC 2047-encoded mail addresses to contact regarding
  880. this upload
  881. """
  882. addresses = [maintainer]
  883. if changed_by != maintainer:
  884. addresses.append(changed_by)
  885. fpr_addresses = gpg_get_key_addresses(fingerprint)
  886. if len(fpr_addresses) > 0 and fix_maintainer(changed_by)[3] not in fpr_addresses and fix_maintainer(maintainer)[3] not in fpr_addresses:
  887. addresses.append(fpr_addresses[0])
  888. encoded_addresses = [ fix_maintainer(e)[1] for e in addresses ]
  889. return encoded_addresses
  890. ################################################################################
  891. def call_editor(text="", suffix=".txt"):
  892. """run editor and return the result as a string
  893. @type text: str
  894. @param text: initial text
  895. @type suffix: str
  896. @param suffix: extension for temporary file
  897. @rtype: str
  898. @return: string with the edited text
  899. """
  900. editor = os.environ.get('VISUAL', os.environ.get('EDITOR', 'vi'))
  901. tmp = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
  902. try:
  903. print >>tmp, text,
  904. tmp.close()
  905. daklib.daksubprocess.check_call([editor, tmp.name])
  906. return open(tmp.name, 'r').read()
  907. finally:
  908. os.unlink(tmp.name)
  909. ################################################################################
  910. def check_reverse_depends(removals, suite, arches=None, session=None, cruft=False, quiet=False):
  911. dbsuite = get_suite(suite, session)
  912. overridesuite = dbsuite
  913. if dbsuite.overridesuite is not None:
  914. overridesuite = get_suite(dbsuite.overridesuite, session)
  915. dep_problem = 0
  916. p2c = {}
  917. all_broken = defaultdict(lambda: defaultdict(set))
  918. if arches:
  919. all_arches = set(arches)
  920. else:
  921. all_arches = set(x.arch_string for x in get_suite_architectures(suite))
  922. all_arches -= set(["source", "all"])
  923. removal_set = set(removals)
  924. metakey_d = get_or_set_metadatakey("Depends", session)
  925. metakey_p = get_or_set_metadatakey("Provides", session)
  926. params = {
  927. 'suite_id': dbsuite.suite_id,
  928. 'metakey_d_id': metakey_d.key_id,
  929. 'metakey_p_id': metakey_p.key_id,
  930. }
  931. for architecture in all_arches | set(['all']):
  932. deps = {}
  933. sources = {}
  934. virtual_packages = {}
  935. params['arch_id'] = get_architecture(architecture, session).arch_id
  936. statement = '''
  937. SELECT b.package, s.source, c.name as component,
  938. (SELECT bmd.value FROM binaries_metadata bmd WHERE bmd.bin_id = b.id AND bmd.key_id = :metakey_d_id) AS depends,
  939. (SELECT bmp.value FROM binaries_metadata bmp WHERE bmp.bin_id = b.id AND bmp.key_id = :metakey_p_id) AS provides
  940. FROM binaries b
  941. JOIN bin_associations ba ON b.id = ba.bin AND ba.suite = :suite_id
  942. JOIN source s ON b.source = s.id
  943. JOIN files_archive_map af ON b.file = af.file_id
  944. JOIN component c ON af.component_id = c.id
  945. WHERE b.architecture = :arch_id'''
  946. query = session.query('package', 'source', 'component', 'depends', 'provides'). \
  947. from_statement(statement).params(params)
  948. for package, source, component, depends, provides in query:
  949. sources[package] = source
  950. p2c[package] = component
  951. if depends is not None:
  952. deps[package] = depends
  953. # Maintain a counter for each virtual package. If a
  954. # Provides: exists, set the counter to 0 and count all
  955. # provides by a package not in the list for removal.
  956. # If the counter stays 0 at the end, we know that only
  957. # the to-be-removed packages provided this virtual
  958. # package.
  959. if provides is not None:
  960. for virtual_pkg in provides.split(","):
  961. virtual_pkg = virtual_pkg.strip()
  962. if virtual_pkg == package: continue
  963. if not virtual_packages.has_key(virtual_pkg):
  964. virtual_packages[virtual_pkg] = 0
  965. if package not in removals:
  966. virtual_packages[virtual_pkg] += 1
  967. # If a virtual package is only provided by the to-be-removed
  968. # packages, treat the virtual package as to-be-removed too.
  969. removal_set.update(virtual_pkg for virtual_pkg in virtual_packages if not virtual_packages[virtual_pkg])
  970. # Check binary dependencies (Depends)
  971. for package in deps:
  972. if package in removals: continue
  973. try:
  974. parsed_dep = apt_pkg.parse_depends(deps[package])
  975. except ValueError as e:
  976. print "Error for package %s: %s" % (package, e)
  977. parsed_dep = []
  978. for dep in parsed_dep:
  979. # Check for partial breakage. If a package has a ORed
  980. # dependency, there is only a dependency problem if all
  981. # packages in the ORed depends will be removed.
  982. unsat = 0
  983. for dep_package, _, _ in dep:
  984. if dep_package in removals:
  985. unsat += 1
  986. if unsat == len(dep):
  987. component = p2c[package]
  988. source = sources[package]
  989. if component != "main":
  990. source = "%s/%s" % (source, component)
  991. all_broken[source][package].add(architecture)
  992. dep_problem = 1
  993. if all_broken and not quiet:
  994. if cruft:
  995. print " - broken Depends:"
  996. else:
  997. print "# Broken Depends:"
  998. for source, bindict in sorted(all_broken.items()):
  999. lines = []
  1000. for binary, arches in sorted(bindict.items()):
  1001. if arches == all_arches or 'all' in arches:
  1002. lines.append(binary)
  1003. else:
  1004. lines.append('%s [%s]' % (binary, ' '.join(sorted(arches))))
  1005. if cruft:
  1006. print ' %s: %s' % (source, lines[0])
  1007. else:
  1008. print '%s: %s' % (source, lines[0])
  1009. for line in lines[1:]:
  1010. if cruft:
  1011. print ' ' + ' ' * (len(source) + 2) + line
  1012. else:
  1013. print ' ' * (len(source) + 2) + line
  1014. if not cruft:
  1015. print
  1016. # Check source dependencies (Build-Depends and Build-Depends-Indep)
  1017. all_broken = defaultdict(set)
  1018. metakey_bd = get_or_set_metadatakey("Build-Depends", session)
  1019. metakey_bdi = get_or_set_metadatakey("Build-Depends-Indep", session)
  1020. params = {
  1021. 'suite_id': dbsuite.suite_id,
  1022. 'metakey_ids': (metakey_bd.key_id, metakey_bdi.key_id),
  1023. }
  1024. statement = '''
  1025. SELECT s.source, string_agg(sm.value, ', ') as build_dep
  1026. FROM source s
  1027. JOIN source_metadata sm ON s.id = sm.src_id
  1028. WHERE s.id in
  1029. (SELECT source FROM src_associations
  1030. WHERE suite = :suite_id)
  1031. AND sm.key_id in :metakey_ids
  1032. GROUP BY s.id, s.source'''
  1033. query = session.query('source', 'build_dep').from_statement(statement). \
  1034. params(params)
  1035. for source, build_dep in query:
  1036. if source in removals: continue
  1037. parsed_dep = []
  1038. if build_dep is not None:
  1039. # Remove [arch] information since we want to see breakage on all arches
  1040. build_dep = re_build_dep_arch.sub("", build_dep)
  1041. try:
  1042. parsed_dep = apt_pkg.parse_src_depends(build_dep)
  1043. except ValueError as e:
  1044. print "Error for source %s: %s" % (source, e)
  1045. for dep in parsed_dep:
  1046. unsat = 0
  1047. for dep_package, _, _ in dep:
  1048. if dep_package in removals:
  1049. unsat += 1
  1050. if unsat == len(dep):
  1051. component, = session.query(Component.component_name) \
  1052. .join(Component.overrides) \
  1053. .filter(Override.suite == overridesuite) \
  1054. .filter(Override.package == re.sub('/(contrib|non-free)$', '', source)) \
  1055. .join(Override.overridetype).filter(OverrideType.overridetype == 'dsc') \
  1056. .first()
  1057. key = source
  1058. if component != "main":
  1059. key = "%s/%s" % (source, component)
  1060. all_broken[key].add(pp_deps(dep))
  1061. dep_problem = 1
  1062. if all_broken and not quiet:
  1063. if cruft:
  1064. print " - broken Build-Depends:"
  1065. else:
  1066. print "# Broken Build-Depends:"
  1067. for source, bdeps in sorted(all_broken.items()):
  1068. bdeps = sorted(bdeps)
  1069. if cruft:
  1070. print ' %s: %s' % (source, bdeps[0])
  1071. else:
  1072. print '%s: %s' % (source, bdeps[0])
  1073. for bdep in bdeps[1:]:
  1074. if cruft:
  1075. print ' ' + ' ' * (len(source) + 2) + bdep
  1076. else:
  1077. print ' ' * (len(source) + 2) + bdep
  1078. if not cruft:
  1079. print
  1080. return dep_problem
  1081. ################################################################################
  1082. def parse_built_using(control):
  1083. """source packages referenced via Built-Using
  1084. @type control: dict-like
  1085. @param control: control file to take Built-Using field from
  1086. @rtype: list of (str, str)
  1087. @return: list of (source_name, source_version) pairs
  1088. """
  1089. built_using = control.get('Built-Using', None)
  1090. if built_using is None:
  1091. return []
  1092. bu = []
  1093. for dep in apt_pkg.parse_depends(built_using):
  1094. assert len(dep) == 1, 'Alternatives are not allowed in Built-Using field'
  1095. source_name, source_version, comp = dep[0]
  1096. assert comp == '=', 'Built-Using must contain strict dependencies'
  1097. bu.append((source_name, source_version))
  1098. return bu
  1099. ################################################################################
  1100. def is_in_debug_section(control):
  1101. """binary package is a debug package
  1102. @type control: dict-like
  1103. @param control: control file of binary package
  1104. @rtype Boolean
  1105. @return: True if the binary package is a debug package
  1106. """
  1107. section = control['Section'].split('/', 1)[-1]
  1108. return section == "debug"