utils.py 46 KB

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