Merge lp:~cjwatson/launchpad/refactor-cron-germinate into lp:launchpad
- refactor-cron-germinate
- Merge into devel
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Jeroen T. Vermeulen | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 14516 | ||||
Proposed branch: | lp:~cjwatson/launchpad/refactor-cron-germinate | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
1226 lines (+950/-135) 10 files modified
cronscripts/generate-extra-overrides.py (+18/-0) cronscripts/publishing/cron.germinate (+5/-126) database/schema/security.cfg (+4/-0) lib/lp/archivepublisher/config.py (+3/-0) lib/lp/archivepublisher/scripts/generate_extra_overrides.py (+339/-0) lib/lp/archivepublisher/tests/publisher-config.txt (+7/-0) lib/lp/archivepublisher/tests/test_generate_extra_overrides.py (+567/-0) lib/lp/soyuz/scripts/tests/germinate-test-data/mock-bin/germinate (+0/-5) lib/lp/soyuz/scripts/tests/germinate-test-data/mock-lp-root/cronscripts/generate-extra-overrides.py (+5/-0) lib/lp/soyuz/scripts/tests/test_cron_germinate.py (+2/-4) |
||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/refactor-cron-germinate | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jeroen T. Vermeulen (community) | Approve | ||
Review via email: mp+84624@code.launchpad.net |
Commit message
[r=jtv][bug=899972] Reimplement most of cron.germinate in Python using the new python-germinate package. This no longer needs to recompute the dependency expansion of common seeds over and over again, so is much faster.
Description of the change
= Summary =
cron.germinate currently takes on the order of ten minutes, and really doesn't have a particularly good excuse for taking so long. Fixing this would probably be enough to let us move back to 30-minute publisher cycles for Ubuntu (as we used to have with dak, back in the dawn of time), which would increase our velocity and make some of us very happy.
The main reason it's so slow is that it runs germinate as a separate process once for each of eight flavours (Ubuntu, Kubuntu, etc., each with its own seed collection) on each of five architectures. There are a few inefficiencies inherent in this approach, but the most significant is that it has to expand dependencies and build-dependencies of the seeds that are common to all the flavours eight times as often as necessary. Since the build-dependency chain in particular of the base system winds its way through a good fraction of main, this winds up being rather a lot of duplicated work.
== Proposed fix ==
I've been working on this problem for some time now, and have just released germinate 2.0 to support solving it properly. The most important change here is that germinate can now process multiple seed collections for a single architecture in a single instance of the core Germinator class, which allows reusing the expansions of common seeds. While I could technically have extended the command-line interface further for this, I felt that the command-line interface was already far too complicated, and decided instead to export a public, documented, and stable Python interface which can be used for this purpose.
This branch is the other half of that fix: a LaunchpadScript that uses germinate's new Python interface to do the same work as the top part of cron.germinate (i.e. everything except for maintenance-
== Pre-implementation notes ==
The Launchpad position for a while has been that cron.germinate is owned by Ubuntu Engineering, and indeed the vast bulk of the work here was in preparing a new germinate release in Ubuntu that does what we need. However, I have talked with various Launchpad people over the last few months so that it was clear that I was working on this, and I did need some help figuring out how to approach testing. Initially I'd been planning on doing a full Soyuz publisher run so that I had Packages and Sources files to work with, but Julian said this would be too slow and pointed me towards methods like factory.
== Implementation details ==
I had to add a new attribute on PublisherConfig pointing to the germinate output directory; previously this was only known to the shell script.
I added a 'generate_
The actual override generation was a fairly straightforward translation from shell to Python.
Predictably, the bulk of the work was in writing tests, since cron.germinate was previously untested and I didn't think continuing with untested code was likely to fly once it was in Python. The top-level shell script is still untested, but there's a lot less in it and I expect that it can eventually be removed entirely.
I ran into a couple of existing bugs that slowed me down. Firstly, I'd hoped to use the fake librarian to speed things up a bit, but ran into bug 713764, which was beyond my ability to fix (although I tried). Secondly, bug 694140 seriously confused me for a while; I added much the same workaround as is found elsewhere in Launchpad for that problem so that germinate's log output doesn't get lost.
I wanted to include a test that the cronscript could be run standalone, but I couldn't see how to ensure that a suitable PublisherConfig was in place.
== Tests ==
bin/test -vvct generate_
This branch makes some small changes to other archivepublisher tests, so:
bin/test -vvct archivepublisher
== Demo and Q/A ==
The deployment steps listed in bug 899972 and https:/
Once that's done, we can Q/A this by doing a timed control publisher run on mawson, rolling out this change, taking a backup copy of ubuntu-germinate and ubuntu-misc, and then doing another timed publisher run and comparing the ubuntu-germinate and ubuntu-misc directories before and after. (There may be some harmless ordering changes and such.)
== Lint ==
./cronscripts/
8: '_pythonpath' imported but unused
Seems to be standard practice in cronscripts.
./lib/lp/
1: narrative uses a moin header.
26: narrative uses a moin header.
46: narrative uses a moin header.
114: narrative uses a moin header.
138: narrative uses a moin header.
163: narrative uses a moin header.
178: want exceeds 78 characters.
179: want exceeds 78 characters.
180: want exceeds 78 characters.
182: want exceeds 78 characters.
This was pre-existing lint; I only made the minimal changes necessary to add the 'germinateroot' attribute, which haven't fundamentally made the situation any worse.
Colin Watson (cjwatson) wrote : | # |
Colin Watson (cjwatson) wrote : | # |
Right. I've tracked down all the problems I could find and released germinate 2.1 to fix all of them; none of them were problems with this branch. The runtime is now down to about 4m50s on mawson; the only differences in output are (a) in places where it doesn't matter very much and (b) accountable to a pure-virtual dependency in libenchant1c2a, where germinate is entirely entitled to choose a provider at random, so I'm not worried about those.
Jeroen T. Vermeulen (jtv) wrote : | # |
A note on the problem of ensuring that a PublisherConfig is in place: factory.
Jeroen T. Vermeulen (jtv) wrote : | # |
It looks to me like you did the right thing in creating a dedicated database user. We may ditch the per-user privileges, but we do want separate users for separate scripts.
Jeroen T. Vermeulen (jtv) wrote : | # |
=== added file 'lib/lp/
--- lib/lp/
+++ lib/lp/
+class AtomicFile:
+ """Facilitate atomic writing of files."""
+
+ def __init__(self, filename):
+ self.filename = filename
+ self.fd = open('%s.new' % self.filename, 'w')
+
+ def __enter__(self):
+ return self.fd
+
+ def __exit__(self, exc_type, exc_value, exc_tb):
+ self.fd.close()
+ os.rename('%s.new' % self.filename, self.filename)
Does it make sense to put the file in place if the context exits with an exception?
Actually this is what I don't like about the design of python context managers: it puts you, as the author of a context manager, in the position where you may have to worry about failures in the context you manage — and yet it's easy for the author of the context to hide them from you. So maybe stupid is just better.
Jeroen T. Vermeulen (jtv) wrote : | # |
Next bit I looked at:
=== added file 'lib/lp/
+ def processOptions(
+ """Handle command-line options."""
+ if self.options.
+ raise OptionValueErro
+
+ self.distribution = getUtility(
+ self.options.
+ if self.distribution is None:
+ raise OptionValueError(
+ "Distribution '%s' not found." % self.options.
+
+ series = None
+ wanted_status = (SeriesStatus.
+ SeriesStatus.
+ for status in wanted_status:
+ series = self.distributi
+ if series.count() > 0:
+ break
+ else:
+ raise LaunchpadScript
+ 'There is no DEVELOPMENT distroseries for %s' %
+ self.options.
+ self.series = series[0]
The part from “series = None” onward seems to be an isolated unit of work. I think it looks for the first distroseries in development state, or failing that, the first in frozen state. But the structure of the code makes that hard to figure out, and then only afterwards I can start wondering why you do it exactly this way.
I fervently recommend extracting that code into a sensibly-named function. (It doesn't need to be a method: AFAICS a distribution goes in, a series comes out, and that is the full extent of its interaction with the rest of the world). Come to think of it, might there already be a suitable method in Distribution somewhere?
The structure of that loop is also a bit hard to follow. It gets easier if you have a single-purpose function that you can just return from instead of using a break!
Finally, a few cosmetic tips:
* When line-breaking a tuple, list, or dict, our “house style” is to add a line break right after the opening parenthesis, bracket, or brace. Each entry ends in a comma and a newline — including the last entry.
wanted_status = (
)
* You don't really need series.count(). In fact I think you can just retrieve the first matching series as series.first(), and you'll get None if there isn't any.
* For Python strings I find consistent double quotes a good habit, at least for free-form texts that may just as well contain an apostrophe.
Jeroen T. Vermeulen (jtv) wrote : | # |
I'm going through the diff sequentially at the moment; the parts I'm ignoring are the parts that I have nothing to say about. When I get to the end I can give you a very brief formal review. :)
=== added file 'lib/lp/
+ def getConfig(self):
+ """Set up a configuration object for this archive."""
+ for archive in self.distributi
+ # We only work on the primary archive.
+ if archive.purpose == ArchivePurpose.
+ return getPubConfig(
+ else:
+ raise LaunchpadScript
+ 'There is no PRIMARY archive for %s' %
+ self.options.
Why not just use self.distributi
(Also, remember to punctuate that error message!)
Jeroen T. Vermeulen (jtv) wrote : | # |
+ def outputPath(self, flavour, series_name, arch, base):
+ return os.path.join(
+ self.config.
+ '%s_%s_%s_%s' % (base, flavour, series_name, arch))
Our dromedary-cased method names normally start with a verb. Read that way, “outputPath” would suggest that the method prints a path.
So consider prefixing this method's name with a verb. The unimaginative catch-all verb is “get”; personally I prefer “compose” for this kind of thing.
Jeroen T. Vermeulen (jtv) wrote : | # |
+ def runGerminate(self, override_file, series_name, arch, flavours,
+ structures):
Rightly or wrongly, the “run” in that name led me to expect that this method would fire up a separate process. Maybe a very short docstring to dispel that notion?
+ germinator = Germinator(arch)
+
+ # Read archive metadata.
+ archive = TagFile(
+ series_name, self.components, arch,
+ 'file://%s' % self.config.
+ germinator.
+
+ for flavour in flavours:
To me, your branch is of very high quality but this is its Achilles' heel. The following loop body is massive! I can't see it all on one page, so it becomes hard even to see just how long it is. If I judged the indentations right, it makes up the entire rest of the method.
Please extract it. It's well nigh impossible to see (now or in the future, after some maintenance) whether any variables are carried across loop iterations. That sort of thing can easily trip you up, especially when it happens by accident.
Once you've extracted the loop body, given its size, it's probably still worth extracting chunks from that.
According to some, the pattern of “comment, block of code, blank line, comment, block of code, blank line” is a strong indication that you need to split things up. It shows that you have already done so in your own mind, but leaves each individual reader to figure out what the dependencies between consecutive blocks are.
Also, of course, small methods with simple purposes are easier to specify in detail and cheaper to test thoroughly.
+ self.logger.
+ flavour, series_name, arch)
We follow a different rule for line-breaking function calls than we do for function definitions.
For calls, line-break right after the opening parenthesis and then indent the arguments list by 4 spaces:
self.
+ # Add this to the germinate log as well so that that can be
+ # debugged more easily. Log a separator line first.
+ self.germinate_
+ self.germinate_
+ flavour, series_name, arch,
+ extra={'progress': True})
What does the “extra” parameter do?
+ # Expand dependencies.
+ structure = structures[flavour]
+ germinator.
+ germinator.
+ germinator.
+ # Write output files.
+
+ # The structure file makes it possible to figure out how the
+ # other output files relate to each other.
+ structure.
+ flavour, series_name, arch, 'structure'))
+
+ # "all" and "all.sources" list the full set of binary and source
+ # packages respectively for a given flavour/
+ # combination.
+ all_path = ...
Colin Watson (cjwatson) wrote : | # |
I think you're right that the file shouldn't be put in place if the
context exits with an exception. If I were writing this without context
managers, then rather than:
with AtomicFile('foo') as handle:
print >>handle, 'bar'
... I'd probably write:
handle = open('foo.new', 'w')
try:
print >>handle, 'bar'
finally:
os.
And that's equivalent to the natural expression in (good) shell too:
set -e
echo bar >foo.new
mv foo.new foo
That all suggests to me that skipping the rename on exception, and
leaving the .new file in place so that somebody with access can look at
it and find out how far the script got before it fell over, is a good
thing. I'll change the code to do that.
Jeroen T. Vermeulen (jtv) wrote : | # |
Getting a better feel for the structure of runGerminate now. Which makes me wonder:
+ # Generate apt-ftparchive "extra overrides" for Build-Essential
+ # fields.
+ if ('build-essential' in structure.names and
+ flavour == flavours[0]):
+ writeOverrides(
I don't suppose you could just do this at the end, outside of all loops?
if 'build-essential' in structures[
Jeroen T. Vermeulen (jtv) wrote : | # |
Ah, I see now why you can't do that: writeOverrides is a closure. It picks up “structure” and “arch” from the surrounding method at its point of definition. There's another easy thing to break by accident! The structure should again be structures[
Might it be worthwhile to separate the closure nature of writeOverrides from its functionality? If you had a separate function that takes all the necessary variables as arguments, it'd be easier to move that last paragraph out of the loop (still assuming that that makes sense at all) and it'd be easier to see that writeOverrides “borrows” some variables from runGerminate.
Colin Watson (cjwatson) wrote : | # |
On Wed, Dec 07, 2011 at 07:20:29AM -0000, Jeroen T. Vermeulen wrote:
> The part from “series = None” onward seems to be an isolated unit of
> work. I think it looks for the first distroseries in development
> state, or failing that, the first in frozen state. But the structure
> of the code makes that hard to figure out, and then only afterwards I
> can start wondering why you do it exactly this way.
>
> I fervently recommend extracting that code into a sensibly-named
> function. (It doesn't need to be a method: AFAICS a distribution goes
> in, a series comes out, and that is the full extent of its interaction
> with the rest of the world). Come to think of it, might there already
> be a suitable method in Distribution somewhere?
Fair comment. Distribution.
sometimes returns series in statuses we don't want here; but if there's
one we can use then it will always return it, so we can just call
currentseries and then check the result.
> * For Python strings I find consistent double quotes a good habit, at
> least for free-form texts that may just as well contain an
> apostrophe.
This is an out-of-context habit of mine from Perl and shell programming:
since single and double quotes have different interpolation
characteristics there, I've trained myself to use single quotes unless
either I have an explicit reason to want interpolation or the string
contains an apostrophe and no double quotes. I realise this doesn't
make as much sense for Python, so I'll go through and amend this.
Jeroen T. Vermeulen (jtv) wrote : | # |
Some very nice tests there! To continue:
=== added file 'lib/lp/
+def file_contents(
+ """Return the contents of the file at path."""
+ with open(path) as handle:
+ return handle.read()
Since you're only reading the file, there's probably no hurry to close this file handle. So you could just return “open(path)
Colin Watson (cjwatson) wrote : | # |
[I've not responded to your entire review point-by-point, but I believe
I've addressed all points where I haven't given an explicit response.]
On Wed, Dec 07, 2011 at 09:22:24AM -0000, Jeroen T. Vermeulen wrote:
> + def runGerminate(self, override_file, series_name, arch, flavours,
> + structures):
>
> Rightly or wrongly, the “run” in that name led me to expect that this
> method would fire up a separate process. Maybe a very short docstring
> to dispel that notion?
The name doesn't actually make a lot of sense; I think originally I was
in the mindset where this actually did fire up a separate process. I've
renamed the script instance to germinateArch (with a docstring), and the
test instance to fetchGerminated
> To me, your branch is of very high quality but this is its Achilles'
> heel. The following loop body is massive! I can't see it all on one
> page, so it becomes hard even to see just how long it is. If I judged
> the indentations right, it makes up the entire rest of the method.
That's fair; I went back and forward a bit on this, but clearly landed
in the wrong place. Actually, now that I've renamed runGerminate to
germinateArch, it's easier in my mind to turn a block of it into
germinateArchFl
like it anyway.)
The methods do start ending up with quite a few parameters, but that's
probably better than the alternative.
> Once you've extracted the loop body, given its size, it's probably
> still worth extracting chunks from that.
I've done a good deal of this, although will probably end up
restructuring a bit further per your later comments.
> According to some, the pattern of “comment, block of code, blank line,
> comment, block of code, blank line” is a strong indication that you
> need to split things up. It shows that you have already done so in
> your own mind, but leaves each individual reader to figure out what
> the dependencies between consecutive blocks are.
I don't necessarily agree with this in all cases (e.g. I don't know that
the new writeGerminateO
further), but I agree that the original version of this code was rather
too far in the other direction.
> + # Add this to the germinate log as well so that that can be
> + # debugged more easily. Log a separator line first.
> + self.germinate_
> + self.germinate_
> + flavour, series_name, arch,
> + extra={'progress': True})
>
> What does the “extra” parameter do?
That attaches a dictionary of extra attributes to the log message which
can then be picked up by the formatter. Germinate uses this for certain
messages (arguably a wart in germinate's design; I should probably look
into doing that more neatly at some point). I've moved this to its own
method so that it can have a clarifying docstring.
> That's a lot of string manipulation. It may be clearer as a regex:
>
> task_header_regex = re.compile("ta...
Colin Watson (cjwatson) wrote : | # |
On Wed, Dec 07, 2011 at 09:56:26AM -0000, Jeroen T. Vermeulen wrote:
> Might it be worthwhile to separate the closure nature of
> writeOverrides from its functionality? If you had a separate function
> that takes all the necessary variables as arguments, it'd be easier to
> move that last paragraph out of the loop (still assuming that that
> makes sense at all) and it'd be easier to see that writeOverrides
> “borrows” some variables from runGerminate.
Good idea. Per our conversation on IRC, I've used functools.partial for
this. I also used it in writeGerminateO
notably improves. What do you think?
Jeroen T. Vermeulen (jtv) wrote : | # |
Thank you for bearing with me, making so many changes after producing an already large and difficult branch. I particularly like the way you seized on method extraction as an opportunity to document the algorithm's steps.
This is a massive step forward in terms of code maintenance alone, and I'm looking forward to seeing how it affects performance.
Colin Watson (cjwatson) wrote : | # |
On Wed, Dec 07, 2011 at 10:14:27AM -0000, Jeroen T. Vermeulen wrote:
> +def file_contents(
> + """Return the contents of the file at path."""
> + with open(path) as handle:
> + return handle.read()
>
> Since you're only reading the file, there's probably no hurry to close this file handle. So you could just return “open(path)
For the record, there was some discussion on this on IRC, and most
people seemed to think that it was better to retain the immediate close,
in particular since implementations other than cPython may not GC-close
it with any alacrity; so I've left this the way it is. (There is a lot
of variation on this in the Launchpad test suite, though ...)
Preview Diff
1 | === added file 'cronscripts/generate-extra-overrides.py' | |||
2 | --- cronscripts/generate-extra-overrides.py 1970-01-01 00:00:00 +0000 | |||
3 | +++ cronscripts/generate-extra-overrides.py 2011-12-14 15:49:40 +0000 | |||
4 | @@ -0,0 +1,18 @@ | |||
5 | 1 | #!/usr/bin/python -S | ||
6 | 2 | # | ||
7 | 3 | # Copyright 2011 Canonical Ltd. This software is licensed under the | ||
8 | 4 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
9 | 5 | |||
10 | 6 | """Generate extra overrides using Germinate.""" | ||
11 | 7 | |||
12 | 8 | import _pythonpath | ||
13 | 9 | |||
14 | 10 | from lp.archivepublisher.scripts.generate_extra_overrides import ( | ||
15 | 11 | GenerateExtraOverrides, | ||
16 | 12 | ) | ||
17 | 13 | |||
18 | 14 | |||
19 | 15 | if __name__ == '__main__': | ||
20 | 16 | script = GenerateExtraOverrides( | ||
21 | 17 | "generate-extra-overrides", dbuser='generate_extra_overrides') | ||
22 | 18 | script.lock_and_run() | ||
23 | 0 | 19 | ||
24 | === modified file 'cronscripts/publishing/cron.germinate' | |||
25 | --- cronscripts/publishing/cron.germinate 2011-10-27 11:36:13 +0000 | |||
26 | +++ cronscripts/publishing/cron.germinate 2011-12-14 15:49:40 +0000 | |||
27 | @@ -12,8 +12,12 @@ | |||
28 | 12 | GERMINATEROOT=$ARCHIVEROOT/../ubuntu-germinate | 12 | GERMINATEROOT=$ARCHIVEROOT/../ubuntu-germinate |
29 | 13 | 13 | ||
30 | 14 | LAUNCHPADROOT=${TEST_LAUNCHPADROOT:-/srv/launchpad.net/codelines/current} | 14 | LAUNCHPADROOT=${TEST_LAUNCHPADROOT:-/srv/launchpad.net/codelines/current} |
31 | 15 | GENERATE=$LAUNCHPADROOT/cronscripts/generate-extra-overrides.py | ||
32 | 15 | MAINTAINCE_CHECK=$LAUNCHPADROOT/cronscripts/publishing/maintenance-check.py | 16 | MAINTAINCE_CHECK=$LAUNCHPADROOT/cronscripts/publishing/maintenance-check.py |
33 | 16 | 17 | ||
34 | 18 | FLAVOURS="ubuntu kubuntu kubuntu-mobile edubuntu xubuntu mythbuntu lubuntu" | ||
35 | 19 | FLAVOURS="$FLAVOURS ubuntustudio" | ||
36 | 20 | |||
37 | 17 | ## Check to see if another germinate run is in progress | 21 | ## Check to see if another germinate run is in progress |
38 | 18 | 22 | ||
39 | 19 | LOCKFILE=$LOCKROOT/cron.germinate.lock | 23 | LOCKFILE=$LOCKROOT/cron.germinate.lock |
40 | @@ -28,134 +32,9 @@ | |||
41 | 28 | 32 | ||
42 | 29 | trap cleanup EXIT | 33 | trap cleanup EXIT |
43 | 30 | 34 | ||
44 | 31 | suite=`$LAUNCHPADROOT/scripts/ftpmaster-tools/lp-query-distro.py development` | ||
45 | 32 | archs=`$LAUNCHPADROOT/scripts/ftpmaster-tools/lp-query-distro.py -s "$suite" archs` | ||
46 | 33 | |||
47 | 34 | echo -n "Running germinate... " | ||
48 | 35 | cd $GERMINATEROOT | 35 | cd $GERMINATEROOT |
49 | 36 | 36 | ||
172 | 37 | # Clean up temporary files | 37 | $GENERATE -d ubuntu $FLAVOURS |
51 | 38 | rm -f \ | ||
52 | 39 | germinate.output ALL ALL.sources \ | ||
53 | 40 | UBUNTU-* KUBUNTU-* EDUBUNTU-* XUBUNTU-* MYTHBUNTU-* LUBUNTU-* \ | ||
54 | 41 | UBUNTUSTUDIO-* | ||
55 | 42 | rm -f all_* all.sources_* | ||
56 | 43 | rm -rf dists | ||
57 | 44 | |||
58 | 45 | # Grab local copies of Sources and Packages files, to avoid problems in case | ||
59 | 46 | # the archive changes under our feet. | ||
60 | 47 | for component in main universe restricted multiverse; do | ||
61 | 48 | base="dists/$suite/$component" | ||
62 | 49 | mkdir -p "$base/source" | ||
63 | 50 | zcat "$ARCHIVEROOT/$base/source/Sources.gz" > "$base/source/Sources" | ||
64 | 51 | for arch in $archs; do | ||
65 | 52 | mkdir -p "$base/binary-$arch" "$base/debian-installer/binary-$arch" | ||
66 | 53 | zcat "$ARCHIVEROOT/$base/binary-$arch/Packages.gz" \ | ||
67 | 54 | > "$base/binary-$arch/Packages" | ||
68 | 55 | zcat "$ARCHIVEROOT/$base/debian-installer/binary-$arch/Packages.gz" \ | ||
69 | 56 | > "$base/debian-installer/binary-$arch/Packages" | ||
70 | 57 | done | ||
71 | 58 | done | ||
72 | 59 | |||
73 | 60 | > "$MISCROOT/more-extra.override.$suite.main.new" | ||
74 | 61 | |||
75 | 62 | germinate_components=main,universe,restricted,multiverse | ||
76 | 63 | for distro in \ | ||
77 | 64 | ubuntu kubuntu kubuntu-mobile edubuntu xubuntu mythbuntu lubuntu \ | ||
78 | 65 | ubuntustudio | ||
79 | 66 | do | ||
80 | 67 | DISTRO="$(echo $distro | tr a-z A-Z)" | ||
81 | 68 | germinate_suite="$distro.$suite" | ||
82 | 69 | for arch in $archs; do | ||
83 | 70 | # Run germinate | ||
84 | 71 | echo " **************** $distro/$suite/$arch ********************* " >> germinate.output | ||
85 | 72 | germinate \ | ||
86 | 73 | --no-rdepends \ | ||
87 | 74 | -m "file://$(pwd)/" \ | ||
88 | 75 | -s "$germinate_suite" \ | ||
89 | 76 | -d "$suite" \ | ||
90 | 77 | -c "$germinate_components" \ | ||
91 | 78 | -a $arch >> germinate.output 2>&1 | ||
92 | 79 | |||
93 | 80 | # The structure file is generally useful; keep per distro/suite/arch | ||
94 | 81 | # copies for convenience | ||
95 | 82 | cp structure structure_"$distro"_"$suite"_"$arch" | ||
96 | 83 | |||
97 | 84 | # Keep per distro/suite/arch copies of 'all' and 'all.sources' for | ||
98 | 85 | # anastacia. | ||
99 | 86 | cp all all_"$distro"_"$suite"_"$arch" | ||
100 | 87 | cp all.sources all.sources_"$distro"_"$suite"_"$arch" | ||
101 | 88 | |||
102 | 89 | # Keep per distro/suite/arch copies of 'minimal' and 'standard' for | ||
103 | 90 | # jessica. | ||
104 | 91 | cp minimal minimal_"$distro"_"$suite"_"$arch" | ||
105 | 92 | cp standard standard_"$distro"_"$suite"_"$arch" | ||
106 | 93 | |||
107 | 94 | # Keep amalgamated copies of 'all' and 'all.sources', just for convenience | ||
108 | 95 | cat all >> ALL; cat all.sources >> ALL.sources | ||
109 | 96 | |||
110 | 97 | # We need to fetch a number of seeds so that we can generate Task fields | ||
111 | 98 | # for them. This changes over time and differs from derivative to | ||
112 | 99 | # derivative, so it's best to just fetch them all. | ||
113 | 100 | taskseeds="$(cut -d: -f1 structure | xargs -n1 | sort -u)" | ||
114 | 101 | |||
115 | 102 | for seed in $taskseeds; do | ||
116 | 103 | cp "$seed" "$seed"_"$distro"_"$suite"_"$arch" | ||
117 | 104 | done | ||
118 | 105 | echo " ********************************************************************** " >> germinate.output | ||
119 | 106 | echo "" >> germinate.output | ||
120 | 107 | echo -n "." | ||
121 | 108 | |||
122 | 109 | ## Generate apt-ftparchive 'extra' overrides for Task: fields | ||
123 | 110 | for seed in $taskseeds; do | ||
124 | 111 | if ! grep -iq '^Task-' "$seed.seedtext"; then | ||
125 | 112 | continue | ||
126 | 113 | fi | ||
127 | 114 | # If the seed contains Task-Name header, override the normal behavior | ||
128 | 115 | if grep -iq '^Task-Name:' "$seed.seedtext"; then | ||
129 | 116 | task=$(grep '^Task-Name:' "$seed.seedtext" | cut -d: -f2) | ||
130 | 117 | elif grep -iq '^Task-Per-Derivative:' "$seed.seedtext"; then | ||
131 | 118 | task="$distro-$seed" | ||
132 | 119 | else | ||
133 | 120 | # If a seed is not per-derivative, then we only honour it for Ubuntu, | ||
134 | 121 | # and its task name is archive-global. | ||
135 | 122 | if [ "$distro" = ubuntu ]; then | ||
136 | 123 | task="$seed" | ||
137 | 124 | else | ||
138 | 125 | continue | ||
139 | 126 | fi | ||
140 | 127 | fi | ||
141 | 128 | if grep -iq '^Task-Seeds:' "$seed.seedtext"; then | ||
142 | 129 | scanseeds="$( (grep '^Task-Seeds:' "$seed.seedtext" | cut -d: -f2 | xargs -n1; echo "$seed") | sort -u )" | ||
143 | 130 | else | ||
144 | 131 | scanseeds="$seed" | ||
145 | 132 | fi | ||
146 | 133 | for scanseed in $scanseeds; do | ||
147 | 134 | egrep -v -- \ | ||
148 | 135 | "^(-|Package| )" "$scanseed"_"$distro"_"$suite"_"$arch" | | ||
149 | 136 | awk '{print $1}' | | ||
150 | 137 | sed -e "s,$,/$arch Task $task," | | ||
151 | 138 | sort -u >> "$MISCROOT/more-extra.override.$suite.main.new" | ||
152 | 139 | done | ||
153 | 140 | done | ||
154 | 141 | |||
155 | 142 | # Generate apt-ftparchive 'extra' overrides for Build-Essential: fields | ||
156 | 143 | if [ -e build-essential ] && [ "$distro" = ubuntu ]; then | ||
157 | 144 | # Keep a copy, just for convenience | ||
158 | 145 | cp build-essential build-essential_"$distro"_"$suite"_"$arch" | ||
159 | 146 | egrep -v -- \ | ||
160 | 147 | "^(-|Package| )" build-essential_"$distro"_"$suite"_"$arch" | | ||
161 | 148 | awk '{print $1}' | | ||
162 | 149 | sed -e "s,$,/$arch Build-Essential yes," | | ||
163 | 150 | sort -u >> "$MISCROOT/more-extra.override.$suite.main.new" | ||
164 | 151 | fi | ||
165 | 152 | done | ||
166 | 153 | done | ||
167 | 154 | echo " done." | ||
168 | 155 | |||
169 | 156 | mv -f \ | ||
170 | 157 | "$MISCROOT/more-extra.override.$suite.main.new" \ | ||
171 | 158 | "$MISCROOT/more-extra.override.$suite.main" | ||
173 | 159 | 38 | ||
174 | 160 | # Now generate the Supported extra overrides for all supported distros. | 39 | # Now generate the Supported extra overrides for all supported distros. |
175 | 161 | SUITES=`$LAUNCHPADROOT/scripts/ftpmaster-tools/lp-query-distro.py supported` | 40 | SUITES=`$LAUNCHPADROOT/scripts/ftpmaster-tools/lp-query-distro.py supported` |
176 | 162 | 41 | ||
177 | === modified file 'database/schema/security.cfg' | |||
178 | --- database/schema/security.cfg 2011-12-06 21:10:57 +0000 | |||
179 | +++ database/schema/security.cfg 2011-12-14 15:49:40 +0000 | |||
180 | @@ -2266,6 +2266,10 @@ | |||
181 | 2266 | type=user | 2266 | type=user |
182 | 2267 | groups=archivepublisher | 2267 | groups=archivepublisher |
183 | 2268 | 2268 | ||
184 | 2269 | [generate_extra_overrides] | ||
185 | 2270 | type=user | ||
186 | 2271 | groups=archivepublisher | ||
187 | 2272 | |||
188 | 2269 | [process_death_row] | 2273 | [process_death_row] |
189 | 2270 | type=user | 2274 | type=user |
190 | 2271 | groups=archivepublisher | 2275 | groups=archivepublisher |
191 | 2272 | 2276 | ||
192 | === modified file 'lib/lp/archivepublisher/config.py' | |||
193 | --- lib/lp/archivepublisher/config.py 2011-08-29 16:43:10 +0000 | |||
194 | +++ lib/lp/archivepublisher/config.py 2011-12-14 15:49:40 +0000 | |||
195 | @@ -74,10 +74,12 @@ | |||
196 | 74 | pubconf.overrideroot = pubconf.archiveroot + '-overrides' | 74 | pubconf.overrideroot = pubconf.archiveroot + '-overrides' |
197 | 75 | pubconf.cacheroot = pubconf.archiveroot + '-cache' | 75 | pubconf.cacheroot = pubconf.archiveroot + '-cache' |
198 | 76 | pubconf.miscroot = pubconf.archiveroot + '-misc' | 76 | pubconf.miscroot = pubconf.archiveroot + '-misc' |
199 | 77 | pubconf.germinateroot = pubconf.archiveroot + '-germinate' | ||
200 | 77 | else: | 78 | else: |
201 | 78 | pubconf.overrideroot = None | 79 | pubconf.overrideroot = None |
202 | 79 | pubconf.cacheroot = None | 80 | pubconf.cacheroot = None |
203 | 80 | pubconf.miscroot = None | 81 | pubconf.miscroot = None |
204 | 82 | pubconf.germinateroot = None | ||
205 | 81 | 83 | ||
206 | 82 | pubconf.poolroot = os.path.join(pubconf.archiveroot, 'pool') | 84 | pubconf.poolroot = os.path.join(pubconf.archiveroot, 'pool') |
207 | 83 | pubconf.distsroot = os.path.join(pubconf.archiveroot, 'dists') | 85 | pubconf.distsroot = os.path.join(pubconf.archiveroot, 'dists') |
208 | @@ -106,6 +108,7 @@ | |||
209 | 106 | self.cacheroot, | 108 | self.cacheroot, |
210 | 107 | self.overrideroot, | 109 | self.overrideroot, |
211 | 108 | self.miscroot, | 110 | self.miscroot, |
212 | 111 | self.germinateroot, | ||
213 | 109 | self.temproot, | 112 | self.temproot, |
214 | 110 | ] | 113 | ] |
215 | 111 | 114 | ||
216 | 112 | 115 | ||
217 | === added file 'lib/lp/archivepublisher/scripts/generate_extra_overrides.py' | |||
218 | --- lib/lp/archivepublisher/scripts/generate_extra_overrides.py 1970-01-01 00:00:00 +0000 | |||
219 | +++ lib/lp/archivepublisher/scripts/generate_extra_overrides.py 2011-12-14 15:49:40 +0000 | |||
220 | @@ -0,0 +1,339 @@ | |||
221 | 1 | # Copyright 2011 Canonical Ltd. This software is licensed under the | ||
222 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
223 | 3 | |||
224 | 4 | """Generate extra overrides using Germinate.""" | ||
225 | 5 | |||
226 | 6 | __metaclass__ = type | ||
227 | 7 | __all__ = [ | ||
228 | 8 | 'GenerateExtraOverrides', | ||
229 | 9 | ] | ||
230 | 10 | |||
231 | 11 | from functools import partial | ||
232 | 12 | import logging | ||
233 | 13 | from optparse import OptionValueError | ||
234 | 14 | import os | ||
235 | 15 | import re | ||
236 | 16 | |||
237 | 17 | from germinate.germinator import Germinator | ||
238 | 18 | from germinate.archive import TagFile | ||
239 | 19 | from germinate.log import GerminateFormatter | ||
240 | 20 | from germinate.seeds import SeedStructure | ||
241 | 21 | |||
242 | 22 | from zope.component import getUtility | ||
243 | 23 | |||
244 | 24 | from canonical.launchpad.webapp.dbpolicy import ( | ||
245 | 25 | DatabaseBlockedPolicy, | ||
246 | 26 | SlaveOnlyDatabasePolicy, | ||
247 | 27 | ) | ||
248 | 28 | from lp.archivepublisher.config import getPubConfig | ||
249 | 29 | from lp.registry.interfaces.distribution import IDistributionSet | ||
250 | 30 | from lp.registry.interfaces.series import SeriesStatus | ||
251 | 31 | from lp.services.scripts.base import ( | ||
252 | 32 | LaunchpadScript, | ||
253 | 33 | LaunchpadScriptFailure, | ||
254 | 34 | ) | ||
255 | 35 | from lp.services.utils import file_exists | ||
256 | 36 | |||
257 | 37 | |||
258 | 38 | class AtomicFile: | ||
259 | 39 | """Facilitate atomic writing of files.""" | ||
260 | 40 | |||
261 | 41 | def __init__(self, filename): | ||
262 | 42 | self.filename = filename | ||
263 | 43 | self.fd = open("%s.new" % self.filename, "w") | ||
264 | 44 | |||
265 | 45 | def __enter__(self): | ||
266 | 46 | return self.fd | ||
267 | 47 | |||
268 | 48 | def __exit__(self, exc_type, exc_value, exc_tb): | ||
269 | 49 | self.fd.close() | ||
270 | 50 | if exc_type is None: | ||
271 | 51 | os.rename("%s.new" % self.filename, self.filename) | ||
272 | 52 | |||
273 | 53 | |||
274 | 54 | def find_operable_series(distribution): | ||
275 | 55 | """Find a series we can operate on in this distribution. | ||
276 | 56 | |||
277 | 57 | We are allowed to modify DEVELOPMENT or FROZEN series, but should leave | ||
278 | 58 | series with any other status alone. | ||
279 | 59 | """ | ||
280 | 60 | series = distribution.currentseries | ||
281 | 61 | if series.status in (SeriesStatus.DEVELOPMENT, SeriesStatus.FROZEN): | ||
282 | 62 | return series | ||
283 | 63 | else: | ||
284 | 64 | return None | ||
285 | 65 | |||
286 | 66 | |||
287 | 67 | class GenerateExtraOverrides(LaunchpadScript): | ||
288 | 68 | """Main class for scripts/ftpmaster-tools/generate-task-overrides.py.""" | ||
289 | 69 | |||
290 | 70 | def __init__(self, *args, **kwargs): | ||
291 | 71 | super(GenerateExtraOverrides, self).__init__(*args, **kwargs) | ||
292 | 72 | self.germinate_logger = None | ||
293 | 73 | |||
294 | 74 | def add_my_options(self): | ||
295 | 75 | """Add a 'distribution' context option.""" | ||
296 | 76 | self.parser.add_option( | ||
297 | 77 | "-d", "--distribution", dest="distribution", | ||
298 | 78 | help="Context distribution name.") | ||
299 | 79 | |||
300 | 80 | @property | ||
301 | 81 | def name(self): | ||
302 | 82 | """See `LaunchpadScript`.""" | ||
303 | 83 | # Include distribution name. Clearer to admins, but also | ||
304 | 84 | # puts runs for different distributions under separate | ||
305 | 85 | # locks so that they can run simultaneously. | ||
306 | 86 | return "%s-%s" % (self._name, self.options.distribution) | ||
307 | 87 | |||
308 | 88 | def processOptions(self): | ||
309 | 89 | """Handle command-line options.""" | ||
310 | 90 | if self.options.distribution is None: | ||
311 | 91 | raise OptionValueError("Specify a distribution.") | ||
312 | 92 | |||
313 | 93 | self.distribution = getUtility(IDistributionSet).getByName( | ||
314 | 94 | self.options.distribution) | ||
315 | 95 | if self.distribution is None: | ||
316 | 96 | raise OptionValueError( | ||
317 | 97 | "Distribution '%s' not found." % self.options.distribution) | ||
318 | 98 | |||
319 | 99 | self.series = find_operable_series(self.distribution) | ||
320 | 100 | if self.series is None: | ||
321 | 101 | raise LaunchpadScriptFailure( | ||
322 | 102 | "There is no DEVELOPMENT distroseries for %s." % | ||
323 | 103 | self.options.distribution) | ||
324 | 104 | |||
325 | 105 | # Even if DistroSeries.component_names starts including partner, we | ||
326 | 106 | # don't want it; this applies to the primary archive only. | ||
327 | 107 | self.components = [component | ||
328 | 108 | for component in self.series.component_names | ||
329 | 109 | if component != "partner"] | ||
330 | 110 | |||
331 | 111 | def getConfig(self): | ||
332 | 112 | """Set up a configuration object for this archive.""" | ||
333 | 113 | archive = self.distribution.main_archive | ||
334 | 114 | if archive: | ||
335 | 115 | return getPubConfig(archive) | ||
336 | 116 | else: | ||
337 | 117 | raise LaunchpadScriptFailure( | ||
338 | 118 | "There is no PRIMARY archive for %s." % | ||
339 | 119 | self.options.distribution) | ||
340 | 120 | |||
341 | 121 | def setUpDirs(self): | ||
342 | 122 | """Create output directories if they did not already exist.""" | ||
343 | 123 | germinateroot = self.config.germinateroot | ||
344 | 124 | if not file_exists(germinateroot): | ||
345 | 125 | self.logger.debug("Creating germinate root %s.", germinateroot) | ||
346 | 126 | os.makedirs(germinateroot) | ||
347 | 127 | miscroot = self.config.miscroot | ||
348 | 128 | if not file_exists(miscroot): | ||
349 | 129 | self.logger.debug("Creating misc root %s.", miscroot) | ||
350 | 130 | os.makedirs(miscroot) | ||
351 | 131 | |||
352 | 132 | def addLogHandler(self): | ||
353 | 133 | """Send germinate's log output to a separate file.""" | ||
354 | 134 | if self.germinate_logger is not None: | ||
355 | 135 | return | ||
356 | 136 | |||
357 | 137 | self.germinate_logger = logging.getLogger("germinate") | ||
358 | 138 | self.germinate_logger.setLevel(logging.INFO) | ||
359 | 139 | log_file = os.path.join(self.config.germinateroot, "germinate.output") | ||
360 | 140 | handler = logging.FileHandler(log_file, mode="w") | ||
361 | 141 | handler.setFormatter(GerminateFormatter()) | ||
362 | 142 | self.germinate_logger.addHandler(handler) | ||
363 | 143 | self.germinate_logger.propagate = False | ||
364 | 144 | |||
365 | 145 | def setUp(self): | ||
366 | 146 | """Process options, and set up internal state.""" | ||
367 | 147 | self.processOptions() | ||
368 | 148 | self.config = self.getConfig() | ||
369 | 149 | self.setUpDirs() | ||
370 | 150 | self.addLogHandler() | ||
371 | 151 | |||
372 | 152 | def makeSeedStructures(self, series_name, flavours, seed_bases=None): | ||
373 | 153 | structures = {} | ||
374 | 154 | for flavour in flavours: | ||
375 | 155 | structures[flavour] = SeedStructure( | ||
376 | 156 | "%s.%s" % (flavour, series_name), seed_bases=seed_bases) | ||
377 | 157 | return structures | ||
378 | 158 | |||
379 | 159 | def logGerminateProgress(self, *args): | ||
380 | 160 | """Log a "progress" entry to the germinate log file. | ||
381 | 161 | |||
382 | 162 | Germinate logs quite a bit of detailed information. To make it | ||
383 | 163 | easier to see the structure of its operation, GerminateFormatter | ||
384 | 164 | allows tagging some log entries as "progress" entries, which are | ||
385 | 165 | printed without a prefix. | ||
386 | 166 | """ | ||
387 | 167 | self.germinate_logger.info(*args, extra={"progress": True}) | ||
388 | 168 | |||
389 | 169 | def composeOutputPath(self, flavour, series_name, arch, base): | ||
390 | 170 | return os.path.join( | ||
391 | 171 | self.config.germinateroot, | ||
392 | 172 | "%s_%s_%s_%s" % (base, flavour, series_name, arch)) | ||
393 | 173 | |||
394 | 174 | def writeGerminateOutput(self, germinator, structure, flavour, | ||
395 | 175 | series_name, arch): | ||
396 | 176 | """Write dependency-expanded output files. | ||
397 | 177 | |||
398 | 178 | These files are a reduced subset of those written by the germinate | ||
399 | 179 | command-line program. | ||
400 | 180 | """ | ||
401 | 181 | path = partial(self.composeOutputPath, flavour, series_name, arch) | ||
402 | 182 | |||
403 | 183 | # The structure file makes it possible to figure out how the other | ||
404 | 184 | # output files relate to each other. | ||
405 | 185 | structure.write(path("structure")) | ||
406 | 186 | |||
407 | 187 | # "all" and "all.sources" list the full set of binary and source | ||
408 | 188 | # packages respectively for a given flavour/suite/architecture | ||
409 | 189 | # combination. | ||
410 | 190 | germinator.write_all_list(structure, path("all")) | ||
411 | 191 | germinator.write_all_source_list(structure, path("all.sources")) | ||
412 | 192 | |||
413 | 193 | # Write the dependency-expanded output for each seed. Several of | ||
414 | 194 | # these are used by archive administration tools, and others are | ||
415 | 195 | # useful for debugging, so it's best to just write them all. | ||
416 | 196 | for seedname in structure.names: | ||
417 | 197 | germinator.write_full_list(structure, path(seedname), seedname) | ||
418 | 198 | |||
419 | 199 | def parseTaskHeaders(self, seedtext): | ||
420 | 200 | """Parse a seed for Task headers. | ||
421 | 201 | |||
422 | 202 | seedtext is a file-like object. Return a dictionary of Task headers, | ||
423 | 203 | with keys canonicalised to lower-case. | ||
424 | 204 | """ | ||
425 | 205 | task_headers = {} | ||
426 | 206 | task_header_regex = re.compile( | ||
427 | 207 | r"task-(.*?):(.*)", flags=re.IGNORECASE) | ||
428 | 208 | for line in seedtext: | ||
429 | 209 | match = task_header_regex.match(line) | ||
430 | 210 | if match is not None: | ||
431 | 211 | key, value = match.groups() | ||
432 | 212 | task_headers[key.lower()] = value.strip() | ||
433 | 213 | return task_headers | ||
434 | 214 | |||
435 | 215 | def getTaskName(self, task_headers, flavour, seedname, primary_flavour): | ||
436 | 216 | """Work out the name of the Task to be generated from this seed. | ||
437 | 217 | |||
438 | 218 | If there is a Task-Name header, it wins; otherwise, seeds with a | ||
439 | 219 | Task-Per-Derivative header are honoured for all flavours and put in | ||
440 | 220 | an appropriate namespace, while other seeds are only honoured for | ||
441 | 221 | the first flavour and have archive-global names. | ||
442 | 222 | """ | ||
443 | 223 | if "name" in task_headers: | ||
444 | 224 | return task_headers["name"] | ||
445 | 225 | elif "per-derivative" in task_headers: | ||
446 | 226 | return "%s-%s" % (flavour, seedname) | ||
447 | 227 | elif primary_flavour: | ||
448 | 228 | return seedname | ||
449 | 229 | else: | ||
450 | 230 | return None | ||
451 | 231 | |||
452 | 232 | def getTaskSeeds(self, task_headers, seedname): | ||
453 | 233 | """Return the list of seeds used to generate a task from this seed. | ||
454 | 234 | |||
455 | 235 | The list of packages in this task comes from this seed plus any | ||
456 | 236 | other seeds listed in a Task-Seeds header. | ||
457 | 237 | """ | ||
458 | 238 | scan_seeds = set([seedname]) | ||
459 | 239 | if "seeds" in task_headers: | ||
460 | 240 | scan_seeds.update(task_headers["seeds"].split()) | ||
461 | 241 | return sorted(scan_seeds) | ||
462 | 242 | |||
463 | 243 | def writeOverrides(self, override_file, germinator, structure, arch, | ||
464 | 244 | seedname, key, value): | ||
465 | 245 | packages = germinator.get_full(structure, seedname) | ||
466 | 246 | for package in sorted(packages): | ||
467 | 247 | print >>override_file, "%s/%s %s %s" % ( | ||
468 | 248 | package, arch, key, value) | ||
469 | 249 | |||
470 | 250 | def germinateArchFlavour(self, override_file, germinator, series_name, | ||
471 | 251 | arch, flavour, structure, primary_flavour): | ||
472 | 252 | """Germinate seeds on a single flavour for a single architecture.""" | ||
473 | 253 | # Expand dependencies. | ||
474 | 254 | germinator.plant_seeds(structure) | ||
475 | 255 | germinator.grow(structure) | ||
476 | 256 | germinator.add_extras(structure) | ||
477 | 257 | |||
478 | 258 | self.writeGerminateOutput(germinator, structure, flavour, series_name, | ||
479 | 259 | arch) | ||
480 | 260 | |||
481 | 261 | write_overrides = partial( | ||
482 | 262 | self.writeOverrides, override_file, germinator, structure, arch) | ||
483 | 263 | |||
484 | 264 | # Generate apt-ftparchive "extra overrides" for Task fields. | ||
485 | 265 | seednames = [name for name in structure.names if name != "extra"] | ||
486 | 266 | for seedname in seednames: | ||
487 | 267 | with structure[seedname] as seedtext: | ||
488 | 268 | task_headers = self.parseTaskHeaders(seedtext) | ||
489 | 269 | if task_headers: | ||
490 | 270 | task = self.getTaskName( | ||
491 | 271 | task_headers, flavour, seedname, primary_flavour) | ||
492 | 272 | if task is not None: | ||
493 | 273 | scan_seeds = self.getTaskSeeds(task_headers, seedname) | ||
494 | 274 | for scan_seed in scan_seeds: | ||
495 | 275 | write_overrides(scan_seed, "Task", task) | ||
496 | 276 | |||
497 | 277 | # Generate apt-ftparchive "extra overrides" for Build-Essential | ||
498 | 278 | # fields. | ||
499 | 279 | if "build-essential" in structure.names and primary_flavour: | ||
500 | 280 | write_overrides("build-essential", "Build-Essential", "yes") | ||
501 | 281 | |||
502 | 282 | def germinateArch(self, override_file, series_name, arch, flavours, | ||
503 | 283 | structures): | ||
504 | 284 | """Germinate seeds on all flavours for a single architecture.""" | ||
505 | 285 | germinator = Germinator(arch) | ||
506 | 286 | |||
507 | 287 | # Read archive metadata. | ||
508 | 288 | archive = TagFile( | ||
509 | 289 | series_name, self.components, arch, | ||
510 | 290 | "file://%s" % self.config.archiveroot, cleanup=True) | ||
511 | 291 | germinator.parse_archive(archive) | ||
512 | 292 | |||
513 | 293 | for flavour in flavours: | ||
514 | 294 | self.logger.info( | ||
515 | 295 | "Germinating for %s/%s/%s", flavour, series_name, arch) | ||
516 | 296 | # Add this to the germinate log as well so that that can be | ||
517 | 297 | # debugged more easily. Log a separator line first. | ||
518 | 298 | self.logGerminateProgress("") | ||
519 | 299 | self.logGerminateProgress( | ||
520 | 300 | "Germinating for %s/%s/%s", flavour, series_name, arch) | ||
521 | 301 | |||
522 | 302 | self.germinateArchFlavour( | ||
523 | 303 | override_file, germinator, series_name, arch, flavour, | ||
524 | 304 | structures[flavour], flavour == flavours[0]) | ||
525 | 305 | |||
526 | 306 | def generateExtraOverrides(self, series_name, series_architectures, | ||
527 | 307 | flavours, seed_bases=None): | ||
528 | 308 | structures = self.makeSeedStructures( | ||
529 | 309 | series_name, flavours, seed_bases=seed_bases) | ||
530 | 310 | |||
531 | 311 | override_path = os.path.join( | ||
532 | 312 | self.config.miscroot, | ||
533 | 313 | "more-extra.override.%s.main" % series_name) | ||
534 | 314 | with AtomicFile(override_path) as override_file: | ||
535 | 315 | for arch in series_architectures: | ||
536 | 316 | self.germinateArch( | ||
537 | 317 | override_file, series_name, arch, flavours, structures) | ||
538 | 318 | |||
539 | 319 | def process(self, seed_bases=None): | ||
540 | 320 | """Do the bulk of the work.""" | ||
541 | 321 | self.setUp() | ||
542 | 322 | |||
543 | 323 | series_name = self.series.name | ||
544 | 324 | series_architectures = sorted( | ||
545 | 325 | [arch.architecturetag for arch in self.series.architectures]) | ||
546 | 326 | |||
547 | 327 | # This takes a while. Ensure that we do it without keeping a | ||
548 | 328 | # database transaction open. | ||
549 | 329 | self.txn.commit() | ||
550 | 330 | with DatabaseBlockedPolicy(): | ||
551 | 331 | self.generateExtraOverrides( | ||
552 | 332 | series_name, series_architectures, self.args, | ||
553 | 333 | seed_bases=seed_bases) | ||
554 | 334 | |||
555 | 335 | def main(self): | ||
556 | 336 | """See `LaunchpadScript`.""" | ||
557 | 337 | # This code has no need to alter the database. | ||
558 | 338 | with SlaveOnlyDatabasePolicy(): | ||
559 | 339 | self.process() | ||
560 | 0 | 340 | ||
561 | === modified file 'lib/lp/archivepublisher/tests/publisher-config.txt' | |||
562 | --- lib/lp/archivepublisher/tests/publisher-config.txt 2010-10-17 13:35:20 +0000 | |||
563 | +++ lib/lp/archivepublisher/tests/publisher-config.txt 2011-12-14 15:49:40 +0000 | |||
564 | @@ -14,6 +14,7 @@ | |||
565 | 14 | ... 'overrideroot', | 14 | ... 'overrideroot', |
566 | 15 | ... 'cacheroot', | 15 | ... 'cacheroot', |
567 | 16 | ... 'miscroot', | 16 | ... 'miscroot', |
568 | 17 | ... 'germinateroot', | ||
569 | 17 | ... 'temproot', | 18 | ... 'temproot', |
570 | 18 | ... ] | 19 | ... ] |
571 | 19 | 20 | ||
572 | @@ -38,6 +39,7 @@ | |||
573 | 38 | overrideroot: /var/tmp/archive/ubuntutest-overrides | 39 | overrideroot: /var/tmp/archive/ubuntutest-overrides |
574 | 39 | cacheroot: /var/tmp/archive/ubuntutest-cache | 40 | cacheroot: /var/tmp/archive/ubuntutest-cache |
575 | 40 | miscroot: /var/tmp/archive/ubuntutest-misc | 41 | miscroot: /var/tmp/archive/ubuntutest-misc |
576 | 42 | germinateroot: /var/tmp/archive/ubuntutest-germinate | ||
577 | 41 | temproot: /var/tmp/archive/ubuntutest-temp | 43 | temproot: /var/tmp/archive/ubuntutest-temp |
578 | 42 | 44 | ||
579 | 43 | 45 | ||
580 | @@ -80,6 +82,7 @@ | |||
581 | 80 | overrideroot: None | 82 | overrideroot: None |
582 | 81 | cacheroot: None | 83 | cacheroot: None |
583 | 82 | miscroot: None | 84 | miscroot: None |
584 | 85 | germinateroot: None | ||
585 | 83 | temproot: /var/tmp/archive/ubuntutest-temp | 86 | temproot: /var/tmp/archive/ubuntutest-temp |
586 | 84 | 87 | ||
587 | 85 | There is a separate location for private PPAs that is used if the | 88 | There is a separate location for private PPAs that is used if the |
588 | @@ -108,6 +111,7 @@ | |||
589 | 108 | overrideroot: None | 111 | overrideroot: None |
590 | 109 | cacheroot: None | 112 | cacheroot: None |
591 | 110 | miscroot: None | 113 | miscroot: None |
592 | 114 | germinateroot: None | ||
593 | 111 | temproot: /var/tmp/archive/ubuntutest-temp | 115 | temproot: /var/tmp/archive/ubuntutest-temp |
594 | 112 | 116 | ||
595 | 113 | 117 | ||
596 | @@ -131,6 +135,7 @@ | |||
597 | 131 | overrideroot: None | 135 | overrideroot: None |
598 | 132 | cacheroot: None | 136 | cacheroot: None |
599 | 133 | miscroot: None | 137 | miscroot: None |
600 | 138 | germinateroot: None | ||
601 | 134 | temproot: /var/tmp/archive/ubuntutest-temp | 139 | temproot: /var/tmp/archive/ubuntutest-temp |
602 | 135 | 140 | ||
603 | 136 | 141 | ||
604 | @@ -155,6 +160,7 @@ | |||
605 | 155 | overrideroot: None | 160 | overrideroot: None |
606 | 156 | cacheroot: None | 161 | cacheroot: None |
607 | 157 | miscroot: None | 162 | miscroot: None |
608 | 163 | germinateroot: None | ||
609 | 158 | temproot: /var/tmp/archive/ubuntutest-temp | 164 | temproot: /var/tmp/archive/ubuntutest-temp |
610 | 159 | 165 | ||
611 | 160 | 166 | ||
612 | @@ -177,4 +183,5 @@ | |||
613 | 177 | overrideroot: /var/tmp/archive/ubuntutest-rebuildtest99/ubuntutest-overrides | 183 | overrideroot: /var/tmp/archive/ubuntutest-rebuildtest99/ubuntutest-overrides |
614 | 178 | cacheroot: /var/tmp/archive/ubuntutest-rebuildtest99/ubuntutest-cache | 184 | cacheroot: /var/tmp/archive/ubuntutest-rebuildtest99/ubuntutest-cache |
615 | 179 | miscroot: /var/tmp/archive/ubuntutest-rebuildtest99/ubuntutest-misc | 185 | miscroot: /var/tmp/archive/ubuntutest-rebuildtest99/ubuntutest-misc |
616 | 186 | germinateroot: /var/tmp/archive/ubuntutest-rebuildtest99/ubuntutest-germinate | ||
617 | 180 | temproot: /var/tmp/archive/ubuntutest-rebuildtest99/ubuntutest-temp | 187 | temproot: /var/tmp/archive/ubuntutest-rebuildtest99/ubuntutest-temp |
618 | 181 | 188 | ||
619 | === added file 'lib/lp/archivepublisher/tests/test_generate_extra_overrides.py' | |||
620 | --- lib/lp/archivepublisher/tests/test_generate_extra_overrides.py 1970-01-01 00:00:00 +0000 | |||
621 | +++ lib/lp/archivepublisher/tests/test_generate_extra_overrides.py 2011-12-14 15:49:40 +0000 | |||
622 | @@ -0,0 +1,567 @@ | |||
623 | 1 | # Copyright 2011 Canonical Ltd. This software is licensed under the | ||
624 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
625 | 3 | |||
626 | 4 | """Test for the `generate-extra-overrides` script.""" | ||
627 | 5 | |||
628 | 6 | __metaclass__ = type | ||
629 | 7 | |||
630 | 8 | import logging | ||
631 | 9 | from optparse import OptionValueError | ||
632 | 10 | import os | ||
633 | 11 | import tempfile | ||
634 | 12 | |||
635 | 13 | from germinate import ( | ||
636 | 14 | archive, | ||
637 | 15 | germinator, | ||
638 | 16 | seeds, | ||
639 | 17 | ) | ||
640 | 18 | |||
641 | 19 | import transaction | ||
642 | 20 | |||
643 | 21 | from canonical.testing.layers import ( | ||
644 | 22 | LaunchpadZopelessLayer, | ||
645 | 23 | ZopelessDatabaseLayer, | ||
646 | 24 | ) | ||
647 | 25 | from lp.archivepublisher.scripts.generate_extra_overrides import ( | ||
648 | 26 | AtomicFile, | ||
649 | 27 | GenerateExtraOverrides, | ||
650 | 28 | ) | ||
651 | 29 | from lp.archivepublisher.utils import RepositoryIndexFile | ||
652 | 30 | from lp.registry.interfaces.pocket import PackagePublishingPocket | ||
653 | 31 | from lp.registry.interfaces.series import SeriesStatus | ||
654 | 32 | from lp.services.log.logger import DevNullLogger | ||
655 | 33 | from lp.services.osutils import ( | ||
656 | 34 | ensure_directory_exists, | ||
657 | 35 | open_for_writing, | ||
658 | 36 | ) | ||
659 | 37 | from lp.services.scripts.base import LaunchpadScriptFailure | ||
660 | 38 | from lp.services.utils import file_exists | ||
661 | 39 | from lp.soyuz.enums import PackagePublishingStatus | ||
662 | 40 | from lp.testing import TestCaseWithFactory | ||
663 | 41 | from lp.testing.faketransaction import FakeTransaction | ||
664 | 42 | |||
665 | 43 | |||
666 | 44 | def file_contents(path): | ||
667 | 45 | """Return the contents of the file at path.""" | ||
668 | 46 | with open(path) as handle: | ||
669 | 47 | return handle.read() | ||
670 | 48 | |||
671 | 49 | |||
672 | 50 | class TestAtomicFile(TestCaseWithFactory): | ||
673 | 51 | """Tests for the AtomicFile helper class.""" | ||
674 | 52 | |||
675 | 53 | layer = ZopelessDatabaseLayer | ||
676 | 54 | |||
677 | 55 | def test_atomic_file_creates_file(self): | ||
678 | 56 | # AtomicFile creates the named file with the requested contents. | ||
679 | 57 | self.useTempDir() | ||
680 | 58 | filename = self.factory.getUniqueString() | ||
681 | 59 | text = self.factory.getUniqueString() | ||
682 | 60 | with AtomicFile(filename) as test: | ||
683 | 61 | test.write(text) | ||
684 | 62 | self.assertEqual(text, file_contents(filename)) | ||
685 | 63 | |||
686 | 64 | def test_atomic_file_removes_dot_new(self): | ||
687 | 65 | # AtomicFile does not leave .new files lying around. | ||
688 | 66 | self.useTempDir() | ||
689 | 67 | filename = self.factory.getUniqueString() | ||
690 | 68 | with AtomicFile(filename): | ||
691 | 69 | pass | ||
692 | 70 | self.assertFalse(file_exists("%s.new" % filename)) | ||
693 | 71 | |||
694 | 72 | |||
695 | 73 | class TestGenerateExtraOverrides(TestCaseWithFactory): | ||
696 | 74 | """Tests for the actual `GenerateExtraOverrides` script.""" | ||
697 | 75 | |||
698 | 76 | layer = LaunchpadZopelessLayer | ||
699 | 77 | |||
700 | 78 | def setUp(self): | ||
701 | 79 | super(TestGenerateExtraOverrides, self).setUp() | ||
702 | 80 | self.seeddir = self.makeTemporaryDirectory() | ||
703 | 81 | # XXX cjwatson 2011-12-06 bug=694140: Make sure germinate doesn't | ||
704 | 82 | # lose its loggers between tests, due to Launchpad's messing with | ||
705 | 83 | # global log state. | ||
706 | 84 | archive._logger = logging.getLogger("germinate.archive") | ||
707 | 85 | germinator._logger = logging.getLogger("germinate.germinator") | ||
708 | 86 | seeds._logger = logging.getLogger("germinate.seeds") | ||
709 | 87 | |||
710 | 88 | def assertFilesEqual(self, expected_path, observed_path): | ||
711 | 89 | self.assertEqual( | ||
712 | 90 | file_contents(expected_path), file_contents(observed_path)) | ||
713 | 91 | |||
714 | 92 | def makeDistro(self): | ||
715 | 93 | """Create a distribution for testing. | ||
716 | 94 | |||
717 | 95 | The distribution will have a root directory set up, which will | ||
718 | 96 | be cleaned up after the test. It will have an attached archive. | ||
719 | 97 | """ | ||
720 | 98 | return self.factory.makeDistribution( | ||
721 | 99 | publish_root_dir=unicode(self.makeTemporaryDirectory())) | ||
722 | 100 | |||
723 | 101 | def makeScript(self, distribution, run_setup=True, extra_args=None): | ||
724 | 102 | """Create a script for testing.""" | ||
725 | 103 | test_args = [] | ||
726 | 104 | if distribution is not None: | ||
727 | 105 | test_args.extend(["-d", distribution.name]) | ||
728 | 106 | if extra_args is not None: | ||
729 | 107 | test_args.extend(extra_args) | ||
730 | 108 | script = GenerateExtraOverrides(test_args=test_args) | ||
731 | 109 | script.logger = DevNullLogger() | ||
732 | 110 | script.txn = FakeTransaction() | ||
733 | 111 | if distribution is not None and run_setup: | ||
734 | 112 | script.setUp() | ||
735 | 113 | else: | ||
736 | 114 | script.distribution = distribution | ||
737 | 115 | return script | ||
738 | 116 | |||
739 | 117 | def makePackage(self, component, dases, **kwargs): | ||
740 | 118 | """Create a published source and binary package for testing.""" | ||
741 | 119 | package = self.factory.makeDistributionSourcePackage( | ||
742 | 120 | distribution=dases[0].distroseries.distribution) | ||
743 | 121 | spph = self.factory.makeSourcePackagePublishingHistory( | ||
744 | 122 | distroseries=dases[0].distroseries, | ||
745 | 123 | pocket=PackagePublishingPocket.RELEASE, | ||
746 | 124 | status=PackagePublishingStatus.PUBLISHED, | ||
747 | 125 | sourcepackagename=package.name, component=component) | ||
748 | 126 | for das in dases: | ||
749 | 127 | build = self.factory.makeBinaryPackageBuild( | ||
750 | 128 | source_package_release=spph.sourcepackagerelease, | ||
751 | 129 | distroarchseries=das, processor=das.default_processor) | ||
752 | 130 | bpr = self.factory.makeBinaryPackageRelease( | ||
753 | 131 | binarypackagename=package.name, build=build, | ||
754 | 132 | component=component, **kwargs) | ||
755 | 133 | lfa = self.factory.makeLibraryFileAlias( | ||
756 | 134 | filename="%s.deb" % package.name) | ||
757 | 135 | transaction.commit() | ||
758 | 136 | bpr.addFile(lfa) | ||
759 | 137 | self.factory.makeBinaryPackagePublishingHistory( | ||
760 | 138 | binarypackagerelease=bpr, distroarchseries=das, | ||
761 | 139 | pocket=PackagePublishingPocket.RELEASE, | ||
762 | 140 | status=PackagePublishingStatus.PUBLISHED) | ||
763 | 141 | return package | ||
764 | 142 | |||
765 | 143 | def makeIndexFiles(self, script, distroseries): | ||
766 | 144 | """Create a limited subset of index files for testing.""" | ||
767 | 145 | ensure_directory_exists(script.config.temproot) | ||
768 | 146 | |||
769 | 147 | for component in distroseries.components: | ||
770 | 148 | index_root = os.path.join( | ||
771 | 149 | script.config.distsroot, distroseries.name, component.name) | ||
772 | 150 | |||
773 | 151 | source_index_root = os.path.join(index_root, "source") | ||
774 | 152 | source_index = RepositoryIndexFile( | ||
775 | 153 | source_index_root, script.config.temproot, "Sources") | ||
776 | 154 | for spp in distroseries.getSourcePackagePublishing( | ||
777 | 155 | PackagePublishingStatus.PUBLISHED, | ||
778 | 156 | PackagePublishingPocket.RELEASE, component=component): | ||
779 | 157 | stanza = spp.getIndexStanza().encode("utf-8") + "\n\n" | ||
780 | 158 | source_index.write(stanza) | ||
781 | 159 | source_index.close() | ||
782 | 160 | |||
783 | 161 | for arch in distroseries.architectures: | ||
784 | 162 | package_index_root = os.path.join( | ||
785 | 163 | index_root, "binary-%s" % arch.architecturetag) | ||
786 | 164 | package_index = RepositoryIndexFile( | ||
787 | 165 | package_index_root, script.config.temproot, "Packages") | ||
788 | 166 | for bpp in distroseries.getBinaryPackagePublishing( | ||
789 | 167 | archtag=arch.architecturetag, | ||
790 | 168 | pocket=PackagePublishingPocket.RELEASE, | ||
791 | 169 | component=component): | ||
792 | 170 | stanza = bpp.getIndexStanza().encode("utf-8") + "\n\n" | ||
793 | 171 | package_index.write(stanza) | ||
794 | 172 | package_index.close() | ||
795 | 173 | |||
796 | 174 | def composeSeedPath(self, flavour, series_name, seed_name): | ||
797 | 175 | return os.path.join( | ||
798 | 176 | self.seeddir, "%s.%s" % (flavour, series_name), seed_name) | ||
799 | 177 | |||
800 | 178 | def makeSeedStructure(self, flavour, series_name, seed_names, | ||
801 | 179 | seed_inherit=None): | ||
802 | 180 | """Create a simple seed structure file.""" | ||
803 | 181 | if seed_inherit is None: | ||
804 | 182 | seed_inherit = {} | ||
805 | 183 | |||
806 | 184 | structure_path = self.composeSeedPath( | ||
807 | 185 | flavour, series_name, "STRUCTURE") | ||
808 | 186 | with open_for_writing(structure_path, "w") as structure: | ||
809 | 187 | for seed_name in seed_names: | ||
810 | 188 | inherit = seed_inherit.get(seed_name, []) | ||
811 | 189 | line = "%s: %s" % (seed_name, " ".join(inherit)) | ||
812 | 190 | print >>structure, line.strip() | ||
813 | 191 | |||
814 | 192 | def makeSeed(self, flavour, series_name, seed_name, entries, | ||
815 | 193 | headers=None): | ||
816 | 194 | """Create a simple seed file.""" | ||
817 | 195 | seed_path = self.composeSeedPath(flavour, series_name, seed_name) | ||
818 | 196 | with open_for_writing(seed_path, "w") as seed: | ||
819 | 197 | if headers is not None: | ||
820 | 198 | for header in headers: | ||
821 | 199 | print >>seed, header | ||
822 | 200 | print >>seed | ||
823 | 201 | for entry in entries: | ||
824 | 202 | print >>seed, " * %s" % entry | ||
825 | 203 | |||
826 | 204 | def getTaskNameFromSeed(self, script, flavour, series_name, seed, | ||
827 | 205 | primary_flavour): | ||
828 | 206 | """Use script to parse a seed and return its task name.""" | ||
829 | 207 | seed_path = self.composeSeedPath(flavour, series_name, seed) | ||
830 | 208 | with open(seed_path) as seed_text: | ||
831 | 209 | task_headers = script.parseTaskHeaders(seed_text) | ||
832 | 210 | return script.getTaskName( | ||
833 | 211 | task_headers, flavour, seed, primary_flavour) | ||
834 | 212 | |||
835 | 213 | def getTaskSeedsFromSeed(self, script, flavour, series_name, seed): | ||
836 | 214 | """Use script to parse a seed and return its task seed list.""" | ||
837 | 215 | seed_path = self.composeSeedPath(flavour, series_name, seed) | ||
838 | 216 | with open(seed_path) as seed_text: | ||
839 | 217 | task_headers = script.parseTaskHeaders(seed_text) | ||
840 | 218 | return script.getTaskSeeds(task_headers, seed) | ||
841 | 219 | |||
842 | 220 | def test_name_is_consistent(self): | ||
843 | 221 | # Script instances for the same distro get the same name. | ||
844 | 222 | distro = self.factory.makeDistribution() | ||
845 | 223 | self.assertEqual( | ||
846 | 224 | GenerateExtraOverrides(test_args=["-d", distro.name]).name, | ||
847 | 225 | GenerateExtraOverrides(test_args=["-d", distro.name]).name) | ||
848 | 226 | |||
849 | 227 | def test_name_is_unique_for_each_distro(self): | ||
850 | 228 | # Script instances for different distros get different names. | ||
851 | 229 | self.assertNotEqual( | ||
852 | 230 | GenerateExtraOverrides( | ||
853 | 231 | test_args=["-d", self.factory.makeDistribution().name]).name, | ||
854 | 232 | GenerateExtraOverrides( | ||
855 | 233 | test_args=["-d", self.factory.makeDistribution().name]).name) | ||
856 | 234 | |||
857 | 235 | def test_requires_distro(self): | ||
858 | 236 | # The --distribution or -d argument is mandatory. | ||
859 | 237 | script = self.makeScript(None) | ||
860 | 238 | self.assertRaises(OptionValueError, script.processOptions) | ||
861 | 239 | |||
862 | 240 | def test_requires_real_distro(self): | ||
863 | 241 | # An incorrect distribution name is flagged as an invalid option | ||
864 | 242 | # value. | ||
865 | 243 | script = self.makeScript( | ||
866 | 244 | None, extra_args=["-d", self.factory.getUniqueString()]) | ||
867 | 245 | self.assertRaises(OptionValueError, script.processOptions) | ||
868 | 246 | |||
869 | 247 | def test_looks_up_distro(self): | ||
870 | 248 | # The script looks up and keeps the distribution named on the | ||
871 | 249 | # command line. | ||
872 | 250 | distro = self.makeDistro() | ||
873 | 251 | self.factory.makeDistroSeries(distro) | ||
874 | 252 | script = self.makeScript(distro) | ||
875 | 253 | self.assertEqual(distro, script.distribution) | ||
876 | 254 | |||
877 | 255 | def test_prefers_development_distro_series(self): | ||
878 | 256 | # The script prefers a DEVELOPMENT series for the named | ||
879 | 257 | # distribution over CURRENT and SUPPORTED series. | ||
880 | 258 | distro = self.makeDistro() | ||
881 | 259 | self.factory.makeDistroSeries(distro, status=SeriesStatus.SUPPORTED) | ||
882 | 260 | self.factory.makeDistroSeries(distro, status=SeriesStatus.CURRENT) | ||
883 | 261 | development_distroseries = self.factory.makeDistroSeries( | ||
884 | 262 | distro, status=SeriesStatus.DEVELOPMENT) | ||
885 | 263 | script = self.makeScript(distro) | ||
886 | 264 | self.assertEqual(development_distroseries, script.series) | ||
887 | 265 | |||
888 | 266 | def test_permits_frozen_distro_series(self): | ||
889 | 267 | # If there is no DEVELOPMENT series, a FROZEN one will do. | ||
890 | 268 | distro = self.makeDistro() | ||
891 | 269 | self.factory.makeDistroSeries(distro, status=SeriesStatus.SUPPORTED) | ||
892 | 270 | self.factory.makeDistroSeries(distro, status=SeriesStatus.CURRENT) | ||
893 | 271 | frozen_distroseries = self.factory.makeDistroSeries( | ||
894 | 272 | distro, status=SeriesStatus.FROZEN) | ||
895 | 273 | script = self.makeScript(distro) | ||
896 | 274 | self.assertEqual(frozen_distroseries, script.series) | ||
897 | 275 | |||
898 | 276 | def test_requires_development_frozen_distro_series(self): | ||
899 | 277 | # If there is no DEVELOPMENT or FROZEN series, the script fails. | ||
900 | 278 | distro = self.makeDistro() | ||
901 | 279 | self.factory.makeDistroSeries(distro, status=SeriesStatus.SUPPORTED) | ||
902 | 280 | self.factory.makeDistroSeries(distro, status=SeriesStatus.CURRENT) | ||
903 | 281 | script = self.makeScript(distro, run_setup=False) | ||
904 | 282 | self.assertRaises(LaunchpadScriptFailure, script.processOptions) | ||
905 | 283 | |||
906 | 284 | def test_components_exclude_partner(self): | ||
907 | 285 | # If a 'partner' component exists, it is excluded. | ||
908 | 286 | distro = self.makeDistro() | ||
909 | 287 | distroseries = self.factory.makeDistroSeries(distro) | ||
910 | 288 | self.factory.makeComponentSelection( | ||
911 | 289 | distroseries=distroseries, component="main") | ||
912 | 290 | self.factory.makeComponentSelection( | ||
913 | 291 | distroseries=distroseries, component="partner") | ||
914 | 292 | script = self.makeScript(distro) | ||
915 | 293 | self.assertEqual(["main"], script.components) | ||
916 | 294 | |||
917 | 295 | def test_compose_output_path_in_germinateroot(self): | ||
918 | 296 | # Output files are written to the correct locations under | ||
919 | 297 | # germinateroot. | ||
920 | 298 | distro = self.makeDistro() | ||
921 | 299 | distroseries = self.factory.makeDistroSeries(distro) | ||
922 | 300 | script = self.makeScript(distro) | ||
923 | 301 | flavour = self.factory.getUniqueString() | ||
924 | 302 | arch = self.factory.getUniqueString() | ||
925 | 303 | base = self.factory.getUniqueString() | ||
926 | 304 | output = script.composeOutputPath( | ||
927 | 305 | flavour, distroseries.name, arch, base) | ||
928 | 306 | self.assertEqual( | ||
929 | 307 | "%s/%s_%s_%s_%s" % ( | ||
930 | 308 | script.config.germinateroot, base, flavour, distroseries.name, | ||
931 | 309 | arch), | ||
932 | 310 | output) | ||
933 | 311 | |||
934 | 312 | def fetchGerminatedOverrides(self, script, series_name, arch, flavours): | ||
935 | 313 | """Helper to call script.germinateArch and return overrides.""" | ||
936 | 314 | structures = script.makeSeedStructures( | ||
937 | 315 | series_name, flavours, seed_bases=["file://%s" % self.seeddir]) | ||
938 | 316 | |||
939 | 317 | override_fd, override_path = tempfile.mkstemp() | ||
940 | 318 | with os.fdopen(override_fd, "w") as override_file: | ||
941 | 319 | script.germinateArch( | ||
942 | 320 | override_file, series_name, arch, flavours, structures) | ||
943 | 321 | return file_contents(override_path).splitlines() | ||
944 | 322 | |||
945 | 323 | def test_germinate_output(self): | ||
946 | 324 | # A single call to germinateArch produces output for all flavours on | ||
947 | 325 | # one architecture. | ||
948 | 326 | distro = self.makeDistro() | ||
949 | 327 | distroseries = self.factory.makeDistroSeries(distribution=distro) | ||
950 | 328 | series_name = distroseries.name | ||
951 | 329 | component = self.factory.makeComponent() | ||
952 | 330 | self.factory.makeComponentSelection( | ||
953 | 331 | distroseries=distroseries, component=component) | ||
954 | 332 | das = self.factory.makeDistroArchSeries(distroseries=distroseries) | ||
955 | 333 | arch = das.architecturetag | ||
956 | 334 | one = self.makePackage(component, [das]) | ||
957 | 335 | two = self.makePackage(component, [das]) | ||
958 | 336 | script = self.makeScript(distro) | ||
959 | 337 | self.makeIndexFiles(script, distroseries) | ||
960 | 338 | |||
961 | 339 | flavour_one = self.factory.getUniqueString() | ||
962 | 340 | flavour_two = self.factory.getUniqueString() | ||
963 | 341 | seed = self.factory.getUniqueString() | ||
964 | 342 | self.makeSeedStructure(flavour_one, series_name, [seed]) | ||
965 | 343 | self.makeSeed(flavour_one, series_name, seed, [one.name]) | ||
966 | 344 | self.makeSeedStructure(flavour_two, series_name, [seed]) | ||
967 | 345 | self.makeSeed(flavour_two, series_name, seed, [two.name]) | ||
968 | 346 | |||
969 | 347 | overrides = self.fetchGerminatedOverrides( | ||
970 | 348 | script, series_name, arch, [flavour_one, flavour_two]) | ||
971 | 349 | self.assertEqual([], overrides) | ||
972 | 350 | |||
973 | 351 | seed_dir_one = os.path.join( | ||
974 | 352 | self.seeddir, "%s.%s" % (flavour_one, series_name)) | ||
975 | 353 | self.assertFilesEqual( | ||
976 | 354 | os.path.join(seed_dir_one, "STRUCTURE"), | ||
977 | 355 | script.composeOutputPath( | ||
978 | 356 | flavour_one, series_name, arch, "structure")) | ||
979 | 357 | self.assertTrue(file_exists(script.composeOutputPath( | ||
980 | 358 | flavour_one, series_name, arch, "all"))) | ||
981 | 359 | self.assertTrue(file_exists(script.composeOutputPath( | ||
982 | 360 | flavour_one, series_name, arch, "all.sources"))) | ||
983 | 361 | self.assertTrue(file_exists(script.composeOutputPath( | ||
984 | 362 | flavour_one, series_name, arch, seed))) | ||
985 | 363 | |||
986 | 364 | seed_dir_two = os.path.join( | ||
987 | 365 | self.seeddir, "%s.%s" % (flavour_two, series_name)) | ||
988 | 366 | self.assertFilesEqual( | ||
989 | 367 | os.path.join(seed_dir_two, "STRUCTURE"), | ||
990 | 368 | script.composeOutputPath( | ||
991 | 369 | flavour_two, series_name, arch, "structure")) | ||
992 | 370 | self.assertTrue(file_exists(script.composeOutputPath( | ||
993 | 371 | flavour_two, series_name, arch, "all"))) | ||
994 | 372 | self.assertTrue(file_exists(script.composeOutputPath( | ||
995 | 373 | flavour_two, series_name, arch, "all.sources"))) | ||
996 | 374 | self.assertTrue(file_exists(script.composeOutputPath( | ||
997 | 375 | flavour_two, series_name, arch, seed))) | ||
998 | 376 | |||
999 | 377 | def test_germinate_output_task(self): | ||
1000 | 378 | # germinateArch produces Task extra overrides. | ||
1001 | 379 | distro = self.makeDistro() | ||
1002 | 380 | distroseries = self.factory.makeDistroSeries(distribution=distro) | ||
1003 | 381 | series_name = distroseries.name | ||
1004 | 382 | component = self.factory.makeComponent() | ||
1005 | 383 | self.factory.makeComponentSelection( | ||
1006 | 384 | distroseries=distroseries, component=component) | ||
1007 | 385 | das = self.factory.makeDistroArchSeries(distroseries=distroseries) | ||
1008 | 386 | arch = das.architecturetag | ||
1009 | 387 | one = self.makePackage(component, [das]) | ||
1010 | 388 | two = self.makePackage(component, [das], depends=one.name) | ||
1011 | 389 | three = self.makePackage(component, [das]) | ||
1012 | 390 | self.makePackage(component, [das]) | ||
1013 | 391 | script = self.makeScript(distro) | ||
1014 | 392 | self.makeIndexFiles(script, distroseries) | ||
1015 | 393 | |||
1016 | 394 | flavour = self.factory.getUniqueString() | ||
1017 | 395 | seed_one = self.factory.getUniqueString() | ||
1018 | 396 | seed_two = self.factory.getUniqueString() | ||
1019 | 397 | self.makeSeedStructure(flavour, series_name, [seed_one, seed_two]) | ||
1020 | 398 | self.makeSeed( | ||
1021 | 399 | flavour, series_name, seed_one, [two.name], | ||
1022 | 400 | headers=["Task-Description: one"]) | ||
1023 | 401 | self.makeSeed( | ||
1024 | 402 | flavour, series_name, seed_two, [three.name], | ||
1025 | 403 | headers=["Task-Description: two"]) | ||
1026 | 404 | |||
1027 | 405 | overrides = self.fetchGerminatedOverrides( | ||
1028 | 406 | script, series_name, arch, [flavour]) | ||
1029 | 407 | expected_overrides = [ | ||
1030 | 408 | "%s/%s Task %s" % (one.name, arch, seed_one), | ||
1031 | 409 | "%s/%s Task %s" % (two.name, arch, seed_one), | ||
1032 | 410 | "%s/%s Task %s" % (three.name, arch, seed_two), | ||
1033 | 411 | ] | ||
1034 | 412 | self.assertContentEqual(expected_overrides, overrides) | ||
1035 | 413 | |||
1036 | 414 | def test_task_name(self): | ||
1037 | 415 | # The Task-Name field is honoured. | ||
1038 | 416 | series_name = self.factory.getUniqueString() | ||
1039 | 417 | package = self.factory.getUniqueString() | ||
1040 | 418 | script = self.makeScript(None) | ||
1041 | 419 | |||
1042 | 420 | flavour = self.factory.getUniqueString() | ||
1043 | 421 | seed = self.factory.getUniqueString() | ||
1044 | 422 | task = self.factory.getUniqueString() | ||
1045 | 423 | self.makeSeed( | ||
1046 | 424 | flavour, series_name, seed, [package], | ||
1047 | 425 | headers=["Task-Name: %s" % task]) | ||
1048 | 426 | |||
1049 | 427 | observed_task = self.getTaskNameFromSeed( | ||
1050 | 428 | script, flavour, series_name, seed, True) | ||
1051 | 429 | self.assertEqual(task, observed_task) | ||
1052 | 430 | |||
1053 | 431 | def test_task_per_derivative(self): | ||
1054 | 432 | # The Task-Per-Derivative field is honoured. | ||
1055 | 433 | series_name = self.factory.getUniqueString() | ||
1056 | 434 | package = self.factory.getUniqueString() | ||
1057 | 435 | script = self.makeScript(None) | ||
1058 | 436 | |||
1059 | 437 | flavour_one = self.factory.getUniqueString() | ||
1060 | 438 | flavour_two = self.factory.getUniqueString() | ||
1061 | 439 | seed_one = self.factory.getUniqueString() | ||
1062 | 440 | seed_two = self.factory.getUniqueString() | ||
1063 | 441 | self.makeSeed( | ||
1064 | 442 | flavour_one, series_name, seed_one, [package], | ||
1065 | 443 | headers=["Task-Description: one"]) | ||
1066 | 444 | self.makeSeed( | ||
1067 | 445 | flavour_one, series_name, seed_two, [package], | ||
1068 | 446 | headers=["Task-Per-Derivative: 1"]) | ||
1069 | 447 | self.makeSeed( | ||
1070 | 448 | flavour_two, series_name, seed_one, [package], | ||
1071 | 449 | headers=["Task-Description: one"]) | ||
1072 | 450 | self.makeSeed( | ||
1073 | 451 | flavour_two, series_name, seed_two, [package], | ||
1074 | 452 | headers=["Task-Per-Derivative: 1"]) | ||
1075 | 453 | |||
1076 | 454 | observed_task_one_one = self.getTaskNameFromSeed( | ||
1077 | 455 | script, flavour_one, series_name, seed_one, True) | ||
1078 | 456 | observed_task_one_two = self.getTaskNameFromSeed( | ||
1079 | 457 | script, flavour_one, series_name, seed_two, True) | ||
1080 | 458 | observed_task_two_one = self.getTaskNameFromSeed( | ||
1081 | 459 | script, flavour_two, series_name, seed_one, False) | ||
1082 | 460 | observed_task_two_two = self.getTaskNameFromSeed( | ||
1083 | 461 | script, flavour_two, series_name, seed_two, False) | ||
1084 | 462 | |||
1085 | 463 | # seed_one is not per-derivative, so it is honoured only for | ||
1086 | 464 | # flavour_one and has a global name. | ||
1087 | 465 | self.assertEqual(seed_one, observed_task_one_one) | ||
1088 | 466 | self.assertIsNone(observed_task_two_one) | ||
1089 | 467 | |||
1090 | 468 | # seed_two is per-derivative, so it is honoured for both flavours | ||
1091 | 469 | # and has the flavour name prefixed. | ||
1092 | 470 | self.assertEqual( | ||
1093 | 471 | "%s-%s" % (flavour_one, seed_two), observed_task_one_two) | ||
1094 | 472 | self.assertEqual( | ||
1095 | 473 | "%s-%s" % (flavour_two, seed_two), observed_task_two_two) | ||
1096 | 474 | |||
1097 | 475 | def test_task_seeds(self): | ||
1098 | 476 | # The Task-Seeds field is honoured. | ||
1099 | 477 | series_name = self.factory.getUniqueString() | ||
1100 | 478 | one = self.getUniqueString() | ||
1101 | 479 | two = self.getUniqueString() | ||
1102 | 480 | script = self.makeScript(None) | ||
1103 | 481 | |||
1104 | 482 | flavour = self.factory.getUniqueString() | ||
1105 | 483 | seed_one = self.factory.getUniqueString() | ||
1106 | 484 | seed_two = self.factory.getUniqueString() | ||
1107 | 485 | self.makeSeed(flavour, series_name, seed_one, [one]) | ||
1108 | 486 | self.makeSeed( | ||
1109 | 487 | flavour, series_name, seed_two, [two], | ||
1110 | 488 | headers=["Task-Seeds: %s" % seed_one]) | ||
1111 | 489 | |||
1112 | 490 | task_seeds = self.getTaskSeedsFromSeed( | ||
1113 | 491 | script, flavour, series_name, seed_two) | ||
1114 | 492 | self.assertContentEqual([seed_one, seed_two], task_seeds) | ||
1115 | 493 | |||
1116 | 494 | def test_germinate_output_build_essential(self): | ||
1117 | 495 | # germinateArch produces Build-Essential extra overrides. | ||
1118 | 496 | distro = self.makeDistro() | ||
1119 | 497 | distroseries = self.factory.makeDistroSeries(distribution=distro) | ||
1120 | 498 | series_name = distroseries.name | ||
1121 | 499 | component = self.factory.makeComponent() | ||
1122 | 500 | self.factory.makeComponentSelection( | ||
1123 | 501 | distroseries=distroseries, component=component) | ||
1124 | 502 | das = self.factory.makeDistroArchSeries(distroseries=distroseries) | ||
1125 | 503 | arch = das.architecturetag | ||
1126 | 504 | package = self.makePackage(component, [das]) | ||
1127 | 505 | script = self.makeScript(distro) | ||
1128 | 506 | self.makeIndexFiles(script, distroseries) | ||
1129 | 507 | |||
1130 | 508 | flavour = self.factory.getUniqueString() | ||
1131 | 509 | seed = "build-essential" | ||
1132 | 510 | self.makeSeedStructure(flavour, series_name, [seed]) | ||
1133 | 511 | self.makeSeed(flavour, series_name, seed, [package.name]) | ||
1134 | 512 | |||
1135 | 513 | overrides = self.fetchGerminatedOverrides( | ||
1136 | 514 | script, series_name, arch, [flavour]) | ||
1137 | 515 | self.assertContentEqual( | ||
1138 | 516 | ["%s/%s Build-Essential yes" % (package.name, arch)], overrides) | ||
1139 | 517 | |||
1140 | 518 | def test_main(self): | ||
1141 | 519 | # If run end-to-end, the script generates override files containing | ||
1142 | 520 | # output for all architectures, and sends germinate's log output to | ||
1143 | 521 | # a file. | ||
1144 | 522 | distro = self.makeDistro() | ||
1145 | 523 | distroseries = self.factory.makeDistroSeries(distribution=distro) | ||
1146 | 524 | series_name = distroseries.name | ||
1147 | 525 | component = self.factory.makeComponent() | ||
1148 | 526 | self.factory.makeComponentSelection( | ||
1149 | 527 | distroseries=distroseries, component=component) | ||
1150 | 528 | das_one = self.factory.makeDistroArchSeries(distroseries=distroseries) | ||
1151 | 529 | arch_one = das_one.architecturetag | ||
1152 | 530 | das_two = self.factory.makeDistroArchSeries(distroseries=distroseries) | ||
1153 | 531 | arch_two = das_two.architecturetag | ||
1154 | 532 | package = self.makePackage(component, [das_one, das_two]) | ||
1155 | 533 | flavour = self.factory.getUniqueString() | ||
1156 | 534 | script = self.makeScript(distro, extra_args=[flavour]) | ||
1157 | 535 | self.makeIndexFiles(script, distroseries) | ||
1158 | 536 | |||
1159 | 537 | seed = self.factory.getUniqueString() | ||
1160 | 538 | self.makeSeedStructure(flavour, series_name, [seed]) | ||
1161 | 539 | self.makeSeed( | ||
1162 | 540 | flavour, series_name, seed, [package.name], | ||
1163 | 541 | headers=["Task-Description: task"]) | ||
1164 | 542 | |||
1165 | 543 | script.process(seed_bases=["file://%s" % self.seeddir]) | ||
1166 | 544 | override_path = os.path.join( | ||
1167 | 545 | script.config.miscroot, | ||
1168 | 546 | "more-extra.override.%s.main" % series_name) | ||
1169 | 547 | expected_overrides = [ | ||
1170 | 548 | "%s/%s Task %s" % (package.name, arch_one, seed), | ||
1171 | 549 | "%s/%s Task %s" % (package.name, arch_two, seed), | ||
1172 | 550 | ] | ||
1173 | 551 | self.assertContentEqual( | ||
1174 | 552 | expected_overrides, file_contents(override_path).splitlines()) | ||
1175 | 553 | |||
1176 | 554 | log_file = os.path.join( | ||
1177 | 555 | script.config.germinateroot, "germinate.output") | ||
1178 | 556 | self.assertIn("Downloading file://", file_contents(log_file)) | ||
1179 | 557 | |||
1180 | 558 | def test_run_script(self): | ||
1181 | 559 | # The script will run stand-alone. | ||
1182 | 560 | from canonical.launchpad.scripts.tests import run_script | ||
1183 | 561 | distro = self.makeDistro() | ||
1184 | 562 | self.factory.makeDistroSeries(distro) | ||
1185 | 563 | transaction.commit() | ||
1186 | 564 | retval, out, err = run_script( | ||
1187 | 565 | "cronscripts/generate-extra-overrides.py", | ||
1188 | 566 | ["-d", distro.name, "-q"]) | ||
1189 | 567 | self.assertEqual(0, retval) | ||
1190 | 0 | 568 | ||
1191 | === removed file 'lib/lp/soyuz/scripts/tests/germinate-test-data/mock-bin/germinate' | |||
1192 | --- lib/lp/soyuz/scripts/tests/germinate-test-data/mock-bin/germinate 2010-10-20 13:23:05 +0000 | |||
1193 | +++ lib/lp/soyuz/scripts/tests/germinate-test-data/mock-bin/germinate 1970-01-01 00:00:00 +0000 | |||
1194 | @@ -1,5 +0,0 @@ | |||
1195 | 1 | #!/bin/sh | ||
1196 | 2 | # | ||
1197 | 3 | # This is a mock germinate script that just produces enough (empty) | ||
1198 | 4 | # files so that the cron.germinate shell script can run. | ||
1199 | 5 | touch structure all all.sources minimal standard | ||
1200 | 6 | 0 | ||
1201 | === added file 'lib/lp/soyuz/scripts/tests/germinate-test-data/mock-lp-root/cronscripts/generate-extra-overrides.py' | |||
1202 | --- lib/lp/soyuz/scripts/tests/germinate-test-data/mock-lp-root/cronscripts/generate-extra-overrides.py 1970-01-01 00:00:00 +0000 | |||
1203 | +++ lib/lp/soyuz/scripts/tests/germinate-test-data/mock-lp-root/cronscripts/generate-extra-overrides.py 2011-12-14 15:49:40 +0000 | |||
1204 | @@ -0,0 +1,5 @@ | |||
1205 | 1 | #! /usr/bin/python | ||
1206 | 2 | # | ||
1207 | 3 | # We don't need to run generate-extra-overrides.py when testing | ||
1208 | 4 | # maintenance-check.py. We could, but it would slow down the tests and its | ||
1209 | 5 | # output is not interesting here. | ||
1210 | 0 | 6 | ||
1211 | === modified file 'lib/lp/soyuz/scripts/tests/test_cron_germinate.py' | |||
1212 | --- lib/lp/soyuz/scripts/tests/test_cron_germinate.py 2011-01-19 17:49:10 +0000 | |||
1213 | +++ lib/lp/soyuz/scripts/tests/test_cron_germinate.py 2011-12-14 15:49:40 +0000 | |||
1214 | @@ -148,10 +148,8 @@ | |||
1215 | 148 | os.path.join(archive_dir, "ubuntu")) | 148 | os.path.join(archive_dir, "ubuntu")) |
1216 | 149 | fake_environ["TEST_LAUNCHPADROOT"] = os.path.abspath( | 149 | fake_environ["TEST_LAUNCHPADROOT"] = os.path.abspath( |
1217 | 150 | os.path.join(basepath, "germinate-test-data/mock-lp-root")) | 150 | os.path.join(basepath, "germinate-test-data/mock-lp-root")) |
1222 | 151 | # Set the PATH in the fake environment so that our mock germinate | 151 | # Set the PATH in the fake environment so that our mock lockfile is |
1223 | 152 | # is used. We could use the real germinate as well, but that will | 152 | # used. |
1220 | 153 | # slow down the tests a lot and its also not interessting for this | ||
1221 | 154 | # test as we do not use any of the germinate information. | ||
1224 | 155 | fake_environ["PATH"] = "%s:%s" % ( | 153 | fake_environ["PATH"] = "%s:%s" % ( |
1225 | 156 | os.path.abspath(os.path.join( | 154 | os.path.abspath(os.path.join( |
1226 | 157 | basepath, "germinate-test-data/mock-bin")), | 155 | basepath, "germinate-test-data/mock-bin")), |
I've tested the core logic on mawson now (stripped out the database interaction and hardcoded paths to a local mirror so that I could run it in isolation). Compared to an output tree generated by the old cron.germinate with germinate 2.0, the substantive output is identical: various files are deliberately no longer generated, and the log output is different, but the actual germinate output and the more-extra. override. precise. main file are identical. It runs in about 7 minutes compared with about 14/15 minutes for the old cron.germinate, which I call a win (although these are single-run figures).
The weird part is that something is wrong that means we aren't even getting a lot of the benefit yet! It isn't actually sharing the results of germinating common seeds. Therefore, that 7/8 minutes of win must essentially come from avoiding duplicated seed parsing, avoiding duplicated Packages/Sources parsing, and a reduction in the number of files we write. I wasn't expecting that to account for so much of the runtime, but I'm not going to complain.
I will investigate further to find out why common seed output isn't being shared. That should bring the runtime down further.