3 # Installs Debian packaes
4 # Copyright (C) 2000 James Troup <james@nocrew.org>
5 # $Id: katie,v 1.17 2001-01-10 06:08:03 troup Exp $
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.
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.
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21 # Based (almost entirely) on dinstall by Guy Maor <maor@debian.org>
23 #########################################################################################
25 # Cartman: "I'm trying to make the best of a bad situation, I don't
26 # need to hear crap from a bunch of hippy freaks living in
27 # denial. Screw you guys, I'm going home."
29 # Kyle: "But Cartman, we're trying to..."
31 # Cartman: "uhh.. screw you guys... home."
33 #########################################################################################
35 import FCNTL, commands, fcntl, getopt, gzip, os, pg, pwd, re, shutil, stat, string, sys, tempfile, time
36 import apt_inst, apt_pkg
37 import utils, db_access
39 ###############################################################################
41 re_isanum = re.compile (r'^\d+$');
42 re_isadeb = re.compile (r'.*\.u?deb$');
43 re_issource = re.compile (r'(.+)_(.+?)\.(orig\.tar\.gz|diff\.gz|tar\.gz|dsc)');
44 re_changes = re.compile (r'changes$');
45 re_default_answer = re.compile(r"\[(.*)\]");
46 re_fdnic = re.compile("\n\n");
47 re_bad_diff = re.compile("^[\-\+][\-\+][\-\+] /dev/null");
49 ###############################################################################
52 reject_footer = """If you don't understand why your files were rejected, or if the
53 override file requires editing, reply to this email.
55 Your rejected files are in incoming/REJECT/. (Some may also be in
56 incoming/ if your .changes file was unparsable.) If only some of the
57 files need to repaired, you may move any good files back to incoming/.
58 Please remove any bad files from incoming/REJECT/."""
60 new_ack_footer = """Your package contains new components which requires manual editing of
61 the override file. It is ok otherwise, so please be patient. New
62 packages are usually added to the override file about once a week.
64 You may have gotten the distribution wrong. You'll get warnings above
65 if files already exist in other distributions."""
67 installed_footer = """If the override file requires editing, file a bug on ftp.debian.org.
69 Thank you for your contribution to Debian GNU."""
71 #########################################################################################
87 legacy_source_untouchable = {};
89 #########################################################################################
91 def usage (exit_code):
92 print """Usage: dinstall [OPTION]... [CHANGES]...
93 -a, --automatic automatic run
94 -d, --debug=VALUE debug
95 -k, --ack-new acknowledge new packages
96 -m, --manual-reject=MSG manual reject with `msg'
97 -n, --dry-run don't do anything
98 -p, --no-lock don't check lockfile !! for cron.daily only !!
99 -r, --no-version-check override version check
100 -u, --distribution=DIST override distribution to `dist'"""
103 def check_signature (filename):
104 global reject_message
106 (result, output) = commands.getstatusoutput("gpg --emulate-md-encode-bug --batch --no-options --no-default-keyring --always-trust --keyring=%s --keyring=%s < %s >/dev/null" % (Cnf["Dinstall::PGPKeyring"], Cnf["Dinstall::GPGKeyring"], filename))
108 reject_message = "Rejected: GPG signature check failed on `%s'.\n%s\n" % (filename, output)
112 #####################################################################################################################
114 # See if a given package is in the override table
116 def in_override_p (package, component, suite, binary_type):
119 if binary_type == "": # must be source
124 # Avoid <undef> on unknown distributions
125 suite_id = db_access.get_suite_id(suite);
128 component_id = db_access.get_component_id(component);
129 type_id = db_access.get_override_type_id(type);
131 # FIXME: nasty non-US speficic hack
132 if string.lower(component[:7]) == "non-us/":
133 component = component[7:];
135 q = projectB.query("SELECT package FROM override WHERE package = '%s' AND suite = %s AND component = %s AND type = %s"
136 % (package, suite_id, component_id, type_id));
137 result = q.getresult();
138 # If checking for a source package fall back on the binary override type
139 if type == "dsc" and not result:
140 type_id = db_access.get_override_type_id("deb");
141 q = projectB.query("SELECT package FROM override WHERE package = '%s' AND suite = %s AND component = %s AND type = %s"
142 % (package, suite_id, component_id, type_id));
143 result = q.getresult();
147 #####################################################################################################################
149 def check_changes(filename):
150 global reject_message, changes, files
152 # Parse the .changes field into a dictionary [FIXME - need to trap errors, pass on to reject_message etc.]
154 changes = utils.parse_changes(filename)
155 except utils.cant_open_exc:
156 reject_message = "Rejected: can't read changes file '%s'.\n" % (filename)
158 except utils.changes_parse_error_exc, line:
159 reject_message = "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
160 changes["maintainer822"] = Cnf["Dinstall::MyEmailAddress"];
163 # Parse the Files field from the .changes into another dictionary [FIXME need to trap errors as above]
164 files = utils.build_file_list(changes, "")
166 # Check for mandatory fields
167 for i in ("source", "binary", "architecture", "version", "distribution","maintainer", "files"):
168 if not changes.has_key(i):
169 reject_message = "Rejected: Missing field `%s' in changes file." % (i)
170 return 0 # Avoid <undef> errors during later tests
172 # Fix the Maintainer: field to be RFC822 compatible
173 (changes["maintainer822"], changes["maintainername"], changes["maintaineremail"]) = utils.fix_maintainer (changes["maintainer"])
175 # Override the Distribution: field if appropriate
176 if Cnf["Dinstall::Options::Override-Distribution"] != "":
177 reject_message = reject_message + "Warning: Distribution was overriden from %s to %s.\n" % (changes["distribution"], Cnf["Dinstall::Options::Override-Distribution"])
178 changes["distribution"] = Cnf["Dinstall::Options::Override-Distribution"]
180 # Split multi-value fields into a lower-level dictionary
181 for i in ("architecture", "distribution", "binary", "closes"):
182 o = changes.get(i, "")
186 for j in string.split(o):
189 # Ensure all the values in Closes: are numbers
190 if changes.has_key("closes"):
191 for i in changes["closes"].keys():
192 if re_isanum.match (i) == None:
193 reject_message = reject_message + "Rejected: `%s' from Closes field isn't a number.\n" % (i)
195 # Map frozen to unstable if frozen doesn't exist
196 if changes["distribution"].has_key("frozen") and not Cnf.has_key("Suite::Frozen"):
197 del changes["distribution"]["frozen"]
198 reject_message = reject_message + "Mapping frozen to unstable.\n"
200 # Map testing to unstable
201 if changes["distribution"].has_key("testing"):
202 del changes["distribution"]["testing"]
203 reject_message = reject_message + "Mapping testing to unstable.\n"
205 # Ensure target distributions exist
206 for i in changes["distribution"].keys():
207 if not Cnf.has_key("Suite::%s" % (i)):
208 reject_message = reject_message + "Rejected: Unknown distribution `%s'.\n" % (i)
210 # Map unreleased arches from stable to unstable
211 if changes["distribution"].has_key("stable"):
212 for i in changes["architecture"].keys():
213 if not Cnf.has_key("Suite::Stable::Architectures::%s" % (i)):
214 reject_message = reject_message + "Mapping stable to unstable for unreleased arch `%s'.\n" % (i)
215 del changes["distribution"]["stable"]
217 # Map arches not being released from frozen to unstable
218 if changes["distribution"].has_key("frozen"):
219 for i in changes["architecture"].keys():
220 if not Cnf.has_key("Suite::Frozen::Architectures::%s" % (i)):
221 reject_message = reject_message + "Mapping frozen to unstable for non-releasing arch `%s'.\n" % (i)
222 del changes["distribution"]["frozen"]
224 # Handle uploads to stable
225 if changes["distribution"].has_key("stable"):
226 # If running from within proposed-updates; assume an install to stable
227 if string.find(os.getcwd(), 'proposed-updates') != -1:
228 # FIXME: should probably remove anything that != stable
229 for i in ("frozen", "unstable"):
230 if changes["distribution"].has_key(i):
231 reject_message = reject_message + "Removing %s from distribution list.\n"
232 del changes["distribution"][i]
233 changes["stable upload"] = 1;
234 # If we can't find a file from the .changes; assume it's a package already in the pool and move into the pool
235 file = files.keys()[0];
236 if os.access(file, os.R_OK) == 0:
237 pool_dir = Cnf["Dir::PoolDir"] + '/' + utils.poolify(changes["source"], files[file]["component"]);
239 # Otherwise (normal case) map stable to updates
241 reject_message = reject_message + "Mapping stable to updates.\n";
242 del changes["distribution"]["stable"];
243 changes["distribution"]["proposed-updates"] = 1;
245 # chopversion = no epoch; chopversion2 = no epoch and no revision (e.g. for .orig.tar.gz comparison)
246 changes["chopversion"] = utils.re_no_epoch.sub('', changes["version"])
247 changes["chopversion2"] = utils.re_no_revision.sub('', changes["chopversion"])
249 if string.find(reject_message, "Rejected:") != -1:
255 global reject_message
257 archive = utils.where_am_i();
259 for file in files.keys():
260 # Check the file is readable
261 if os.access(file,os.R_OK) == 0:
262 reject_message = reject_message + "Rejected: Can't read `%s'.\n" % (file)
263 files[file]["type"] = "unreadable";
265 # If it's byhand skip remaining checks
266 if files[file]["section"] == "byhand":
267 files[file]["byhand"] = 1;
268 files[file]["type"] = "byhand";
269 # Checks for a binary package...
270 elif re_isadeb.match(file) != None:
271 # Extract package information using dpkg-deb
272 control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))
274 # Check for mandatory fields
275 if control.Find("Package") == None:
276 reject_message = reject_message + "Rejected: %s: No package field in control.\n" % (file)
277 if control.Find("Architecture") == None:
278 reject_message = reject_message + "Rejected: %s: No architecture field in control.\n" % (file)
279 if control.Find("Version") == None:
280 reject_message = reject_message + "Rejected: %s: No version field in control.\n" % (file)
282 # Ensure the package name matches the one give in the .changes
283 if not changes["binary"].has_key(control.Find("Package", "")):
284 reject_message = reject_message + "Rejected: %s: control file lists name as `%s', which isn't in changes file.\n" % (file, control.Find("Package", ""))
286 # Validate the architecture
287 if not Cnf.has_key("Suite::Unstable::Architectures::%s" % (control.Find("Architecture", ""))):
288 reject_message = reject_message + "Rejected: Unknown architecture '%s'.\n" % (control.Find("Architecture", ""))
290 # Check the architecture matches the one given in the .changes
291 if not changes["architecture"].has_key(control.Find("Architecture", "")):
292 reject_message = reject_message + "Rejected: %s: control file lists arch as `%s', which isn't in changes file.\n" % (file, control.Find("Architecture", ""))
293 # Check the section & priority match those given in the .changes (non-fatal)
294 if control.Find("Section") != None and files[file]["section"] != "" and files[file]["section"] != control.Find("Section"):
295 reject_message = reject_message + "Warning: %s control file lists section as `%s', but changes file has `%s'.\n" % (file, control.Find("Section", ""), files[file]["section"])
296 if control.Find("Priority") != None and files[file]["priority"] != "" and files[file]["priority"] != control.Find("Priority"):
297 reject_message = reject_message + "Warning: %s control file lists priority as `%s', but changes file has `%s'.\n" % (file, control.Find("Priority", ""), files[file]["priority"])
299 epochless_version = utils.re_no_epoch.sub('', control.Find("Version", ""))
301 files[file]["package"] = control.Find("Package");
302 files[file]["architecture"] = control.Find("Architecture");
303 files[file]["version"] = control.Find("Version");
304 files[file]["maintainer"] = control.Find("Maintainer", "");
305 if file[-5:] == ".udeb":
306 files[file]["dbtype"] = "udeb";
307 elif file[-4:] == ".deb":
308 files[file]["dbtype"] = "deb";
310 reject_message = reject_message + "Rejected: %s is neither a .deb or a .udeb.\n " % (file);
311 files[file]["type"] = "deb";
312 files[file]["fullname"] = "%s_%s_%s.deb" % (control.Find("Package", ""), epochless_version, control.Find("Architecture", ""))
313 files[file]["source"] = control.Find("Source", "");
314 if files[file]["source"] == "":
315 files[file]["source"] = files[file]["package"];
316 # Checks for a source package...
318 m = re_issource.match(file)
320 files[file]["package"] = m.group(1)
321 files[file]["version"] = m.group(2)
322 files[file]["type"] = m.group(3)
324 # Ensure the source package name matches the Source filed in the .changes
325 if changes["source"] != files[file]["package"]:
326 reject_message = reject_message + "Rejected: %s: changes file doesn't say %s for Source\n" % (file, files[file]["package"])
328 # Ensure the source version matches the version in the .changes file
329 if files[file]["type"] == "orig.tar.gz":
330 changes_version = changes["chopversion2"]
332 changes_version = changes["chopversion"]
333 if changes_version != files[file]["version"]:
334 reject_message = reject_message + "Rejected: %s: should be %s according to changes file.\n" % (file, changes_version)
336 # Ensure the .changes lists source in the Architecture field
337 if not changes["architecture"].has_key("source"):
338 reject_message = reject_message + "Rejected: %s: changes file doesn't list `source' in Architecture field.\n" % (file)
340 # Check the signature of a .dsc file
341 if files[file]["type"] == "dsc":
342 check_signature(file)
344 files[file]["fullname"] = file
346 # Not a binary or source package? Assume byhand...
348 files[file]["byhand"] = 1;
349 files[file]["type"] = "byhand";
351 files[file]["oldfiles"] = {}
352 for suite in changes["distribution"].keys():
354 if files[file].has_key("byhand"):
357 if Cnf.has_key("Suite:%s::Components" % (suite)) and not Cnf.has_key("Suite::%s::Components::%s" % (suite, files[file]["component"])):
358 reject_message = reject_message + "Rejected: unknown component `%s' for suite `%s'.\n" % (files[file]["component"], suite)
361 # See if the package is NEW
362 if not in_override_p(files[file]["package"], files[file]["component"], suite, files[file].get("dbtype","")):
363 files[file]["new"] = 1
365 # Find any old binary packages
366 if files[file]["type"] == "deb":
367 q = projectB.query("SELECT b.id, b.version, f.filename, l.path, c.name FROM binaries b, bin_associations ba, suite s, location l, component c, architecture a, files f WHERE b.package = '%s' AND s.suite_name = '%s' AND a.arch_string = '%s' AND ba.bin = b.id AND ba.suite = s.id AND b.architecture = a.id AND f.location = l.id AND l.component = c.id AND b.file = f.id"
368 % (files[file]["package"], suite, files[file]["architecture"]))
369 oldfiles = q.dictresult()
370 for oldfile in oldfiles:
371 files[file]["oldfiles"][suite] = oldfile
372 # Check versions [NB: per-suite only; no cross-suite checking done (yet)]
373 if apt_pkg.VersionCompare(files[file]["version"], oldfile["version"]) != 1:
374 if Cnf["Dinstall::Options::No-Version-Check"]:
375 reject_message = reject_message + "Overriden rejection"
377 reject_message = reject_message + "Rejected"
378 reject_message = reject_message + ": %s Old version `%s' >= new version `%s'.\n" % (file, oldfile["version"], files[file]["version"])
379 # Check for existing copies of the file
380 if not changes.has_key("stable upload"):
381 q = projectB.query("SELECT b.id FROM binaries b, architecture a WHERE b.package = '%s' AND b.version = '%s' AND a.arch_string = '%s' AND a.id = b.architecture" % (files[file]["package"], files[file]["version"], files[file]["architecture"]))
382 if q.getresult() != []:
383 reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (file)
385 # Find any old .dsc files
386 elif files[file]["type"] == "dsc":
387 q = projectB.query("SELECT s.id, s.version, f.filename, l.path, c.name FROM source s, src_associations sa, suite su, location l, component c, files f WHERE s.source = '%s' AND su.suite_name = '%s' AND sa.source = s.id AND sa.suite = su.id AND f.location = l.id AND l.component = c.id AND f.id = s.file"
388 % (files[file]["package"], suite))
389 oldfiles = q.dictresult()
390 if len(oldfiles) >= 1:
391 files[file]["oldfiles"][suite] = oldfiles[0]
393 # Validate the component
394 component = files[file]["component"];
395 component_id = db_access.get_component_id(component);
396 if component_id == -1:
397 reject_message = reject_message + "Rejected: file '%s' has unknown component '%s'.\n" % (file, component);
400 # Check the md5sum & size against existing files (if any)
401 location = Cnf["Dir::PoolDir"];
402 files[file]["location id"] = db_access.get_location_id (location, component, archive);
404 files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"]);
405 files_id = db_access.get_files_id(files[file]["pool name"] + file, files[file]["size"], files[file]["md5sum"], files[file]["location id"]);
407 reject_message = reject_message + "Rejected: INTERNAL ERROR, get_files_id() returned multiple matches for %s.\n" % (file)
409 reject_message = reject_message + "Rejected: md5sum and/or size mismatch on existing copy of %s.\n" % (file)
410 files[file]["files id"] = files_id
412 # Check for packages that have moved from one component to another
413 if files[file]["oldfiles"].has_key(suite) and files[file]["oldfiles"][suite]["name"] != files[file]["component"]:
414 files[file]["othercomponents"] = files[file]["oldfiles"][suite]["name"];
417 if string.find(reject_message, "Rejected:") != -1:
422 ###############################################################################
425 global dsc, dsc_files, reject_message, reprocess, orig_tar_id, legacy_source_untouchable;
427 for file in files.keys():
428 if files[file]["type"] == "dsc":
430 dsc = utils.parse_changes(file)
431 except utils.cant_open_exc:
432 reject_message = reject_message + "Rejected: can't read changes file '%s'.\n" % (filename)
434 except utils.changes_parse_error_exc, line:
435 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
438 dsc_files = utils.build_file_list(dsc, 1)
439 except utils.no_files_exc:
440 reject_message = reject_message + "Rejected: no Files: field in .dsc file.\n";
443 # Try and find all files mentioned in the .dsc. This has
444 # to work harder to cope with the multiple possible
445 # locations of an .orig.tar.gz.
446 for dsc_file in dsc_files.keys():
447 if files.has_key(dsc_file):
448 actual_md5 = files[dsc_file]["md5sum"];
449 actual_size = int(files[dsc_file]["size"]);
450 found = "%s in incoming" % (dsc_file)
451 # Check the file does not already exist in the archive
452 if not changes.has_key("stable upload"):
453 q = projectB.query("SELECT f.id FROM files f, location l WHERE (f.filename ~ '/%s$' OR f.filename = '%s') AND l.id = f.location" % (utils.regex_safe(dsc_file), dsc_file));
454 if q.getresult() != []:
455 reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (dsc_file)
456 elif dsc_file[-12:] == ".orig.tar.gz":
458 q = projectB.query("SELECT l.path, f.filename, l.type, f.id FROM files f, location l WHERE (f.filename ~ '/%s$' OR f.filename = '%s') AND l.id = f.location" % (utils.regex_safe(dsc_file), dsc_file));
463 # Unfortunately, we make get more than one match
464 # here if, for example, the package was in potato
465 # but had a -sa upload in woody. So we need to a)
466 # choose the right one and b) mark all wrong ones
467 # as excluded from the source poolification (to
468 # avoid file overwrites).
470 x = ql[0]; # default to something sane in case we don't match any or have only one
474 old_file = i[0] + i[1];
475 actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
476 actual_size = os.stat(old_file)[stat.ST_SIZE];
477 if actual_md5 == dsc_files[dsc_file]["md5sum"] and actual_size == int(dsc_files[dsc_file]["size"]):
480 legacy_source_untouchable[i[3]] = "";
482 old_file = x[0] + x[1];
483 actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
484 actual_size = os.stat(old_file)[stat.ST_SIZE];
487 dsc_files[dsc_file]["files id"] = x[3]; # need this for updating dsc_files in install()
489 if suite_type == "legacy" or suite_type == "legacy-mixed":
492 # Not there? Check in Incoming...
493 # [See comment above process_it() for explanation
494 # of why this is necessary...]
495 if os.access(dsc_file, os.R_OK) != 0:
496 files[dsc_file] = {};
497 files[dsc_file]["size"] = os.stat(dsc_file)[stat.ST_SIZE];
498 files[dsc_file]["md5sum"] = dsc_files[dsc_file]["md5sum"];
499 files[dsc_file]["section"] = files[file]["section"];
500 files[dsc_file]["priority"] = files[file]["priority"];
501 files[dsc_file]["component"] = files[file]["component"];
502 files[dsc_file]["type"] = "orig.tar.gz";
506 reject_message = reject_message + "Rejected: %s refers to %s, but I can't find it in Incoming or in the pool.\n" % (file, dsc_file);
509 reject_message = reject_message + "Rejected: %s refers to %s, but I can't find it in Incoming." % (file, dsc_file);
511 if actual_md5 != dsc_files[dsc_file]["md5sum"]:
512 reject_message = reject_message + "Rejected: md5sum for %s doesn't match %s.\n" % (found, file);
513 if actual_size != int(dsc_files[dsc_file]["size"]):
514 reject_message = reject_message + "Rejected: size for %s doesn't match %s.\n" % (found, file);
516 if string.find(reject_message, "Rejected:") != -1:
521 ###############################################################################
523 # Some cunning stunt broke dpkg-source in dpkg 1.8{,.1}; detect the
524 # resulting bad source packages and reject them.
526 # Even more amusingly the fix in 1.8.1.1 didn't actually fix the
527 # problem just changed the symptoms.
530 global dsc, dsc_files, reject_message, reprocess, orig_tar_id;
532 for filename in files.keys():
533 if files[filename]["type"] == "diff.gz":
534 file = gzip.GzipFile(filename, 'r');
535 for line in file.readlines():
536 if re_bad_diff.search(line):
537 reject_message = reject_message + "Rejected: source package was produced by broken dpkg 1.8.1[.1]; please rebuild with later version.\n";
540 if string.find(reject_message, "Rejected:") != -1:
545 ###############################################################################
547 def check_md5sums ():
548 global reject_message;
550 for file in files.keys():
552 file_handle = utils.open_file(file,"r");
553 except utils.cant_open_exc:
556 if apt_pkg.md5sum(file_handle) != files[file]["md5sum"]:
557 reject_message = reject_message + "Rejected: md5sum check failed for %s.\n" % (file);
559 #####################################################################################################################
561 def action (changes_filename):
562 byhand = confirm = suites = summary = new = "";
564 # changes["distribution"] may not exist in corner cases
565 # (e.g. unreadable changes files)
566 if not changes.has_key("distribution"):
567 changes["distribution"] = {};
569 for suite in changes["distribution"].keys():
570 if Cnf.has_key("Suite::%s::Confirm"):
571 confirm = confirm + suite + ", "
572 suites = suites + suite + ", "
573 confirm = confirm[:-2]
576 for file in files.keys():
577 if files[file].has_key("byhand"):
579 summary = summary + file + " byhand\n"
580 elif files[file].has_key("new"):
582 summary = summary + "(new) %s %s %s\n" % (file, files[file]["priority"], files[file]["section"])
583 if files[file].has_key("othercomponents"):
584 summary = summary + "WARNING: Already present in %s distribution.\n" % (files[file]["othercomponents"])
585 if files[file]["type"] == "deb":
586 summary = summary + apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))["Description"] + '\n';
588 files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"])
589 destination = Cnf["Dir::PoolRoot"] + files[file]["pool name"] + file
590 summary = summary + file + "\n to " + destination + "\n"
592 short_summary = summary;
594 # This is for direport's benefit...
595 f = re_fdnic.sub("\n .\n", changes.get("changes",""));
597 if confirm or byhand or new:
598 summary = summary + "Changes: " + f;
600 summary = summary + announce (short_summary, 0)
602 (prompt, answer) = ("", "XXX")
603 if Cnf["Dinstall::Options::No-Action"] or Cnf["Dinstall::Options::Automatic"]:
606 if string.find(reject_message, "Rejected") != -1:
607 if time.time()-os.path.getmtime(changes_filename) < 86400:
608 print "SKIP (too new)\n" + reject_message,;
609 prompt = "[S]kip, Manual reject, Quit ?";
611 print "REJECT\n" + reject_message,;
612 prompt = "[R]eject, Manual reject, Skip, Quit ?";
613 if Cnf["Dinstall::Options::Automatic"]:
616 print "NEW to %s\n%s%s" % (suites, reject_message, summary),;
617 prompt = "[S]kip, New ack, Manual reject, Quit ?";
618 if Cnf["Dinstall::Options::Automatic"] and Cnf["Dinstall::Options::Ack-New"]:
621 print "BYHAND\n" + reject_message + summary,;
622 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
624 print "CONFIRM to %s\n%s%s" % (confirm, reject_message, summary),
625 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
627 print "INSTALL\n" + reject_message + summary,;
628 prompt = "[I]nstall, Manual reject, Skip, Quit ?";
629 if Cnf["Dinstall::Options::Automatic"]:
632 while string.find(prompt, answer) == -1:
634 answer = utils.our_raw_input()
635 m = re_default_answer.match(prompt)
638 answer = string.upper(answer[:1])
641 reject (changes_filename, "");
643 manual_reject (changes_filename);
645 install (changes_filename, summary, short_summary);
647 acknowledge_new (changes_filename, summary);
651 #####################################################################################################################
653 def install (changes_filename, summary, short_summary):
654 global install_count, install_bytes
656 # Stable uploads are a special case
657 if changes.has_key("stable upload"):
658 stable_install (changes_filename, summary, short_summary);
663 archive = utils.where_am_i();
665 # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
666 projectB.query("BEGIN WORK");
668 # Add the .dsc file to the DB
669 for file in files.keys():
670 if files[file]["type"] == "dsc":
671 package = dsc["source"]
672 version = dsc["version"] # NB: not files[file]["version"], that has no epoch
673 maintainer = dsc["maintainer"]
674 maintainer = string.replace(maintainer, "'", "\\'")
675 maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
676 filename = files[file]["pool name"] + file;
677 dsc_location_id = files[file]["location id"];
678 if not files[file]["files id"]:
679 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], dsc_location_id)
680 projectB.query("INSERT INTO source (source, version, maintainer, file) VALUES ('%s', '%s', %d, %d)"
681 % (package, version, maintainer_id, files[file]["files id"]))
683 for suite in changes["distribution"].keys():
684 suite_id = db_access.get_suite_id(suite);
685 projectB.query("INSERT INTO src_associations (suite, source) VALUES (%d, currval('source_id_seq'))" % (suite_id))
687 # Add the source files to the DB (files and dsc_files)
688 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files[file]["files id"]));
689 for dsc_file in dsc_files.keys():
690 filename = files[file]["pool name"] + dsc_file;
691 # If the .orig.tar.gz is already in the pool, it's
692 # files id is stored in dsc_files by check_dsc().
693 files_id = dsc_files[dsc_file].get("files id", None);
695 files_id = db_access.get_files_id(filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], dsc_location_id);
696 # FIXME: needs to check for -1/-2 and or handle exception
698 files_id = db_access.set_files_id (filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], dsc_location_id);
699 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files_id));
701 # Add the .deb files to the DB
702 for file in files.keys():
703 if files[file]["type"] == "deb":
704 package = files[file]["package"]
705 version = files[file]["version"]
706 maintainer = files[file]["maintainer"]
707 maintainer = string.replace(maintainer, "'", "\\'")
708 maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
709 architecture = files[file]["architecture"]
710 architecture_id = db_access.get_architecture_id (architecture);
711 type = files[file]["dbtype"];
712 component = files[file]["component"]
713 source = files[file]["source"]
715 if string.find(source, "(") != -1:
716 m = utils.re_extract_src_version.match(source)
718 source_version = m.group(2)
719 if not source_version:
720 source_version = version
721 filename = files[file]["pool name"] + file;
722 if not files[file]["files id"]:
723 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], files[file]["location id"])
724 source_id = db_access.get_source_id (source, source_version);
726 projectB.query("INSERT INTO binaries (package, version, maintainer, source, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, %d, '%s')"
727 % (package, version, maintainer_id, source_id, architecture_id, files[file]["files id"], type));
729 projectB.query("INSERT INTO binaries (package, version, maintainer, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, '%s')"
730 % (package, version, maintainer_id, architecture_id, files[file]["files id"], type));
731 for suite in changes["distribution"].keys():
732 suite_id = db_access.get_suite_id(suite);
733 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES (%d, currval('binaries_id_seq'))" % (suite_id));
735 # If the .orig.tar.gz is in a legacy directory we need to poolify
736 # it, so that apt-get source (and anything else that goes by the
737 # "Directory:" field in the Sources.gz file) works.
738 if orig_tar_id != None:
739 q = projectB.query("SELECT DISTINCT ON (f.id) l.path, f.filename, f.id as files_id, df.source, df.id as dsc_files_id, f.size, f.md5sum FROM files f, dsc_files df, location l WHERE df.source IN (SELECT source FROM dsc_files WHERE file = %s) AND f.id = df.file AND l.id = f.location AND (l.type = 'legacy' OR l.type = 'legacy-mixed')" % (orig_tar_id));
742 # Is this an old upload superseded by a newer -sa upload? (See check_dsc() for details)
743 if legacy_source_untouchable.has_key(qid["files_id"]):
745 # First move the files to the new location
746 legacy_filename = qid["path"]+qid["filename"];
747 pool_location = utils.poolify (changes["source"], files[file]["component"]);
748 pool_filename = pool_location + os.path.basename(qid["filename"]);
749 destination = Cnf["Dir::PoolDir"] + pool_location
750 utils.move(legacy_filename, destination);
751 # Then Update the DB's files table
752 q = projectB.query("UPDATE files SET filename = '%s', location = '%s' WHERE id = '%s'" % (pool_filename, dsc_location_id, qid["files_id"]));
754 # Install the files into the pool
755 for file in files.keys():
756 if files[file].has_key("byhand"):
758 destination = Cnf["Dir::PoolDir"] + files[file]["pool name"] + file
759 destdir = os.path.dirname(destination)
760 utils.move (file, destination)
761 install_bytes = install_bytes + float(files[file]["size"])
763 # Copy the .changes file across for suite which need it.
764 for suite in changes["distribution"].keys():
765 if Cnf.has_key("Suite::%s::CopyChanges" % (suite)):
766 utils.copy (changes_filename, Cnf["Dir::RootDir"] + Cnf["Suite::%s::CopyChanges" % (suite)]);
768 projectB.query("COMMIT WORK");
770 utils.move (changes_filename, Cnf["Dir::IncomingDir"] + 'DONE/' + os.path.basename(changes_filename))
772 install_count = install_count + 1;
774 if not Cnf["Dinstall::Options::No-Mail"]:
775 mail_message = """Return-Path: %s
778 Bcc: troup@auric.debian.org
779 Subject: %s INSTALLED
785 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], os.path.basename(changes_filename), reject_message, summary, installed_footer)
786 utils.send_mail (mail_message, "")
787 announce (short_summary, 1)
789 #####################################################################################################################
791 def stable_install (changes_filename, summary, short_summary):
792 global install_count, install_bytes
794 print "Installing to stable."
796 archive = utils.where_am_i();
798 # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
799 projectB.query("BEGIN WORK");
801 # Add the .dsc file to the DB
802 for file in files.keys():
803 if files[file]["type"] == "dsc":
804 package = dsc["source"]
805 version = dsc["version"] # NB: not files[file]["version"], that has no epoch
806 q = projectB.query("SELECT id FROM source WHERE source = '%s' AND version = '%s'" % (package, version))
809 sys.stderr.write("INTERNAL ERROR: couldn't find '%s' (%s) in source table.\n" % (package, version));
811 source_id = ql[0][0];
812 suite_id = db_access.get_suite_id('proposed-updates');
813 projectB.query("DELETE FROM src_associations WHERE suite = '%s' AND source = '%s'" % (suite_id, source_id));
814 suite_id = db_access.get_suite_id('stable');
815 projectB.query("INSERT INTO src_associations (suite, source) VALUES ('%s', '%s')" % (suite_id, source_id));
817 # Add the .deb files to the DB
818 for file in files.keys():
819 if files[file]["type"] == "deb":
820 package = files[file]["package"]
821 version = files[file]["version"]
822 architecture = files[file]["architecture"]
823 q = projectB.query("SELECT b.id FROM binaries b, architecture a WHERE b.package = '%s' AND b.version = '%s' AND (a.arch_string = '%s' OR a.arch_string = 'all') AND b.architecture = a.id" % (package, version, architecture))
826 sys.stderr.write("INTERNAL ERROR: couldn't find '%s' (%s for %s architecture) in binaries table.\n" % (package, version, architecture));
828 binary_id = ql[0][0];
829 suite_id = db_access.get_suite_id('proposed-updates');
830 projectB.query("DELETE FROM bin_associations WHERE suite = '%s' AND bin = '%s'" % (suite_id, binary_id));
831 suite_id = db_access.get_suite_id('stable');
832 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES ('%s', '%s')" % (suite_id, binary_id));
834 projectB.query("COMMIT WORK");
836 utils.move (changes_filename, Cnf["Rhona::Morgue"] + os.path.basename(changes_filename));
838 # Update the Stable ChangeLog file
840 new_changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + ".ChangeLog";
841 changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + "ChangeLog";
842 if os.path.exists(new_changelog_filename):
843 os.unlink (new_changelog_filename);
845 new_changelog = utils.open_file(new_changelog_filename, 'w');
846 for file in files.keys():
847 if files[file]["type"] == "deb":
848 new_changelog.write("stable/%s/binary-%s/%s\n" % (files[file]["component"], files[file]["architecture"], file));
849 elif re_issource.match(file) != None:
850 new_changelog.write("stable/%s/source/%s\n" % (files[file]["component"], file));
852 new_changelog.write("%s\n" % (file));
853 chop_changes = re_fdnic.sub("\n", changes["changes"]);
854 new_changelog.write(chop_changes + '\n\n');
855 if os.access(changelog_filename, os.R_OK) != 0:
856 changelog = utils.open_file(changelog_filename, 'r');
857 new_changelog.write(changelog.read());
858 new_changelog.close();
859 if os.access(changelog_filename, os.R_OK) != 0:
860 os.unlink(changelog_filename);
861 utils.move(new_changelog_filename, changelog_filename);
863 install_count = install_count + 1;
865 if not Cnf["Dinstall::Options::No-Mail"]:
866 mail_message = """Return-Path: %s
869 Bcc: troup@auric.debian.org
870 Subject: %s INSTALLED into stable
876 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], os.path.basename(changes_filename), reject_message, summary, installed_footer)
877 utils.send_mail (mail_message, "")
878 announce (short_summary, 1)
880 #####################################################################################################################
882 def reject (changes_filename, manual_reject_mail_filename):
885 base_changes_filename = os.path.basename(changes_filename);
886 reason_filename = re_changes.sub("reason", base_changes_filename);
887 reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename);
889 # Move the .changes files and it's contents into REJECT/ (if we can; errors are ignored)
891 utils.move (changes_filename, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], base_changes_filename));
892 except utils.cant_overwrite_exc:
893 sys.stderr.write("W: couldn't overwrite existing file '%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], base_changes_filename));
895 for file in files.keys():
896 if os.path.exists(file):
898 utils.move (file, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], file));
899 except utils.cant_overwrite_exc:
900 sys.stderr.write("W: couldn't overwrite existing file '%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], file));
903 # If this is not a manual rejection generate the .reason file and rejection mail message
904 if manual_reject_mail_filename == "":
905 if os.path.exists(reject_filename):
906 os.unlink(reject_filename);
907 fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
908 os.write(fd, reject_message);
910 reject_mail_message = """From: %s
912 Bcc: troup@auric.debian.org
917 %s""" % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], os.path.basename(changes_filename), reject_message, reject_footer);
918 else: # Have a manual rejection file to use
919 reject_mail_message = ""; # avoid <undef>'s
921 # Send the rejection mail if appropriate
922 if not Cnf["Dinstall::Options::No-Mail"]:
923 utils.send_mail (reject_mail_message, manual_reject_mail_filename);
925 ##################################################################
927 def manual_reject (changes_filename):
928 # Build up the rejection email
929 user_email_address = string.replace(string.split(pwd.getpwuid(os.getuid())[4],',')[0], '.', '')
930 user_email_address = user_email_address + " <%s@%s>" % (pwd.getpwuid(os.getuid())[0], Cnf["Dinstall::MyHost"])
931 manual_reject_message = Cnf.get("Dinstall::Options::Manual-Reject", "")
933 reject_mail_message = """From: %s
936 Bcc: troup@auric.debian.org
942 %s""" % (user_email_address, Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], os.path.basename(changes_filename), manual_reject_message, reject_message, reject_footer)
944 # Write the rejection email out as the <foo>.reason file
945 reason_filename = re_changes.sub("reason", os.path.basename(changes_filename));
946 reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename)
947 if os.path.exists(reject_filename):
948 os.unlink(reject_filename);
949 fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
950 os.write(fd, reject_mail_message);
953 # If we weren't given one, spawn an editor so the user can add one in
954 if manual_reject_message == "":
955 result = os.system("vi +6 %s" % (reject_file))
957 sys.stderr.write ("vi invocation failed for `%s'!" % (reject_file))
960 # Then process it as if it were an automatic rejection
961 reject (changes_filename, reject_filename)
963 #####################################################################################################################
965 def acknowledge_new (changes_filename, summary):
968 changes_filename = os.path.basename(changes_filename);
970 new_ack_new[changes_filename] = 1;
972 if new_ack_old.has_key(changes_filename):
973 print "Ack already sent.";
976 print "Sending new ack.";
977 if not Cnf["Dinstall::Options::No-Mail"]:
978 new_ack_message = """Return-Path: %s
981 Bcc: troup@auric.debian.org
985 %s""" % (Cnf["Dinstall::MyEmailAddress"], Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes_filename, summary, new_ack_footer);
986 utils.send_mail(new_ack_message,"");
988 #####################################################################################################################
990 def announce (short_summary, action):
991 # Only do announcements for source uploads with a recent dpkg-dev installed
992 if float(changes.get("format", 0)) < 1.6 or not changes["architecture"].has_key("source"):
998 for dist in changes["distribution"].keys():
999 list = Cnf.Find("Suite::%s::Announce" % (dist))
1000 if list == None or lists_done.has_key(list):
1002 lists_done[list] = 1
1003 summary = summary + "Announcing to %s\n" % (list)
1006 mail_message = """Return-Path: %s
1009 Bcc: troup@auric.debian.org
1010 Subject: Installed %s %s (%s)
1016 """ % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], list, changes["source"], changes["version"], string.join(changes["architecture"].keys(), ' ' ),
1017 changes["filecontents"], short_summary)
1018 utils.send_mail (mail_message, "")
1020 (dsc_rfc822, dsc_name, dsc_email) = utils.fix_maintainer (dsc.get("maintainer",Cnf["Dinstall::MyEmailAddress"]));
1021 bugs = changes["closes"].keys()
1023 if dsc_name == changes["maintainername"]:
1024 summary = summary + "Closing bugs: "
1026 summary = summary + "%s " % (bug)
1028 mail_message = """Return-Path: %s
1030 To: %s-close@bugs.debian.org
1031 Bcc: troup@auric.debian.org
1032 Subject: Bug#%s: fixed in %s %s
1034 We believe that the bug you reported is fixed in the latest version of
1035 %s, which has been installed in the Debian FTP archive:
1037 %s""" % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], bug, bug, changes["source"], changes["version"], changes["source"], short_summary)
1039 if changes["distribution"].has_key("stable"):
1040 mail_message = mail_message + """Note that this package is not part of the released stable Debian
1041 distribution. It may have dependencies on other unreleased software,
1042 or other instabilities. Please take care if you wish to install it.
1043 The update will eventually make its way into the next released Debian
1046 mail_message = mail_message + """A summary of the changes between this version and the previous one is
1049 Thank you for reporting the bug, which will now be closed. If you
1050 have further comments please address them to %s@bugs.debian.org,
1051 and the maintainer will reopen the bug report if appropriate.
1053 Debian distribution maintenance software
1055 %s (supplier of updated %s package)
1057 (This message was generated automatically at their request; if you
1058 believe that there is a problem with it please contact the archive
1059 administrators by mailing ftpmaster@debian.org)
1062 %s""" % (bug, changes["maintainer"], changes["source"], changes["filecontents"])
1064 utils.send_mail (mail_message, "")
1066 summary = summary + "Setting bugs to severity fixed: "
1067 control_message = ""
1069 summary = summary + "%s " % (bug)
1070 control_message = control_message + "severity %s fixed\n" % (bug)
1071 if action and control_message != "":
1072 mail_message = """Return-Path: %s
1074 To: control@bugs.debian.org
1075 Bcc: troup@auric.debian.org, %s
1076 Subject: Fixed in NMU of %s %s
1081 This message was generated automatically in response to a
1082 non-maintainer upload. The .changes file follows.
1085 """ % (Cnf["Dinstall::MyEmailAddress"], changes["maintainer822"], changes["maintainer822"], changes["source"], changes["version"], control_message, changes["filecontents"])
1086 utils.send_mail (mail_message, "")
1087 summary = summary + "\n"
1091 ###############################################################################
1093 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
1094 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
1095 # dsccheckdistrib() can find the .orig.tar.gz but it will not have
1096 # processed it during it's checks of -2. If -1 has been deleted or
1097 # otherwise not checked by da-install, the .orig.tar.gz will not have
1098 # been checked at all. To get round this, we force the .orig.tar.gz
1099 # into the .changes structure and reprocess the .changes file.
1101 def process_it (changes_file):
1102 global reprocess, orig_tar_id, changes, dsc, dsc_files, files;
1106 # Reset some globals
1112 legacy_source_untouchable = {};
1114 # Absolutize the filename to avoid the requirement of being in the
1115 # same directory as the .changes file.
1116 changes_file = os.path.abspath(changes_file);
1118 # And since handling of installs to stable munges with the CWD;
1119 # save and restore it.
1122 check_signature (changes_file);
1123 check_changes (changes_file);
1131 action(changes_file);
1136 ###############################################################################
1139 global Cnf, projectB, reject_message, install_bytes, new_ack_old
1143 Cnf = apt_pkg.newConfiguration();
1144 apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file());
1146 Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
1147 ('d',"debug","Dinstall::Options::Debug", "IntVal"),
1148 ('h',"help","Dinstall::Options::Help"),
1149 ('k',"ack-new","Dinstall::Options::Ack-New"),
1150 ('m',"manual-reject","Dinstall::Options::Manual-Reject", "HasArg"),
1151 ('n',"no-action","Dinstall::Options::No-Action"),
1152 ('p',"no-lock", "Dinstall::Options::No-Lock"),
1153 ('r',"no-version-check", "Dinstall::Options::No-Version-Check"),
1154 ('s',"no-mail", "Dinstall::Options::No-Mail"),
1155 ('u',"override-distribution", "Dinstall::Options::Override-Distribution", "HasArg"),
1156 ('v',"version","Dinstall::Options::Version")];
1158 changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
1160 if Cnf["Dinstall::Options::Help"]:
1163 if Cnf["Dinstall::Options::Version"]:
1164 print "katie version 0.0000000000";
1167 postgresql_user = None; # Default == Connect as user running program.
1169 # -n/--dry-run invalidates some other options which would involve things happening
1170 if Cnf["Dinstall::Options::No-Action"]:
1171 Cnf["Dinstall::Options::Automatic"] = ""
1172 Cnf["Dinstall::Options::Ack-New"] = ""
1173 postgresql_user = Cnf["DB::ROUser"];
1175 projectB = pg.connect('projectb', Cnf["DB::Host"], int(Cnf["DB::Port"]), None, None, postgresql_user);
1177 db_access.init(Cnf, projectB);
1179 # Check that we aren't going to clash with the daily cron job
1181 if os.path.exists("%s/Archive_Maintenance_In_Progress" % (Cnf["Dir::RootDir"])) and not Cnf["Dinstall::Options::No-Lock"]:
1182 sys.stderr.write("Archive maintenance in progress. Try again later.\n");
1185 # Obtain lock if not in no-action mode
1187 if not Cnf["Dinstall::Options::No-Action"]:
1188 lock_fd = os.open(Cnf["Dinstall::LockFile"], os.O_RDWR);
1189 fcntl.lockf(lock_fd, FCNTL.F_TLOCK);
1191 # Read in the list of already-acknowledged NEW packages
1192 new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'r');
1194 for line in new_ack_list.readlines():
1195 new_ack_old[line[:-1]] = 1;
1196 new_ack_list.close();
1198 # Process the changes files
1199 for changes_file in changes_files:
1201 print "\n" + changes_file;
1202 process_it (changes_file);
1206 if install_count > 1:
1208 sys.stderr.write("Installed %d package %s, %s.\n" % (install_count, sets, utils.size_type(int(install_bytes))));
1210 # Write out the list of already-acknowledged NEW packages
1211 if Cnf["Dinstall::Options::Ack-New"]:
1212 new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'w')
1213 for i in new_ack_new.keys():
1214 new_ack_list.write(i+'\n')
1215 new_ack_list.close()
1218 if __name__ == '__main__':