Merge lp:~allenap/maas/celery-in-dns-removal into lp:~maas-committers/maas/trunk

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 3454
Proposed branch: lp:~allenap/maas/celery-in-dns-removal
Merge into: lp:~maas-committers/maas/trunk
Prerequisite: lp:~allenap/maas/celery-in-dns-remove-setup-rndc-configuration
Diff against target: 2017 lines (+659/-823)
27 files modified
docs/development/lease-scanning-and-dns.rst (+8/-12)
docs/development/tagging.rst (+5/-0)
etc/maas/templates/dns/named.conf.options.inside.maas.template (+5/-4)
etc/maas/templates/dns/named.conf.template (+3/-1)
required-packages/base (+0/-1)
src/maas/celeryconfig.py (+0/-34)
src/maas/settings.py (+0/-3)
src/maasserver/dns/config.py (+101/-74)
src/maasserver/dns/connect.py (+20/-20)
src/maasserver/dns/tests/test_config.py (+76/-65)
src/maasserver/management/commands/write_dns_config.py (+2/-2)
src/maasserver/models/node.py (+4/-4)
src/maasserver/models/tests/test_dhcplease.py (+2/-2)
src/maasserver/models/tests/test_node.py (+4/-4)
src/maasserver/start_up.py (+2/-2)
src/maasserver/tests/test_commands_write_dns_config.py (+3/-3)
src/maasserver/tests/test_forms_network.py (+6/-6)
src/maasserver/tests/test_start_up.py (+1/-1)
src/maastesting/celery.py (+0/-84)
src/maastesting/tests/test_celery.py (+0/-67)
src/provisioningserver/dns/actions.py (+142/-0)
src/provisioningserver/dns/tests/test_actions.py (+271/-0)
src/provisioningserver/dns/tests/test_config.py (+4/-3)
src/provisioningserver/logger/utils.py (+0/-47)
src/provisioningserver/rpc/clusterservice.py (+0/-4)
src/provisioningserver/tasks.py (+0/-153)
src/provisioningserver/tests/test_tasks.py (+0/-227)
To merge this branch: bzr merge lp:~allenap/maas/celery-in-dns-removal
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Jeroen T. Vermeulen (community) Approve
Review via email: mp+241852@code.launchpad.net

Commit message

Remove all remaining Celery dependencies.

Description of the change

Much of this is mechanical, including name changes for several
functions, and fair amount of code has been removed.

I changed names because my brain was aching trying to follow all the
calls. The API had grown ad-hoc and there was no consistency. There are
still some warts (there are code comments to explain). Anyway, the main
things I changed are:

- In the region the functions to update DNS settings/options and zones
  are all prefixed with "dns_".

- In the cluster the functions to update BIND configurations and write
  zones files are all prefixed with "bind_". These are lower-level than
  the dns_* functions.

There's no RPC between region and cluster, so the division is really
just a conceptual one, for now. As before, we only ever update a single
DNS server which runs on the same machine as the region controller.

To be clear: MAAS does not support DNS on multiple region controllers,
and never has, so don't worry that that's missing here. The division of
responsibility between the dns_* and bind_* functions lends itself to
solving this later.

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Brilliant, thanks! I have a few notes inline, but nothing serious. Some things may be worth polishing in later branches.

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

Thank you for taking this on, I really appreciate it.

Revision history for this message
Gavin Panella (allenap) wrote :

This is now ready to land. After discussion with Andres, I'm holding on this because it doesn't really fit with the goals of 1.7.1. It's up to the folk in Austin this week to decide when to land it :)

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Saturday 15 Nov 2014 11:46:41 you wrote:
> To be clear: MAAS does not support DNS on multiple region controllers,
> and never has, so don't worry that that's missing here. The division of
> responsibility between the dns_* and bind_* functions lends itself to
> solving this later.

Just a thought, but if each region controller physical host runs a bind daemon
then a per-host lock will suffice to duplicate the jobs to the right places.

This has the advantage of making our DNS HA.

Revision history for this message
Gavin Panella (allenap) wrote :

> Just a thought, but if each region controller physical host runs a
> bind daemon then a per-host lock will suffice to duplicate the jobs to
> the right places.

It will, but a web API request to, say, provision a node arrives at only
one host (different each time maybe, but only one host sees each
individual request), and that's when we trigger updates to DNS. The
region controller that services that request needs to be able to tell at
least one region controller on each other host in the region to update
BIND's configuration and zone files.

> This has the advantage of making our DNS HA.

We have a "mesh" so that every region controller can talk to every
cluster controller, and we could extend that so that every region
controller can talk to every other region controller, and thus address
the problem above.

Alternative #1: we could set up BIND to be HA on its own. Its config
would be updated by a periodic job, say, but zone data updates would
come via nsupdate. BIND would then sort out distributing those changes
as required.

Alternative #2: we create an independent BIND management service. The
region calls out to it when a change is needed and it then encapsulates
configuration changes, zone data changes, and HA, and the region doesn't
need to know.

Personally I think I like #2 most. I suspect it might also be the least
work. Meshing together all the region controllers was originally my
favourite, but I see many possibilities for deadlock there.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Tuesday 18 Nov 2014 10:48:21 you wrote:
> > Just a thought, but if each region controller physical host runs a
> > bind daemon then a per-host lock will suffice to duplicate the jobs to
> > the right places.
>
> It will, but a web API request to, say, provision a node arrives at only
> one host (different each time maybe, but only one host sees each
> individual request), and that's when we trigger updates to DNS. The
> region controller that services that request needs to be able to tell at
> least one region controller on each other host in the region to update
> BIND's configuration and zone files.

Right, that's what I mean by a per-host lock. We just create a job on every
host.

>
> > This has the advantage of making our DNS HA.
>
> We have a "mesh" so that every region controller can talk to every
> cluster controller, and we could extend that so that every region
> controller can talk to every other region controller, and thus address
> the problem above.

We could also do PG triggers.

>
> Alternative #1: we could set up BIND to be HA on its own. Its config
> would be updated by a periodic job, say, but zone data updates would
> come via nsupdate. BIND would then sort out distributing those changes
> as required.

We would still have the problem of deciding from which appserver we send
updates / amend the zone file. But it would be nice to use its own
replication!

>
> Alternative #2: we create an independent BIND management service. The
> region calls out to it when a change is needed and it then encapsulates
> configuration changes, zone data changes, and HA, and the region doesn't
> need to know.
>
> Personally I think I like #2 most. I suspect it might also be the least
> work. Meshing together all the region controllers was originally my
> favourite, but I see many possibilities for deadlock there.

It's an idea. We should put something together to discuss all of these
options.

I suspect the trigger thing might be the least work but I've not thought hard
about it yet.

Revision history for this message
MAAS Lander (maas-lander) wrote :

Voting does not meet specified criteria. Required: Approve >= 1, Disapprove == 0. Got: .

Revision history for this message
Gavin Panella (allenap) :
review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (21.2 KiB)

The attempt to merge lp:~allenap/maas/celery-in-dns-removal into lp:maas failed. Below is the output from the failed tests.

Ign http://security.ubuntu.com trusty-security InRelease
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Get:1 http://security.ubuntu.com trusty-security Release.gpg [933 B]
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Get:2 http://security.ubuntu.com trusty-security Release [62.0 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Hit http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Hit http://nova.clouds.archive.ubuntu.com trusty-updates Release
Get:3 http://security.ubuntu.com trusty-security/main Sources [58.3 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Get:4 http://security.ubuntu.com trusty-security/universe Sources [17.4 kB]
Get:5 http://security.ubuntu.com trusty-security/main amd64 Packages [186 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Get:6 http://security.ubuntu.com trusty-security/universe amd64 Packages [81.9 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources
Get:7 http://security.ubuntu.com trusty-security/main Translation-en [93.3 kB]
Get:8 http://security.ubuntu.com trusty-security/universe Translation-en [44.3 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Fetched 544 kB in 2s (194 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 authbind bind9 bind9utils build-essential bzr-builddeb curl daemontools debhelper dh-apport distro-info dnsutils firefox freeipmi-tools gjs ipython isc-dhcp-common libjs-raphael libjs-yui3-full libjs-yui3-min libpq-dev make pep8 postgresql pyflakes python-amqplib python-bzrlib python-convoy python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-extras python-fixtures python-flake8 python-formencode python-hivex python-httplib2 python-jinja2 python-jsonschema python-lockfile python-lxml python-mimeparse python-mock python-netaddr python-netifaces python-nose python-oauth python-oops python-oops-amqp python-oops-datedir-repo python-oops-twisted python-oops-wsgi python-openssl python-paramiko python-pexpect python-pip python-pocket...

Revision history for this message
MAAS Lander (maas-lander) wrote :

There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'docs/development/lease-scanning-and-dns.rst'
2--- docs/development/lease-scanning-and-dns.rst 2014-10-02 11:53:59 +0000
3+++ docs/development/lease-scanning-and-dns.rst 2015-01-13 18:36:01 +0000
4@@ -26,15 +26,11 @@
5 stores the active leases in the DHCPLease table.
6
7
8-Updating the DNS zone file
9-==========================
10-
11-If a new lease is found then the dns.change_dns_zones() function gets called
12-which invokes two tasks::
13-
14- #. ``write_dns_zone_config()``
15- #. ``rndc_command()``
16-
17-The first is responsible for writing out a new zone file with the appropriate
18-sequence number and timestamp, and then the second is chained on to that
19-and sends an rndc message to the DNS server to reload the zone.
20+Updating one or more DNS zone files
21+===================================
22+
23+If a new lease is found then the dns.dns_update_zones() function gets called
24+which takes two steps::
25+
26+ #. Write out updated zone files.
27+ #. Ask BIND to reload the zone.
28
29=== modified file 'docs/development/tagging.rst'
30--- docs/development/tagging.rst 2014-08-16 10:34:55 +0000
31+++ docs/development/tagging.rst 2015-01-13 18:36:01 +0000
32@@ -16,6 +16,11 @@
33 New or updated tag definition
34 -----------------------------
35
36+**Note** that this is somewhat outdated. See `bug 1372544`_ (*Tag changes will
37+never be evaluated on unconnected clusters*) for more information.
38+
39+.. _bug 1372544: https://bugs.launchpad.net/maas/+bug/1372544
40+
41 When a new tag is created or an existing tag is modified, its
42 expression must be evaluated for every node known to the region. It's
43 a moderately computationally intensive process, so the work is spread
44
45=== modified file 'etc/maas/templates/dns/named.conf.options.inside.maas.template'
46--- etc/maas/templates/dns/named.conf.options.inside.maas.template 2014-08-29 04:27:00 +0000
47+++ etc/maas/templates/dns/named.conf.options.inside.maas.template 2015-01-13 18:36:01 +0000
48@@ -1,10 +1,11 @@
49 {{if upstream_dns}}
50- forwarders {
51- {{upstream_dns}};
52- };
53+forwarders {
54+{{for upstream_dns_address in upstream_dns}}
55+ {{upstream_dns_address}};
56+{{endfor}}
57+};
58 {{endif}}
59
60 allow-query { any; };
61 allow-recursion { trusted; };
62 allow-query-cache { trusted; };
63-
64
65=== modified file 'etc/maas/templates/dns/named.conf.template'
66--- etc/maas/templates/dns/named.conf.template 2014-08-29 04:27:00 +0000
67+++ etc/maas/templates/dns/named.conf.template 2015-01-13 18:36:01 +0000
68@@ -11,7 +11,9 @@
69 # Access control for recursive queries. See named.conf.options.inside.maas
70 # for the directives used on this ACL.
71 acl "trusted" {
72- {{trusted_networks}}
73+{{for trusted_network in trusted_networks}}
74+ {{trusted_network}};
75+{{endfor}}
76 localnets;
77 localhost;
78 };
79
80=== modified file 'required-packages/base'
81--- required-packages/base 2014-09-24 13:05:22 +0000
82+++ required-packages/base 2015-01-13 18:36:01 +0000
83@@ -13,7 +13,6 @@
84 postgresql
85 python-amqplib
86 python-bzrlib
87-python-celery
88 python-convoy
89 python-crochet
90 python-curtin
91
92=== removed file 'src/maas/celeryconfig.py'
93--- src/maas/celeryconfig.py 2014-09-29 15:51:04 +0000
94+++ src/maas/celeryconfig.py 1970-01-01 00:00:00 +0000
95@@ -1,34 +0,0 @@
96-# Copyright 2012-2014 Canonical Ltd. This software is licensed under the
97-# GNU Affero General Public License version 3 (see the file LICENSE).
98-
99-"""Celery settings for the region controller."""
100-
101-from __future__ import (
102- absolute_import,
103- print_function,
104- unicode_literals,
105-)
106-
107-str = None
108-
109-__metaclass__ = type
110-
111-
112-# Each cluster should have its own queue created automatically by Celery.
113-CELERY_CREATE_MISSING_QUEUES = True
114-
115-CELERY_IMPORTS = (
116- # Tasks.
117- "provisioningserver.tasks",
118- )
119-
120-CELERY_ACKS_LATE = True
121-
122-# Do not store the tasks' return values (aka tombstones);
123-# This improves performance.
124-CELERY_IGNORE_RESULT = True
125-
126-# Don't queue, always run tasks immediately, and propagate exceptions back to
127-# the caller. This eliminates the need for a queue.
128-CELERY_ALWAYS_EAGER = True
129-CELERY_EAGER_PROPAGATES_EXCEPTIONS = True
130
131=== modified file 'src/maas/settings.py'
132--- src/maas/settings.py 2014-11-10 21:21:47 +0000
133+++ src/maas/settings.py 2015-01-13 18:36:01 +0000
134@@ -309,9 +309,6 @@
135 'maasjson': 'maasserver.json',
136 }
137
138-# Set up celery to use the production settings.
139-os.environ['CELERY_CONFIG_MODULE'] = 'maas.celeryconfig'
140-
141 # Allow the user to override settings in maas_local_settings.
142 import_local_settings()
143
144
145=== modified file 'src/maasserver/dns/config.py'
146--- src/maasserver/dns/config.py 2014-10-15 10:28:01 +0000
147+++ src/maasserver/dns/config.py 2015-01-13 18:36:01 +0000
148@@ -13,15 +13,11 @@
149
150 __metaclass__ = type
151 __all__ = [
152- 'add_zone',
153- 'change_dns_zones',
154- 'get_trusted_networks',
155- 'is_dns_enabled',
156- 'next_zone_serial',
157- 'write_full_dns_config',
158+ 'dns_add_zones',
159+ 'dns_update_all_zones',
160+ 'dns_update_zones',
161 ]
162
163-
164 from django.conf import settings
165 from maasserver import locks
166 from maasserver.dns.zonegenerator import ZoneGenerator
167@@ -35,7 +31,15 @@
168 Sequence,
169 )
170 from maasserver.utils import synchronised
171-from provisioningserver import tasks
172+from provisioningserver.dns.actions import (
173+ bind_reconfigure,
174+ bind_reload,
175+ bind_reload_with_retries,
176+ bind_reload_zone,
177+ bind_write_configuration,
178+ bind_write_options,
179+ bind_write_zones,
180+ )
181 from provisioningserver.logger import get_maas_logger
182
183
184@@ -67,88 +71,111 @@
185
186
187 @synchronised(locks.dns)
188-def change_dns_zones(nodegroups):
189- """Update the zone configuration for the given list of Nodegroups.
190-
191- :param nodegroups: The list of nodegroups (or the nodegroup) for which the
192- zone should be updated.
193- :type nodegroups: list (or :class:`NodeGroup`)
194- """
195- if not (is_dns_enabled() and is_dns_in_use()):
196- return
197- serial = next_zone_serial()
198- for zone in ZoneGenerator(nodegroups, serial):
199- maaslog.info("Generating new DNS zone file for %s", zone.zone_name)
200- zone_reload_subtask = tasks.rndc_command.subtask(
201- args=[['reload', zone.zone_name]])
202- tasks.write_dns_zone_config.delay(
203- zones=[zone], callback=zone_reload_subtask)
204-
205-
206-@synchronised(locks.dns)
207-def add_zone(nodegroup):
208- """Add to the DNS server a new zone for the given `nodegroup`.
209-
210- To do this we have to write a new configuration file for the zone
211- and update the master config to include this new configuration.
212- These are done in turn by chaining Celery subtasks.
213-
214- :param nodegroup: The nodegroup for which the zone should be added.
215- :type nodegroup: :class:`NodeGroup`
216- """
217- if not (is_dns_enabled() and is_dns_in_use()):
218- return
219+def dns_add_zones(clusters):
220+ """Add zone files for the given cluster(s), and serve them.
221+
222+ Serving these new zone files means updating BIND's configuration to
223+ include them, then asking it to load the new configuration.
224+
225+ :param clusters: The clusters(s) for which zones should be served.
226+ :type clusters: :py:class:`NodeGroup`, or an iterable thereof.
227+ """
228+ if not (is_dns_enabled() and is_dns_in_use()):
229+ return
230+
231 zones_to_write = ZoneGenerator(
232- nodegroup, serial_generator=next_zone_serial
233- ).as_list()
234+ clusters, serial_generator=next_zone_serial).as_list()
235 if len(zones_to_write) == 0:
236 return None
237 serial = next_zone_serial()
238 # Compute non-None zones.
239 zones = ZoneGenerator(NodeGroup.objects.all(), serial).as_list()
240- reconfig_subtask = tasks.rndc_command.subtask(args=[['reconfig']])
241- write_dns_config_subtask = tasks.write_dns_config.subtask(
242- kwargs={
243- 'zones': zones, 'callback': reconfig_subtask,
244- 'trusted_networks': get_trusted_networks()})
245- tasks.write_dns_zone_config.delay(
246- zones=zones_to_write, callback=write_dns_config_subtask)
247-
248-
249-@synchronised(locks.dns)
250-def write_full_dns_config(reload_retry=False, force=False):
251- """Write the DNS configuration.
252-
253- :param reload_retry: Should the reload rndc command be retried in case
254- of failure? Defaults to `False`.
255+ bind_write_zones(zones_to_write)
256+ bind_write_configuration(zones, trusted_networks=get_trusted_networks())
257+ bind_reconfigure()
258+
259+
260+@synchronised(locks.dns)
261+def dns_update_zones(clusters):
262+ """Update the zone files for the given cluster(s).
263+
264+ Once the new zone files have been written, BIND is asked to reload them.
265+ It's assumed that BIND already serves the zones that are being written.
266+
267+ :param clusters: Those cluster(s) for which the zone should be updated.
268+ :type clusters: A :py:class:`NodeGroup`, or an iterable thereof.
269+ """
270+ if not (is_dns_enabled() and is_dns_in_use()):
271+ return
272+
273+ serial = next_zone_serial()
274+ for zone in ZoneGenerator(clusters, serial):
275+ maaslog.info("Generating new DNS zone file for %s", zone.zone_name)
276+ bind_write_zones([zone])
277+ bind_reload_zone(zone.zone_name)
278+
279+
280+@synchronised(locks.dns)
281+def dns_update_all_zones(reload_retry=False, force=False):
282+ """Update all zone files for the given cluster(s), and serve them.
283+
284+ Serving these zone files means updating BIND's configuration to include
285+ them, then asking it to load the new configuration.
286+
287+ :param reload_retry: Should the DNS server reload be retried in case
288+ of failure? Defaults to `False`.
289 :type reload_retry: bool
290- :param force: Write the configuration even if no interface is
291- configured to manage DNS.
292+ :param force: Update the configuration even if no interface is configured
293+ to manage DNS. This makes sense when deconfiguring an interface.
294 :type force: bool
295 """
296- write_conf = (
297- is_dns_enabled() and (force or is_dns_in_use()))
298+ write_conf = is_dns_enabled() and (force or is_dns_in_use())
299 if not write_conf:
300 return
301+
302+ clusters = NodeGroup.objects.all()
303 zones = ZoneGenerator(
304- NodeGroup.objects.all(), serial_generator=next_zone_serial
305- ).as_list()
306+ clusters, serial_generator=next_zone_serial).as_list()
307+ bind_write_zones(zones)
308+
309+ # We should not be calling bind_write_options() here; call-sites should be
310+ # making a separate call. It's a historical legacy, where many sites now
311+ # expect this side-effect from calling dns_update_all_zones(), and some
312+ # that call it for this side-effect alone. At present all it does is set
313+ # the upstream DNS servers, nothing to do with serving zones at all!
314+ bind_write_options(upstream_dns=get_upstream_dns())
315+
316+ # Nor should we be rewriting ACLs that are related only to allowing
317+ # recursive queries to the upstream DNS servers. Again, this is legacy,
318+ # where the "trusted" ACL ended up in the same configuration file as the
319+ # zone stanzas, and so both need to be rewritten at the same time.
320+ bind_write_configuration(zones, trusted_networks=get_trusted_networks())
321+
322+ # Reloading with retries may be a legacy from Celery days, or it may be
323+ # necessary to recover from races during start-up. We're not sure if it is
324+ # actually needed but it seems safer to maintain this behaviour until we
325+ # have a better understanding.
326+ if reload_retry:
327+ bind_reload_with_retries()
328+ else:
329+ bind_reload()
330+
331+
332+def get_upstream_dns():
333+ """Return the IP addresses of configured upstream DNS servers.
334+
335+ :return: A list of IP addresses.
336+ """
337 upstream_dns = Config.objects.get_config("upstream_dns")
338- tasks.write_full_dns_config.delay(
339- zones=zones,
340- callback=tasks.rndc_command.subtask(
341- args=[['reload'], reload_retry]),
342- upstream_dns=upstream_dns,
343- trusted_networks=get_trusted_networks())
344+ return [] if upstream_dns is None else upstream_dns.split()
345
346
347 def get_trusted_networks():
348 """Return the CIDR representation of all the Networks we know about.
349
350- This must be a whitespace separated list, where each item ends in a
351- semicolon, or blank if there's no networks.
352+ :return: A list of CIDR-format network specifications.
353 """
354- networks = " ".join(
355- "%s;" % net.get_network().cidr
356- for net in Network.objects.all())
357- return networks
358+ return [
359+ unicode(net.get_network().cidr)
360+ for net in Network.objects.all()
361+ ]
362
363=== modified file 'src/maasserver/dns/connect.py'
364--- src/maasserver/dns/connect.py 2014-08-29 09:42:38 +0000
365+++ src/maasserver/dns/connect.py 2015-01-13 18:36:01 +0000
366@@ -36,13 +36,13 @@
367 def dns_post_save_NodeGroup(sender, instance, created, **kwargs):
368 """Create or update DNS zones related to the saved nodegroup."""
369 from maasserver.dns.config import (
370- write_full_dns_config,
371- add_zone,
372+ dns_update_all_zones,
373+ dns_add_zones,
374 )
375 if created:
376- add_zone(instance)
377+ dns_add_zones(instance)
378 else:
379- write_full_dns_config()
380+ dns_update_all_zones()
381
382
383 # XXX rvb 2012-09-12: This is only needed because we use that
384@@ -52,24 +52,24 @@
385 def dns_post_save_NodeGroupInterface(sender, instance, created, **kwargs):
386 """Create or update DNS zones related to the saved nodegroupinterface."""
387 from maasserver.dns.config import (
388- write_full_dns_config,
389- add_zone,
390+ dns_update_all_zones,
391+ dns_add_zones,
392 )
393 if created:
394- add_zone(instance.nodegroup)
395+ dns_add_zones(instance.nodegroup)
396 else:
397- write_full_dns_config()
398+ dns_update_all_zones()
399
400
401 def dns_post_edit_management_NodeGroupInterface(instance, old_values, deleted):
402 """Delete DNS zones related to the interface."""
403- from maasserver.dns.config import write_full_dns_config
404+ from maasserver.dns.config import dns_update_all_zones
405 [old_field] = old_values
406 if old_field == NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS:
407 # Force the dns config to be written as this might have been
408 # triggered by the last DNS-enabled interface being deleted
409 # or switched off (i.e. management set to DHCP or UNMANAGED).
410- write_full_dns_config(force=True)
411+ dns_update_all_zones(force=True)
412
413
414 connect_to_field_change(
415@@ -81,8 +81,8 @@
416 def dns_post_delete_Node(sender, instance, **kwargs):
417 """When a Node is deleted, update the Node's zone file."""
418 try:
419- from maasserver.dns.config import change_dns_zones
420- change_dns_zones(instance.nodegroup)
421+ from maasserver.dns.config import dns_update_zones
422+ dns_update_zones(instance.nodegroup)
423 except NodeGroup.DoesNotExist:
424 # If this Node is being deleted because the whole NodeGroup
425 # has been deleted, no need to update the zone file because
426@@ -92,29 +92,29 @@
427
428 def dns_post_edit_hostname_Node(instance, old_values, **kwargs):
429 """When a Node has been flagged, update the related zone."""
430- from maasserver.dns.config import change_dns_zones
431- change_dns_zones(instance.nodegroup)
432+ from maasserver.dns.config import dns_update_zones
433+ dns_update_zones(instance.nodegroup)
434
435
436 connect_to_field_change(dns_post_edit_hostname_Node, Node, ['hostname'])
437
438
439 def dns_setting_changed(sender, instance, created, **kwargs):
440- from maasserver.dns.config import write_full_dns_config
441- write_full_dns_config()
442+ from maasserver.dns.config import dns_update_all_zones
443+ dns_update_all_zones()
444
445
446 @receiver(post_save, sender=Network)
447 def dns_post_save_Network(sender, instance, **kwargs):
448 """When a network is added/changed, put it in the DNS trusted networks."""
449- from maasserver.dns.config import write_full_dns_config
450- write_full_dns_config()
451+ from maasserver.dns.config import dns_update_all_zones
452+ dns_update_all_zones()
453
454
455 @receiver(post_delete, sender=Network)
456 def dns_post_delete_Network(sender, instance, **kwargs):
457- from maasserver.dns.config import write_full_dns_config
458- write_full_dns_config()
459+ from maasserver.dns.config import dns_update_all_zones
460+ dns_update_all_zones()
461
462
463 Config.objects.config_changed_connect("upstream_dns", dns_setting_changed)
464
465=== modified file 'src/maasserver/dns/tests/test_config.py'
466--- src/maasserver/dns/tests/test_config.py 2014-10-29 17:05:36 +0000
467+++ src/maasserver/dns/tests/test_config.py 2015-01-13 18:36:01 +0000
468@@ -18,20 +18,19 @@
469 from itertools import islice
470 import random
471
472-from celery.task import task
473 from django.conf import settings
474 from django.core.management import call_command
475 from maasserver import locks
476 from maasserver.dns import config as dns_config_module
477 from maasserver.dns.config import (
478- add_zone,
479- change_dns_zones,
480+ dns_add_zones,
481+ dns_update_all_zones,
482+ dns_update_zones,
483 get_trusted_networks,
484+ get_upstream_dns,
485 is_dns_enabled,
486 is_dns_in_use,
487 next_zone_serial,
488- tasks as dns_tasks,
489- write_full_dns_config,
490 zone_serial,
491 )
492 from maasserver.enum import (
493@@ -46,17 +45,16 @@
494 from maasserver.testing.factory import factory
495 from maasserver.testing.testcase import MAASServerTestCase
496 from maastesting.fakemethod import FakeMethod
497-from maastesting.matchers import MockCalledOnceWith
498-from mock import (
499- ANY,
500- sentinel,
501+from maastesting.matchers import (
502+ MockCalledOnceWith,
503+ MockNotCalled,
504 )
505+from mock import sentinel
506 from netaddr import (
507 IPAddress,
508 IPNetwork,
509 IPRange,
510 )
511-from provisioningserver import tasks
512 from provisioningserver.dns.config import (
513 compose_config_path,
514 DNSConfig,
515@@ -141,7 +139,7 @@
516 interface.static_ip_range_low, interface.static_ip_range_high)
517 static_ip = unicode(islice(ips, lease_number, lease_number + 1).next())
518 staticaddress = factory.make_StaticIPAddress(ip=static_ip, mac=mac)
519- change_dns_zones([nodegroup])
520+ dns_update_zones([nodegroup])
521 return nodegroup, node, staticaddress
522
523 def dig_resolve(self, fqdn, version=4):
524@@ -184,57 +182,57 @@
525 def check_dns_is_locked():
526 self.assertTrue(
527 locks.dns.is_locked(), "locks.dns isn't locked")
528- return False # Prevent change_dns_zones from continuing.
529+ return False # Prevent dns_update_zones from continuing.
530
531 is_dns_enabled = self.patch_autospec(
532 dns_config_module, 'is_dns_enabled')
533 is_dns_enabled.side_effect = check_dns_is_locked
534 return is_dns_enabled
535
536- def test_change_dns_zone_uses_lock(self):
537- is_dns_enabled = self.patch_is_dns_enabled()
538- dns_config_module.change_dns_zones(sentinel.nodegroup)
539- self.assertThat(is_dns_enabled, MockCalledOnceWith())
540-
541- def test_add_zone_uses_lock(self):
542- is_dns_enabled = self.patch_is_dns_enabled()
543- dns_config_module.add_zone(sentinel.nodegroup)
544- self.assertThat(is_dns_enabled, MockCalledOnceWith())
545-
546- def test_write_full_dns_config_uses_lock(self):
547- is_dns_enabled = self.patch_is_dns_enabled()
548- dns_config_module.write_full_dns_config()
549+ def test_dns_update_zones_uses_lock(self):
550+ is_dns_enabled = self.patch_is_dns_enabled()
551+ dns_config_module.dns_update_zones(sentinel.nodegroup)
552+ self.assertThat(is_dns_enabled, MockCalledOnceWith())
553+
554+ def test_dns_add_zones_uses_lock(self):
555+ is_dns_enabled = self.patch_is_dns_enabled()
556+ dns_config_module.dns_add_zones(sentinel.nodegroup)
557+ self.assertThat(is_dns_enabled, MockCalledOnceWith())
558+
559+ def test_dns_update_all_zones_config_uses_lock(self):
560+ is_dns_enabled = self.patch_is_dns_enabled()
561+ dns_config_module.dns_update_all_zones()
562 self.assertThat(is_dns_enabled, MockCalledOnceWith())
563
564
565 class TestDNSConfigModifications(TestDNSServer):
566
567- def test_add_zone_loads_dns_zone(self):
568+ def test_dns_add_zones_loads_dns_zone(self):
569 nodegroup, node, static = self.create_nodegroup_with_static_ip()
570 self.patch(settings, 'DNS_CONNECT', True)
571- add_zone(nodegroup)
572+ dns_add_zones(nodegroup)
573 self.assertDNSMatches(node.hostname, nodegroup.name, static.ip)
574
575- def test_add_zone_preserves_trusted_networks(self):
576+ def test_dns_add_zones_preserves_trusted_networks(self):
577 nodegroup, node, static = self.create_nodegroup_with_static_ip()
578 trusted_network = factory.make_ipv4_address()
579 get_trusted_networks_patch = self.patch(
580 dns_config_module, 'get_trusted_networks')
581- get_trusted_networks_patch.return_value = trusted_network + ';'
582+ get_trusted_networks_patch.return_value = [trusted_network]
583 self.patch(settings, 'DNS_CONNECT', True)
584- add_zone(nodegroup)
585+ dns_add_zones(nodegroup)
586 self.assertThat(
587 compose_config_path(DNSConfig.target_file_name),
588 FileContains(matcher=Contains(trusted_network)))
589
590- def test_change_dns_zone_changes_dns_zone(self):
591+ def test_dns_update_zones_changes_dns_zone(self):
592 nodegroup, _, _ = self.create_nodegroup_with_static_ip()
593 self.patch(settings, 'DNS_CONNECT', True)
594- write_full_dns_config()
595+ dns_update_all_zones()
596 nodegroup, new_node, new_static = (
597 self.create_nodegroup_with_static_ip(
598 nodegroup=nodegroup, lease_number=2))
599- change_dns_zones(nodegroup)
600+ dns_update_zones(nodegroup)
601 self.assertDNSMatches(new_node.hostname, nodegroup.name, new_static.ip)
602
603 def test_is_dns_enabled_return_false_if_DNS_CONNECT_False(self):
604@@ -252,60 +250,57 @@
605 self.create_managed_nodegroup()
606 self.assertTrue(is_dns_in_use())
607
608- def test_write_full_dns_loads_full_dns_config(self):
609+ def test_dns_update_all_zones_loads_full_dns_config(self):
610 nodegroup, node, static = self.create_nodegroup_with_static_ip()
611 self.patch(settings, 'DNS_CONNECT', True)
612- write_full_dns_config()
613+ dns_update_all_zones()
614 self.assertDNSMatches(node.hostname, nodegroup.name, static.ip)
615
616- def test_write_full_dns_passes_reload_retry_parameter(self):
617+ def test_dns_update_all_zones_passes_reload_retry_parameter(self):
618 self.patch(settings, 'DNS_CONNECT', True)
619- recorder = FakeMethod()
620 self.create_managed_nodegroup()
621-
622- @task
623- def recorder_task(*args, **kwargs):
624- return recorder(*args, **kwargs)
625- self.patch(tasks, 'rndc_command', recorder_task)
626- write_full_dns_config(reload_retry=True)
627- self.assertEqual(
628- ([(['reload'], True)]), recorder.extract_args())
629-
630- def test_write_full_dns_passes_upstream_dns_parameter(self):
631+ bind_reload_with_retries = self.patch_autospec(
632+ dns_config_module, "bind_reload_with_retries")
633+ dns_update_all_zones(reload_retry=True)
634+ self.assertThat(bind_reload_with_retries, MockCalledOnceWith())
635+
636+ def test_dns_update_all_zones_passes_upstream_dns_parameter(self):
637 self.patch(settings, 'DNS_CONNECT', True)
638 self.create_managed_nodegroup()
639 random_ip = factory.make_ipv4_address()
640 Config.objects.set_config("upstream_dns", random_ip)
641- patched_task = self.patch(dns_tasks.write_full_dns_config, "delay")
642- write_full_dns_config()
643- self.assertThat(patched_task, MockCalledOnceWith(
644- zones=ANY, callback=ANY, trusted_networks=ANY,
645- upstream_dns=random_ip))
646+ bind_write_options = self.patch_autospec(
647+ dns_config_module, "bind_write_options")
648+ dns_update_all_zones()
649+ self.assertThat(
650+ bind_write_options,
651+ MockCalledOnceWith(upstream_dns=[random_ip]))
652
653- def test_write_full_dns_writes_trusted_networks_parameter(self):
654+ def test_dns_update_all_zones_writes_trusted_networks_parameter(self):
655 self.patch(settings, 'DNS_CONNECT', True)
656 self.create_managed_nodegroup()
657 trusted_network = factory.make_ipv4_address()
658 get_trusted_networks_patch = self.patch(
659 dns_config_module, 'get_trusted_networks')
660- get_trusted_networks_patch.return_value = trusted_network + ';'
661- write_full_dns_config()
662+ get_trusted_networks_patch.return_value = [trusted_network]
663+ dns_update_all_zones()
664 self.assertThat(
665 compose_config_path(DNSConfig.target_file_name),
666 FileContains(matcher=Contains(trusted_network)))
667
668- def test_write_full_dns_doesnt_call_task_it_no_interface_configured(self):
669+ def test_dns_update_all_zones_does_nada_if_no_interface_configured(self):
670 self.patch(settings, 'DNS_CONNECT', True)
671- patched_task = self.patch(dns_tasks.write_full_dns_config, "delay")
672- write_full_dns_config()
673- self.assertEqual(0, patched_task.call_count)
674+ bind_write_configuration = self.patch_autospec(
675+ dns_config_module, "bind_write_configuration")
676+ dns_update_all_zones()
677+ self.assertThat(bind_write_configuration, MockNotCalled())
678
679 def test_dns_config_has_NS_record(self):
680 ip = factory.make_ipv4_address()
681 self.patch(settings, 'DEFAULT_MAAS_URL', 'http://%s/' % ip)
682 nodegroup, node, static = self.create_nodegroup_with_static_ip()
683 self.patch(settings, 'DNS_CONNECT', True)
684- write_full_dns_config()
685+ dns_update_all_zones()
686 # Get the NS record for the zone 'nodegroup.name'.
687 ns_record = dig_call(
688 port=self.bind.config.port,
689@@ -419,7 +414,7 @@
690 ip = "%s" % random.choice(ip_range)
691 lease = factory.make_DHCPLease(
692 nodegroup=nodegroup, mac=mac.mac_address, ip=ip)
693- change_dns_zones([nodegroup])
694+ dns_update_zones([nodegroup])
695 self.assertDNSMatches(node.hostname, nodegroup.name, lease.ip)
696
697
698@@ -435,19 +430,35 @@
699 node.hostname, nodegroup.name, static.ip, version=6)
700
701
702+class TestGetUpstreamDNS(MAASServerTestCase):
703+ """Test for maasserver/dns/config.py:get_upstream_dns()"""
704+
705+ def test__returns_empty_list_if_not_set(self):
706+ self.assertEqual([], get_upstream_dns())
707+
708+ def test__returns_list_of_one_address_if_set(self):
709+ address = factory.make_ipv4_address()
710+ Config.objects.set_config("upstream_dns", address)
711+ self.assertEqual([address], get_upstream_dns())
712+
713+ def test__returns_list_of_many_address_if_set(self):
714+ addresses = [factory.make_ipv4_address(), factory.make_ipv4_address()]
715+ Config.objects.set_config("upstream_dns", " ".join(addresses))
716+ self.assertEqual(addresses, get_upstream_dns())
717+
718+
719 class TestGetTrustedNetworks(MAASServerTestCase):
720 """Test for maasserver/dns/config.py:get_trusted_networks()"""
721
722 def test__returns_empty_string_if_no_networks(self):
723- self.assertEqual("", get_trusted_networks())
724+ self.assertEqual([], get_trusted_networks())
725
726 def test__returns_single_network(self):
727 net = factory.make_Network()
728- expected = unicode(net.get_network().cidr) + ';'
729+ expected = [unicode(net.get_network().cidr)]
730 self.assertEqual(expected, get_trusted_networks())
731
732 def test__returns_many_networks(self):
733 nets = [factory.make_Network() for _ in xrange(random.randint(1, 5))]
734- expected = "; ".join(unicode(net.get_network().cidr) for net in nets)
735- expected += ';'
736+ expected = [unicode(net.get_network().cidr) for net in nets]
737 self.assertEqual(expected, get_trusted_networks())
738
739=== modified file 'src/maasserver/management/commands/write_dns_config.py'
740--- src/maasserver/management/commands/write_dns_config.py 2014-08-15 09:44:42 +0000
741+++ src/maasserver/management/commands/write_dns_config.py 2015-01-13 18:36:01 +0000
742@@ -24,7 +24,7 @@
743 ]
744
745 from django.core.management.base import BaseCommand
746-from maasserver.dns.config import write_full_dns_config
747+from maasserver.dns.config import dns_update_all_zones
748
749
750 class Command(BaseCommand):
751@@ -33,4 +33,4 @@
752 "this region has cluster controllers configured to manage DNS.")
753
754 def handle(self, *args, **options):
755- write_full_dns_config()
756+ dns_update_all_zones()
757
758=== modified file 'src/maasserver/models/node.py'
759--- src/maasserver/models/node.py 2015-01-13 17:10:46 +0000
760+++ src/maasserver/models/node.py 2015-01-13 18:36:01 +0000
761@@ -1427,8 +1427,8 @@
762 """
763 deallocated_ips = StaticIPAddress.objects.deallocate_by_node(self)
764 self.delete_host_maps(deallocated_ips)
765- from maasserver.dns.config import change_dns_zones
766- change_dns_zones([self.nodegroup])
767+ from maasserver.dns.config import dns_update_zones
768+ dns_update_zones([self.nodegroup])
769
770 def get_boot_purpose(self):
771 """
772@@ -1518,7 +1518,7 @@
773 """
774 # Avoid circular imports.
775 from metadataserver.models import NodeUserData
776- from maasserver.dns.config import change_dns_zones
777+ from maasserver.dns.config import dns_update_zones
778
779 if not by_user.has_perm(NODE_PERMISSION.EDIT, self):
780 # You can't stop a node you don't own unless you're an
781@@ -1560,7 +1560,7 @@
782 transition_timeout = None
783
784 # Update the DNS zone with the new static IP info as necessary.
785- change_dns_zones(self.nodegroup)
786+ dns_update_zones(self.nodegroup)
787
788 power_info = self.get_effective_power_info()
789 if not power_info.can_be_started:
790
791=== modified file 'src/maasserver/models/tests/test_dhcplease.py'
792--- src/maasserver/models/tests/test_dhcplease.py 2014-11-25 13:06:05 +0000
793+++ src/maasserver/models/tests/test_dhcplease.py 2015-01-13 18:36:01 +0000
794@@ -166,10 +166,10 @@
795 map_leases(nodegroup))
796
797 def test_update_leases_does_not_update_dns_zone_if_nothing_added(self):
798- self.patch(dns, 'change_dns_zones')
799+ self.patch(dns, 'dns_update_zones')
800 nodegroup = factory.make_NodeGroup()
801 DHCPLease.objects.update_leases(nodegroup, {})
802- self.assertFalse(dns.change_dns_zones.called)
803+ self.assertFalse(dns.dns_update_zones.called)
804
805 def test_get_hostname_ip_mapping_returns_mapping(self):
806 nodegroup = factory.make_NodeGroup()
807
808=== modified file 'src/maasserver/models/tests/test_node.py'
809--- src/maasserver/models/tests/test_node.py 2015-01-13 17:10:46 +0000
810+++ src/maasserver/models/tests/test_node.py 2015-01-13 18:36:01 +0000
811@@ -1228,7 +1228,7 @@
812 {node.nodegroup: expected}))
813
814 def test_deallocate_static_ip_updates_dns(self):
815- change_dns_zones = self.patch(dns_config, 'change_dns_zones')
816+ dns_update_zones = self.patch(dns_config, 'dns_update_zones')
817 nodegroup = factory.make_NodeGroup(
818 management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS,
819 status=NODEGROUP_STATUS.ACCEPTED)
820@@ -1236,7 +1236,7 @@
821 nodegroup=nodegroup, status=NODE_STATUS.ALLOCATED,
822 owner=factory.make_User(), power_type='ether_wake')
823 node.release()
824- self.assertThat(change_dns_zones, MockCalledOnceWith([node.nodegroup]))
825+ self.assertThat(dns_update_zones, MockCalledOnceWith([node.nodegroup]))
826
827 def test_release_logs_and_raises_errors_in_stopping(self):
828 node = factory.make_Node(status=NODE_STATUS.DEPLOYED)
829@@ -2424,12 +2424,12 @@
830 user = factory.make_User()
831 node = self.make_acquired_node_with_mac(user)
832
833- change_dns_zones = self.patch(dns_config, "change_dns_zones")
834+ dns_update_zones = self.patch(dns_config, "dns_update_zones")
835
836 node.start(user)
837
838 self.assertThat(
839- change_dns_zones, MockCalledOnceWith(node.nodegroup))
840+ dns_update_zones, MockCalledOnceWith(node.nodegroup))
841
842 def test__starts_nodes(self):
843 user = factory.make_User()
844
845=== modified file 'src/maasserver/start_up.py'
846--- src/maasserver/start_up.py 2014-10-10 13:35:47 +0000
847+++ src/maasserver/start_up.py 2015-01-13 18:36:01 +0000
848@@ -26,7 +26,7 @@
849 security,
850 )
851 from maasserver.bootresources import ensure_boot_source_definition
852-from maasserver.dns.config import write_full_dns_config
853+from maasserver.dns.config import dns_update_all_zones
854 from maasserver.fields import register_mac_type
855 from maasserver.models import NodeGroup
856 from provisioningserver.upgrade_cluster import create_gnupg_home
857@@ -77,4 +77,4 @@
858 ensure_boot_source_definition()
859
860 # Regenerate MAAS's DNS configuration. This should be reentrant, really.
861- write_full_dns_config(reload_retry=True)
862+ dns_update_all_zones(reload_retry=True)
863
864=== modified file 'src/maasserver/tests/test_commands_write_dns_config.py'
865--- src/maasserver/tests/test_commands_write_dns_config.py 2014-09-23 21:51:24 +0000
866+++ src/maasserver/tests/test_commands_write_dns_config.py 2015-01-13 18:36:01 +0000
867@@ -26,7 +26,7 @@
868 from maasserver.testing.factory import factory
869 from maasserver.testing.testcase import MAASServerTestCase
870 from netaddr import IPNetwork
871-from provisioningserver import tasks
872+from provisioningserver.dns import actions
873 from provisioningserver.dns.testing import patch_dns_config_path
874 from testtools.matchers import FileExists
875
876@@ -37,8 +37,8 @@
877 dns_conf_dir = self.make_dir()
878 patch_dns_config_path(self, dns_conf_dir)
879 self.patch(settings, 'DNS_CONNECT', True)
880- # Prevent rndc task dispatch.
881- self.patch(tasks, "rndc_command")
882+ # Prevent rndc execution.
883+ self.patch(actions, "execute_rndc_command")
884 domain = factory.make_string()
885 factory.make_NodeGroup(
886 name=domain,
887
888=== modified file 'src/maasserver/tests/test_forms_network.py'
889--- src/maasserver/tests/test_forms_network.py 2014-12-11 15:14:32 +0000
890+++ src/maasserver/tests/test_forms_network.py 2015-01-13 18:36:01 +0000
891@@ -181,8 +181,8 @@
892 form.errors)
893
894 def test_writes_dns_when_network_edited(self):
895- write_full_dns_config = self.patch(
896- dns_config_module, "write_full_dns_config")
897+ dns_update_all_zones = self.patch(
898+ dns_config_module, "dns_update_all_zones")
899 network = factory.make_ipv4_network()
900 name = factory.make_name('network')
901 definition = {
902@@ -194,14 +194,14 @@
903 }
904 form = NetworkForm(data=definition)
905 form.save()
906- self.assertThat(write_full_dns_config, MockCalledOnceWith())
907+ self.assertThat(dns_update_all_zones, MockCalledOnceWith())
908
909 def test_writes_dns_when_network_deleted(self):
910 network = factory.make_Network()
911- write_full_dns_config = self.patch(
912- dns_config_module, "write_full_dns_config")
913+ dns_update_all_zones = self.patch(
914+ dns_config_module, "dns_update_all_zones")
915 network.delete()
916- self.assertThat(write_full_dns_config, MockCalledOnceWith())
917+ self.assertThat(dns_update_all_zones, MockCalledOnceWith())
918
919
920 class TestCreateNetworkFromNodeGroupInterface(MAASServerTestCase):
921
922=== modified file 'src/maasserver/tests/test_start_up.py'
923--- src/maasserver/tests/test_start_up.py 2014-10-20 21:11:45 +0000
924+++ src/maasserver/tests/test_start_up.py 2015-01-13 18:36:01 +0000
925@@ -77,7 +77,7 @@
926
927 def test__calls_write_full_dns_config(self):
928 recorder = FakeMethod()
929- self.patch(start_up, 'write_full_dns_config', recorder)
930+ self.patch(start_up, 'dns_update_all_zones', recorder)
931 start_up.inner_start_up()
932 self.assertEqual(
933 (1, [()]),
934
935=== removed file 'src/maastesting/celery.py'
936--- src/maastesting/celery.py 2014-09-29 15:42:42 +0000
937+++ src/maastesting/celery.py 1970-01-01 00:00:00 +0000
938@@ -1,84 +0,0 @@
939-# Copyright 2012 Canonical Ltd. This software is licensed under the
940-# GNU Affero General Public License version 3 (see the file LICENSE).
941-
942-"""A fixture to make Celery run tasks in a synchronous fashion."""
943-
944-from __future__ import (
945- absolute_import,
946- print_function,
947- unicode_literals,
948- )
949-
950-str = None
951-
952-__metaclass__ = type
953-__all__ = [
954- 'CeleryFixture',
955- ]
956-
957-from celery import (
958- current_app,
959- signals,
960- )
961-from fixtures import Fixture
962-from testtools.monkey import MonkeyPatcher
963-
964-
965-class CeleryFixture(Fixture):
966- """This fixture will record Celery tasks as they're run.
967-
968- This fixture can be used directly::
969-
970- >>> from unittest import TestCase
971-
972- >>> class CeleryTest1(TestCase):
973- ...
974- ... def setUp(self):
975- ... super(CeleryTest1, self).setUp()
976- ... self.useFixture(CeleryFixture())
977-
978- It can also be converted into a FixtureResource::
979-
980- >>> from testresources import FixtureResource
981-
982- >>> class CeleryTest2(TestCase):
983- ...
984- ... resources = (
985- ... ("celery", FixtureResource(CeleryFixture())),
986- ... )
987- ...
988-
989- """
990-
991- def setUp(self):
992- super(CeleryFixture, self).setUp()
993- self.configure()
994- self.record_tasks()
995-
996- def configure(self):
997- patcher = MonkeyPatcher()
998- patcher.add_patch(current_app.conf, 'CELERY_ALWAYS_EAGER', True)
999- patcher.add_patch(
1000- current_app.conf, 'CELERY_EAGER_PROPAGATES_EXCEPTIONS', True)
1001- self.addCleanup(patcher.restore)
1002- patcher.patch()
1003-
1004- def record_tasks(self):
1005- self.tasks = []
1006-
1007- def on_task_postrun(**kwargs):
1008- self.tasks.append(kwargs)
1009- signals.task_postrun.connect(on_task_postrun, weak=False)
1010- self.addCleanup(lambda: self.cleanup_tasks())
1011-
1012- def get_task_routing(self):
1013- """Get a mapping between the name of the tasks and the queue they
1014- were sent to.
1015- """
1016- return {
1017- task_info['task'].name: task_info['task'].queue
1018- for task_info in self.tasks
1019- }
1020-
1021- def cleanup_tasks(self):
1022- self.tasks = []
1023
1024=== removed file 'src/maastesting/tests/test_celery.py'
1025--- src/maastesting/tests/test_celery.py 2014-09-24 10:12:35 +0000
1026+++ src/maastesting/tests/test_celery.py 1970-01-01 00:00:00 +0000
1027@@ -1,67 +0,0 @@
1028-# Copyright 2012, 2013 Canonical Ltd. This software is licensed under the
1029-# GNU Affero General Public License version 3 (see the file LICENSE).
1030-
1031-"""Test matchers."""
1032-
1033-from __future__ import (
1034- absolute_import,
1035- print_function,
1036- unicode_literals,
1037- )
1038-
1039-str = None
1040-
1041-__metaclass__ = type
1042-__all__ = []
1043-
1044-import random
1045-
1046-from celery.decorators import task
1047-from celery.result import EagerResult
1048-from maastesting.celery import CeleryFixture
1049-from maastesting.testcase import MAASTestCase
1050-
1051-
1052-@task()
1053-def task_add(x, y):
1054- return x + y
1055-
1056-
1057-@task()
1058-def task_exception(x, y):
1059- raise RuntimeError()
1060-
1061-
1062-class TestCeleryFixture(MAASTestCase):
1063- """Tests `CeleryFixture`."""
1064-
1065- def setUp(self):
1066- super(TestCeleryFixture, self).setUp()
1067- self.celery = self.useFixture(CeleryFixture())
1068-
1069- def test_celery_eagerresult_contains_result(self):
1070- # The result is an instance of EagerResult and it contains the actual
1071- # result.
1072- x = random.randrange(100)
1073- y = random.randrange(100)
1074- result = task_add.delay(x, y)
1075- self.assertIsInstance(result, EagerResult)
1076- self.assertEqual(x + y, result.result)
1077-
1078- def test_celery_exception_raised(self):
1079- self.assertRaises(RuntimeError, task_exception.delay, 1, 2)
1080-
1081- def test_celery_records_tasks(self):
1082- x = random.randrange(100)
1083- y = random.randrange(100)
1084- task_add.delay(x=x, y=y)
1085- z = random.randrange(100)
1086- t = random.randrange(100)
1087- task_add.delay(x=z, y=t)
1088- tasks = self.celery.tasks
1089- self.assertEqual(2, len(tasks))
1090- self.assertEqual(
1091- ['maastesting.tests.test_celery.task_add'] * 2,
1092- [task['task'].name for task in tasks])
1093- self.assertEqual({'x': x, 'y': y}, tasks[0]['kwargs'])
1094- self.assertEqual({'x': z, 'y': t}, tasks[1]['kwargs'])
1095
1096=== added file 'src/provisioningserver/dns/actions.py'
1097--- src/provisioningserver/dns/actions.py 1970-01-01 00:00:00 +0000
1098+++ src/provisioningserver/dns/actions.py 2015-01-13 18:36:01 +0000
1099@@ -0,0 +1,142 @@
1100+# Copyright 2014 Canonical Ltd. This software is licensed under the
1101+# GNU Affero General Public License version 3 (see the file LICENSE).
1102+
1103+"""Low-level actions to manage the DNS service, like reloading zones."""
1104+
1105+from __future__ import (
1106+ absolute_import,
1107+ print_function,
1108+ unicode_literals,
1109+ )
1110+
1111+str = None
1112+
1113+__metaclass__ = type
1114+__all__ = [
1115+ "bind_reconfigure",
1116+ "bind_reload",
1117+ "bind_reload_zone",
1118+ "bind_write_configuration",
1119+ "bind_write_options",
1120+ "bind_write_zones",
1121+]
1122+
1123+import collections
1124+from subprocess import CalledProcessError
1125+from time import sleep
1126+
1127+from provisioningserver.dns.config import (
1128+ DNSConfig,
1129+ execute_rndc_command,
1130+ set_up_options_conf,
1131+ )
1132+from provisioningserver.logger import get_maas_logger
1133+from provisioningserver.utils.shell import ExternalProcessError
1134+
1135+
1136+maaslog = get_maas_logger("dns")
1137+
1138+
1139+def bind_reconfigure():
1140+ """Ask BIND to reload its configuration and *new* zone files.
1141+
1142+ From rndc(8):
1143+
1144+ Reload the configuration file and load new zones, but do not reload
1145+ existing zone files even if they have changed. This is faster than a
1146+ full reload when there is a large number of zones because it avoids the
1147+ need to examine the modification times of the zones files.
1148+
1149+ """
1150+ try:
1151+ execute_rndc_command(("reconfig",))
1152+ except CalledProcessError as exc:
1153+ maaslog.error("Reloading BIND configuration failed: %s", exc)
1154+ # Log before upgrade so that the output does not go to maaslog.
1155+ ExternalProcessError.upgrade(exc)
1156+ raise
1157+
1158+
1159+def bind_reload():
1160+ """Ask BIND to reload its configuration and all zone files."""
1161+ try:
1162+ execute_rndc_command(("reload",))
1163+ except CalledProcessError as exc:
1164+ maaslog.error("Reloading BIND failed: %s", exc)
1165+ # Log before upgrade so that the output does not go to maaslog.
1166+ ExternalProcessError.upgrade(exc)
1167+ raise
1168+
1169+
1170+def bind_reload_with_retries(attempts=10, interval=2):
1171+ """Ask BIND to reload its configuration and all zone files.
1172+
1173+ :param attempts: The number of attempts.
1174+ :param interval: The time in seconds to sleep between each attempt.
1175+ """
1176+ for countdown in xrange(attempts - 1, -1, -1):
1177+ try:
1178+ bind_reload()
1179+ except CalledProcessError:
1180+ if countdown == 0:
1181+ raise
1182+ else:
1183+ sleep(interval)
1184+ else:
1185+ break
1186+
1187+
1188+def bind_reload_zone(zone_name):
1189+ """Ask BIND to reload the zone file for the given zone.
1190+
1191+ :param zone_name: The name of the zone to reload.
1192+ """
1193+ try:
1194+ execute_rndc_command(("reload", zone_name))
1195+ except CalledProcessError as exc:
1196+ maaslog.error("Reloading BIND zone %r failed: %s", zone_name, exc)
1197+ # Log before upgrade so that the output does not go to maaslog.
1198+ ExternalProcessError.upgrade(exc)
1199+ raise
1200+
1201+
1202+def bind_write_configuration(zones, trusted_networks):
1203+ """Write BIND's configuration.
1204+
1205+ :param zones: Those zones to include in main config.
1206+ :type zones: Sequence of :py:class:`DNSZoneData`.
1207+
1208+ :param trusted_networks: A sequence of CIDR network specifications that
1209+ are permitted to use the DNS server as a forwarder.
1210+ """
1211+ # trusted_networks was formerly specified as a single IP address with
1212+ # netmask. These assertions are here to prevent code that assumes that
1213+ # slipping through.
1214+ assert not isinstance(trusted_networks, (bytes, unicode))
1215+ assert isinstance(trusted_networks, collections.Sequence)
1216+
1217+ dns_config = DNSConfig(zones=zones)
1218+ dns_config.write_config(trusted_networks=trusted_networks)
1219+
1220+
1221+def bind_write_options(upstream_dns):
1222+ """Write BIND options.
1223+
1224+ :param upstream_dns: A sequence of upstream DNS servers.
1225+ """
1226+ # upstream_dns was formerly specified as a single IP address. These
1227+ # assertions are here to prevent code that assumes that slipping through.
1228+ assert not isinstance(upstream_dns, (bytes, unicode))
1229+ assert isinstance(upstream_dns, collections.Sequence)
1230+
1231+ set_up_options_conf(upstream_dns=upstream_dns)
1232+
1233+
1234+def bind_write_zones(zones):
1235+ """Write out DNS zones.
1236+
1237+ :param zones: Those zones to write.
1238+ :type zones: Sequence of :py:class:`DNSZoneData`.
1239+ """
1240+ for zone in zones:
1241+ zone.write_config()
1242
1243=== added file 'src/provisioningserver/dns/tests/test_actions.py'
1244--- src/provisioningserver/dns/tests/test_actions.py 1970-01-01 00:00:00 +0000
1245+++ src/provisioningserver/dns/tests/test_actions.py 2015-01-13 18:36:01 +0000
1246@@ -0,0 +1,271 @@
1247+# Copyright 2014 Canonical Ltd. This software is licensed under the
1248+# GNU Affero General Public License version 3 (see the file LICENSE).
1249+
1250+"""Tests for :py:module:`provisioningserver.dns.actions`."""
1251+
1252+from __future__ import (
1253+ absolute_import,
1254+ print_function,
1255+ unicode_literals,
1256+ )
1257+
1258+str = None
1259+
1260+__metaclass__ = type
1261+__all__ = []
1262+
1263+import os
1264+from os.path import join
1265+import random
1266+from random import randint
1267+from subprocess import CalledProcessError
1268+from textwrap import dedent
1269+
1270+from fixtures import FakeLogger
1271+from maastesting.factory import factory
1272+from maastesting.matchers import (
1273+ MockCalledOnceWith,
1274+ MockCallsMatch,
1275+ )
1276+from maastesting.testcase import MAASTestCase
1277+from mock import (
1278+ call,
1279+ sentinel,
1280+ )
1281+from netaddr import IPNetwork
1282+from provisioningserver.dns import actions
1283+from provisioningserver.dns.config import (
1284+ MAAS_NAMED_CONF_NAME,
1285+ MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME,
1286+ )
1287+from provisioningserver.dns.testing import patch_dns_config_path
1288+from provisioningserver.dns.zoneconfig import (
1289+ DNSForwardZoneConfig,
1290+ DNSReverseZoneConfig,
1291+ )
1292+from provisioningserver.testing.testcase import PservTestCase
1293+from provisioningserver.utils.shell import ExternalProcessError
1294+from testtools.matchers import (
1295+ AllMatch,
1296+ Contains,
1297+ FileContains,
1298+ FileExists,
1299+ )
1300+
1301+
1302+class TestReconfigure(MAASTestCase):
1303+ """Tests for :py:func:`actions.bind_reconfigure`."""
1304+
1305+ def test__executes_rndc_command(self):
1306+ self.patch_autospec(actions, "execute_rndc_command")
1307+ actions.bind_reconfigure()
1308+ self.assertThat(
1309+ actions.execute_rndc_command,
1310+ MockCalledOnceWith(("reconfig",)))
1311+
1312+ def test__logs_subprocess_error(self):
1313+ erc = self.patch_autospec(actions, "execute_rndc_command")
1314+ erc.side_effect = factory.make_CalledProcessError()
1315+ with FakeLogger("maas") as logger:
1316+ self.assertRaises(CalledProcessError, actions.bind_reconfigure)
1317+ self.assertDocTestMatches(
1318+ "Reloading BIND configuration failed: "
1319+ "Command ... returned non-zero exit status ...",
1320+ logger.output)
1321+
1322+ def test__upgrades_subprocess_error(self):
1323+ erc = self.patch_autospec(actions, "execute_rndc_command")
1324+ erc.side_effect = factory.make_CalledProcessError()
1325+ self.assertRaises(ExternalProcessError, actions.bind_reconfigure)
1326+
1327+
1328+class TestReload(MAASTestCase):
1329+ """Tests for :py:func:`actions.bind_reload`."""
1330+
1331+ def test__executes_rndc_command(self):
1332+ self.patch_autospec(actions, "execute_rndc_command")
1333+ actions.bind_reload()
1334+ self.assertThat(
1335+ actions.execute_rndc_command,
1336+ MockCalledOnceWith(("reload",)))
1337+
1338+ def test__logs_subprocess_error(self):
1339+ erc = self.patch_autospec(actions, "execute_rndc_command")
1340+ erc.side_effect = factory.make_CalledProcessError()
1341+ with FakeLogger("maas") as logger:
1342+ self.assertRaises(CalledProcessError, actions.bind_reload)
1343+ self.assertDocTestMatches(
1344+ "Reloading BIND failed: "
1345+ "Command ... returned non-zero exit status ...",
1346+ logger.output)
1347+
1348+ def test__upgrades_subprocess_error(self):
1349+ erc = self.patch_autospec(actions, "execute_rndc_command")
1350+ erc.side_effect = factory.make_CalledProcessError()
1351+ self.assertRaises(ExternalProcessError, actions.bind_reload)
1352+
1353+
1354+class TestReloadWithRetries(MAASTestCase):
1355+ """Tests for :py:func:`actions.bind_reload_with_retries`."""
1356+
1357+ def test__calls_bind_reload_count_times(self):
1358+ self.patch_autospec(actions, "sleep") # Disable.
1359+ bind_reload = self.patch_autospec(actions, "bind_reload")
1360+ bind_reload.side_effect = factory.make_CalledProcessError()
1361+ attempts = randint(3, 13)
1362+ self.assertRaises(
1363+ CalledProcessError, actions.bind_reload_with_retries,
1364+ attempts=attempts)
1365+ expected_calls = [call()] * attempts
1366+ self.assertThat(
1367+ actions.bind_reload,
1368+ MockCallsMatch(*expected_calls))
1369+
1370+ def test__returns_on_success(self):
1371+ self.patch_autospec(actions, "sleep") # Disable.
1372+ bind_reload = self.patch(actions, "bind_reload")
1373+ bind_reload.side_effect = [
1374+ factory.make_CalledProcessError(),
1375+ factory.make_CalledProcessError(),
1376+ None, # Success
1377+ ]
1378+ actions.bind_reload_with_retries(attempts=5)
1379+ expected_calls = [call(), call(), call()]
1380+ self.assertThat(
1381+ actions.bind_reload,
1382+ MockCallsMatch(*expected_calls))
1383+
1384+ def test__sleeps_interval_seconds_between_attempts(self):
1385+ self.patch_autospec(actions, "sleep") # Disable.
1386+ bind_reload = self.patch_autospec(actions, "bind_reload")
1387+ bind_reload.side_effect = factory.make_CalledProcessError()
1388+ attempts = randint(3, 13)
1389+ self.assertRaises(
1390+ CalledProcessError, actions.bind_reload_with_retries,
1391+ attempts=attempts, interval=sentinel.interval)
1392+ expected_sleep_calls = [call(sentinel.interval)] * (attempts - 1)
1393+ self.assertThat(actions.sleep, MockCallsMatch(*expected_sleep_calls))
1394+
1395+
1396+class TestReloadZone(MAASTestCase):
1397+ """Tests for :py:func:`actions.bind_reload_zone`."""
1398+
1399+ def test__executes_rndc_command(self):
1400+ self.patch_autospec(actions, "execute_rndc_command")
1401+ actions.bind_reload_zone(sentinel.zone)
1402+ self.assertThat(
1403+ actions.execute_rndc_command,
1404+ MockCalledOnceWith(("reload", sentinel.zone)))
1405+
1406+ def test__logs_subprocess_error(self):
1407+ erc = self.patch_autospec(actions, "execute_rndc_command")
1408+ erc.side_effect = factory.make_CalledProcessError()
1409+ with FakeLogger("maas") as logger:
1410+ self.assertRaises(
1411+ CalledProcessError, actions.bind_reload_zone, sentinel.zone)
1412+ self.assertDocTestMatches(
1413+ "Reloading BIND zone ... failed: "
1414+ "Command ... returned non-zero exit status ...",
1415+ logger.output)
1416+
1417+ def test__upgrades_subprocess_error(self):
1418+ erc = self.patch_autospec(actions, "execute_rndc_command")
1419+ erc.side_effect = factory.make_CalledProcessError()
1420+ self.assertRaises(
1421+ ExternalProcessError, actions.bind_reload_zone, sentinel.zone)
1422+
1423+
1424+class TestConfiguration(PservTestCase):
1425+ """Tests for the `bind_write_*` functions."""
1426+
1427+ def setUp(self):
1428+ super(TestConfiguration, self).setUp()
1429+ # Ensure that files are written to a temporary directory.
1430+ self.dns_conf_dir = self.make_dir()
1431+ patch_dns_config_path(self, self.dns_conf_dir)
1432+ # Patch out calls to 'execute_rndc_command'.
1433+ self.patch_autospec(actions, 'execute_rndc_command')
1434+
1435+ def test_bind_write_configuration_writes_file(self):
1436+ domain = factory.make_string()
1437+ zones = [
1438+ DNSReverseZoneConfig(
1439+ domain, serial=random.randint(1, 100),
1440+ network=factory.make_ipv4_network()),
1441+ DNSReverseZoneConfig(
1442+ domain, serial=random.randint(1, 100),
1443+ network=factory.make_ipv6_network()),
1444+ ]
1445+ actions.bind_write_configuration(
1446+ zones=zones, trusted_networks=[])
1447+ self.assertThat(
1448+ os.path.join(self.dns_conf_dir, MAAS_NAMED_CONF_NAME),
1449+ FileExists())
1450+
1451+ def test_bind_write_configuration_writes_file_with_acl(self):
1452+ trusted_networks = [
1453+ factory.make_ipv4_network(),
1454+ factory.make_ipv6_network(),
1455+ ]
1456+ actions.bind_write_configuration(
1457+ zones=[], trusted_networks=trusted_networks)
1458+ expected_file = os.path.join(self.dns_conf_dir, MAAS_NAMED_CONF_NAME)
1459+ self.assertThat(expected_file, FileExists())
1460+ expected_content = dedent("""\
1461+ acl "trusted" {
1462+ %s;
1463+ %s;
1464+ localnets;
1465+ localhost;
1466+ };
1467+ """)
1468+ expected_content %= tuple(trusted_networks)
1469+ self.assertThat(expected_file, FileContains(
1470+ matcher=Contains(expected_content)))
1471+
1472+ def test_bind_write_zones_writes_file(self):
1473+ domain = factory.make_string()
1474+ network = IPNetwork('192.168.0.3/24')
1475+ dns_ip = factory.pick_ip_in_network(network)
1476+ ip = factory.pick_ip_in_network(network)
1477+ forward_zone = DNSForwardZoneConfig(
1478+ domain, serial=random.randint(1, 100),
1479+ mapping={factory.make_string(): [ip]},
1480+ dns_ip=dns_ip)
1481+ reverse_zone = DNSReverseZoneConfig(
1482+ domain, serial=random.randint(1, 100), network=network)
1483+ actions.bind_write_zones(zones=[forward_zone, reverse_zone])
1484+
1485+ forward_file_name = 'zone.%s' % domain
1486+ reverse_file_name = 'zone.0.168.192.in-addr.arpa'
1487+ expected_files = [
1488+ join(self.dns_conf_dir, forward_file_name),
1489+ join(self.dns_conf_dir, reverse_file_name),
1490+ ]
1491+ self.assertThat(expected_files, AllMatch(FileExists()))
1492+
1493+ def test_bind_write_options_sets_up_config(self):
1494+ # bind_write_configuration_and_zones writes the config file, writes
1495+ # the zone files, and reloads the dns service.
1496+ upstream_dns = [
1497+ factory.make_ipv4_address(),
1498+ factory.make_ipv4_address(),
1499+ ]
1500+ actions.bind_write_options(upstream_dns=upstream_dns)
1501+ expected_options_file = join(
1502+ self.dns_conf_dir, MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME)
1503+ self.assertThat(expected_options_file, FileExists())
1504+ expected_options_content = dedent("""\
1505+ forwarders {
1506+ %s;
1507+ %s;
1508+ };
1509+
1510+ allow-query { any; };
1511+ allow-recursion { trusted; };
1512+ allow-query-cache { trusted; };
1513+ """)
1514+ expected_options_content %= tuple(upstream_dns)
1515+ self.assertThat(
1516+ expected_options_file,
1517+ FileContains(expected_options_content))
1518
1519=== modified file 'src/provisioningserver/dns/tests/test_config.py'
1520--- src/provisioningserver/dns/tests/test_config.py 2014-11-17 11:02:09 +0000
1521+++ src/provisioningserver/dns/tests/test_config.py 2015-01-13 18:36:01 +0000
1522@@ -133,13 +133,14 @@
1523
1524 def test_set_up_options_conf_writes_configuration(self):
1525 dns_conf_dir = patch_dns_config_path(self)
1526- fake_dns = factory.make_ipv4_address()
1527+ fake_dns = [factory.make_ipv4_address(), factory.make_ipv4_address()]
1528 set_up_options_conf(upstream_dns=fake_dns)
1529 target_file = os.path.join(
1530 dns_conf_dir, MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME)
1531 self.assertThat(
1532- target_file,
1533- FileContains(matcher=Contains(fake_dns)))
1534+ target_file, MatchesAll(*(
1535+ FileContains(matcher=Contains(address))
1536+ for address in fake_dns)))
1537
1538 def test_set_up_options_conf_handles_no_upstream_dns(self):
1539 dns_conf_dir = patch_dns_config_path(self)
1540
1541=== removed file 'src/provisioningserver/logger/utils.py'
1542--- src/provisioningserver/logger/utils.py 2014-09-17 08:51:31 +0000
1543+++ src/provisioningserver/logger/utils.py 1970-01-01 00:00:00 +0000
1544@@ -1,47 +0,0 @@
1545-# Copyright 2014 Canonical Ltd. This software is licensed under the
1546-# GNU Affero General Public License version 3 (see the file LICENSE).
1547-
1548-"""Utilities for logging."""
1549-
1550-from __future__ import (
1551- absolute_import,
1552- print_function,
1553- unicode_literals,
1554- )
1555-
1556-str = None
1557-
1558-__metaclass__ = type
1559-__all__ = [
1560- "log_call",
1561-]
1562-
1563-from functools import wraps
1564-import logging
1565-
1566-from provisioningserver.logger.log import get_maas_logger
1567-
1568-
1569-maaslog = get_maas_logger("calls")
1570-
1571-
1572-def log_call(level=logging.INFO):
1573- """Log to the maaslog that something happened with a task.
1574-
1575- :param event: The event that we want to log.
1576- :param task_name: The name of the task.
1577- :**kwargs: A dict of args passed to the task.
1578- """
1579- def _decorator(func):
1580- @wraps(func)
1581- def wrapper(*args, **kwargs):
1582- arg_string = "%s %s" % (args, kwargs)
1583- maaslog.log(
1584- level, "Starting task '%s' with args: %s" %
1585- (func.__name__, arg_string))
1586- func(*args, **kwargs)
1587- maaslog.log(
1588- level, "Finished task '%s' with args: %s" %
1589- (func.__name__, arg_string))
1590- return wrapper
1591- return _decorator
1592
1593=== modified file 'src/provisioningserver/rpc/clusterservice.py'
1594--- src/provisioningserver/rpc/clusterservice.py 2014-12-17 20:55:55 +0000
1595+++ src/provisioningserver/rpc/clusterservice.py 2015-01-13 18:36:01 +0000
1596@@ -18,7 +18,6 @@
1597
1598 from functools import partial
1599 import json
1600-import logging
1601 from os import urandom
1602 import random
1603 import re
1604@@ -42,7 +41,6 @@
1605 from provisioningserver.drivers.hardware.ucsm import probe_and_enlist_ucsm
1606 from provisioningserver.drivers.hardware.virsh import probe_virsh_and_enlist
1607 from provisioningserver.logger.log import get_maas_logger
1608-from provisioningserver.logger.utils import log_call
1609 from provisioningserver.network import discover_networks
1610 from provisioningserver.rpc import (
1611 cluster,
1612@@ -254,7 +252,6 @@
1613 nameservers=nameservers, netmasks=netmasks),
1614 }
1615
1616- @log_call(level=logging.DEBUG)
1617 @cluster.PowerOn.responder
1618 def power_on(self, system_id, hostname, power_type, context):
1619 """Turn a node on."""
1620@@ -264,7 +261,6 @@
1621 d.addCallback(lambda _: {})
1622 return d
1623
1624- @log_call(level=logging.DEBUG)
1625 @cluster.PowerOff.responder
1626 def power_off(self, system_id, hostname, power_type, context):
1627 """Turn a node off."""
1628
1629=== removed file 'src/provisioningserver/tasks.py'
1630--- src/provisioningserver/tasks.py 2014-11-14 17:18:10 +0000
1631+++ src/provisioningserver/tasks.py 1970-01-01 00:00:00 +0000
1632@@ -1,153 +0,0 @@
1633-# Copyright 2012-2014 Canonical Ltd. This software is licensed under the
1634-# GNU Affero General Public License version 3 (see the file LICENSE).
1635-
1636-"""Provisioning server tasks that are run in Celery workers."""
1637-
1638-from __future__ import (
1639- absolute_import,
1640- print_function,
1641- unicode_literals,
1642- )
1643-
1644-str = None
1645-
1646-__metaclass__ = type
1647-__all__ = [
1648- 'rndc_command',
1649- 'write_dns_config',
1650- 'write_dns_zone_config',
1651- 'write_full_dns_config',
1652- ]
1653-
1654-from functools import wraps
1655-from subprocess import CalledProcessError
1656-
1657-from celery.app import app_or_default
1658-from celery.task import task
1659-from provisioningserver.dns.config import (
1660- DNSConfig,
1661- execute_rndc_command,
1662- set_up_options_conf,
1663- )
1664-from provisioningserver.logger import get_maas_logger
1665-from provisioningserver.logger.utils import log_call
1666-
1667-
1668-celery_config = app_or_default().conf
1669-
1670-maaslog = get_maas_logger("tasks")
1671-
1672-
1673-# The tasks catch bare exceptions in an attempt to circumvent Celery's
1674-# bizarre exception handling which prints a stack trace but not the
1675-# error message contained in the exception itself! The message is
1676-# printed and then the exception re-raised so that it marks the task as
1677-# failed - in doing so it logs the stack trace, which is why the code
1678-# does not do a simple maaslog.exception(exc).
1679-def log_exception_text(func):
1680- """Wrap a function and log any exception text raised."""
1681- @wraps(func)
1682- def wrapper(*args, **kwargs):
1683- try:
1684- func(*args, **kwargs)
1685- except Exception as e:
1686- maaslog.error("%s: %s", func.__name__, unicode(e))
1687- raise
1688- return wrapper
1689-
1690-
1691-# =====================================================================
1692-# DNS-related tasks
1693-# =====================================================================
1694-
1695-# How many times should a rndc task be retried?
1696-RNDC_COMMAND_MAX_RETRY = 10
1697-
1698-# How long to wait between rndc tasks retries (in seconds)?
1699-RNDC_COMMAND_RETRY_DELAY = 2
1700-
1701-
1702-@task(max_retries=RNDC_COMMAND_MAX_RETRY)
1703-@log_call()
1704-@log_exception_text
1705-def rndc_command(arguments, retry=False, callback=None):
1706- """Use rndc to execute a command.
1707- :param arguments: Argument list passed down to the rndc command.
1708- :type arguments : list
1709- :param retry: Should this task be retried in case of failure?
1710- :type retry: bool
1711- :param callback: Callback subtask.
1712- :type callback: callable
1713- """
1714- try:
1715- execute_rndc_command(arguments)
1716- except CalledProcessError as exc:
1717- if retry:
1718- return rndc_command.retry(
1719- exc=exc, countdown=RNDC_COMMAND_RETRY_DELAY)
1720- else:
1721- maaslog.error("rndc_command failed: %s", unicode(exc))
1722- raise
1723- if callback is not None:
1724- callback.delay()
1725-
1726-
1727-@task
1728-@log_call()
1729-@log_exception_text
1730-def write_full_dns_config(zones=None, callback=None, **kwargs):
1731- """Write out the DNS configuration files: the main configuration
1732- file and the zone files.
1733- :param zones: List of zones to write.
1734- :type zones: list of :class:`DNSZoneData`
1735- :param callback: Callback subtask.
1736- :type callback: callable
1737- :param **kwargs: Keyword args passed to DNSConfig.write_config()
1738- """
1739- if zones is not None:
1740- for zone in zones:
1741- zone.write_config()
1742- # Write main config file.
1743- dns_config = DNSConfig(zones=zones)
1744- dns_config.write_config(**kwargs)
1745- # Write the included options file.
1746- set_up_options_conf(**kwargs)
1747- if callback is not None:
1748- callback.delay()
1749-
1750-
1751-@task
1752-@log_call()
1753-@log_exception_text
1754-def write_dns_config(zones=(), callback=None, **kwargs):
1755- """Write out the DNS configuration file.
1756-
1757- :param zones: List of zones to include as part of the main
1758- config.
1759- :type zones: list of :class:`DNSZoneData`
1760- :param callback: Callback subtask.
1761- :type callback: callable
1762- :param **kwargs: Keyword args passed to DNSConfig.write_config()
1763- """
1764- dns_config = DNSConfig(zones=zones)
1765- dns_config.write_config(**kwargs)
1766- if callback is not None:
1767- callback.delay()
1768-
1769-
1770-@task
1771-@log_call()
1772-@log_exception_text
1773-def write_dns_zone_config(zones, callback=None, **kwargs):
1774- """Write out DNS zones.
1775-
1776- :param zone: The zone data to write the configuration for.
1777- :type zone: :class:`DNSZoneData`
1778- :param callback: Callback subtask.
1779- :type callback: callable
1780- :param **kwargs: Keyword args passed to DNSZoneConfig.write_config()
1781- """
1782- for zone in zones:
1783- zone.write_config()
1784- if callback is not None:
1785- callback.delay()
1786
1787=== removed file 'src/provisioningserver/tests/test_tasks.py'
1788--- src/provisioningserver/tests/test_tasks.py 2014-11-14 17:18:10 +0000
1789+++ src/provisioningserver/tests/test_tasks.py 1970-01-01 00:00:00 +0000
1790@@ -1,227 +0,0 @@
1791-# Copyright 2012-2014 Canonical Ltd. This software is licensed under the
1792-# GNU Affero General Public License version 3 (see the file LICENSE).
1793-
1794-"""Tests for Celery tasks."""
1795-
1796-from __future__ import (
1797- absolute_import,
1798- print_function,
1799- unicode_literals,
1800- )
1801-
1802-str = None
1803-
1804-__metaclass__ = type
1805-__all__ = []
1806-
1807-import os
1808-import random
1809-from subprocess import CalledProcessError
1810-
1811-import celery
1812-from celery import states
1813-from maastesting.celery import CeleryFixture
1814-from maastesting.factory import factory
1815-from maastesting.fakemethod import (
1816- FakeMethod,
1817- MultiFakeMethod,
1818- )
1819-from netaddr import IPNetwork
1820-from provisioningserver import tasks
1821-from provisioningserver.dns.config import (
1822- MAAS_NAMED_CONF_NAME,
1823- MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME,
1824- )
1825-from provisioningserver.dns.testing import patch_dns_config_path
1826-from provisioningserver.dns.zoneconfig import (
1827- DNSForwardZoneConfig,
1828- DNSReverseZoneConfig,
1829- )
1830-from provisioningserver.tasks import (
1831- rndc_command,
1832- RNDC_COMMAND_MAX_RETRY,
1833- write_dns_config,
1834- write_dns_zone_config,
1835- write_full_dns_config,
1836- )
1837-from provisioningserver.testing.testcase import PservTestCase
1838-from provisioningserver.utils.shell import ExternalProcessError
1839-from testtools.matchers import (
1840- Equals,
1841- FileExists,
1842- MatchesListwise,
1843- )
1844-
1845-
1846-def assertTaskRetried(runner, result, nb_retries, task_name):
1847- # In celery version 2.5 (in Saucy) a retried tasks that eventually
1848- # succeeds comes out in a 'SUCCESS' state and in 3.1 (in Trusty) is comes
1849- # out with a 'RETRY' state.
1850- # In both cases the task is successfully retried.
1851- if celery.VERSION[0] == 2:
1852- runner.assertTrue(result.successful())
1853- else:
1854- runner.assertEqual(
1855- len(runner.celery.tasks), nb_retries)
1856- last_task = runner.celery.tasks[0]
1857- # The last task succeeded.
1858- runner.assertEqual(
1859- (last_task['task'].name, last_task['state']),
1860- (task_name, states.SUCCESS))
1861-
1862-
1863-class TestDNSTasks(PservTestCase):
1864-
1865- def setUp(self):
1866- super(TestDNSTasks, self).setUp()
1867- # Patch DNS_CONFIG_DIR so that the configuration files will be
1868- # written in a temporary directory.
1869- self.dns_conf_dir = self.make_dir()
1870- patch_dns_config_path(self, self.dns_conf_dir)
1871- # Record the calls to 'execute_rndc_command' (instead of
1872- # executing real rndc commands).
1873- self.rndc_recorder = FakeMethod()
1874- self.patch(tasks, 'execute_rndc_command', self.rndc_recorder)
1875- self.celery = self.useFixture(CeleryFixture())
1876-
1877- def test_write_dns_config_writes_file(self):
1878- zone_names = [random.randint(1, 100), random.randint(1, 100)]
1879- command = factory.make_string()
1880- result = write_dns_config.delay(
1881- zone_names=zone_names,
1882- callback=rndc_command.subtask(args=[command]))
1883-
1884- self.assertThat(
1885- (
1886- result.successful(),
1887- os.path.join(self.dns_conf_dir, MAAS_NAMED_CONF_NAME),
1888- self.rndc_recorder.calls,
1889- ),
1890- MatchesListwise(
1891- (
1892- Equals(True),
1893- FileExists(),
1894- Equals([((command,), {})]),
1895- )),
1896- result)
1897-
1898- def test_write_dns_zone_config_writes_file(self):
1899- command = factory.make_string()
1900- domain = factory.make_string()
1901- network = IPNetwork('192.168.0.3/24')
1902- dns_ip = factory.pick_ip_in_network(network)
1903- ip = factory.pick_ip_in_network(network)
1904- forward_zone = DNSForwardZoneConfig(
1905- domain, serial=random.randint(1, 100),
1906- mapping={factory.make_string(): [ip]},
1907- dns_ip=dns_ip)
1908- reverse_zone = DNSReverseZoneConfig(
1909- domain, serial=random.randint(1, 100), network=network)
1910- result = write_dns_zone_config.delay(
1911- zones=[forward_zone, reverse_zone],
1912- callback=rndc_command.subtask(args=[command]))
1913-
1914- forward_file_name = 'zone.%s' % domain
1915- reverse_file_name = 'zone.0.168.192.in-addr.arpa'
1916- self.assertThat(
1917- (
1918- result.successful(),
1919- os.path.join(self.dns_conf_dir, forward_file_name),
1920- os.path.join(self.dns_conf_dir, reverse_file_name),
1921- self.rndc_recorder.calls,
1922- ),
1923- MatchesListwise(
1924- (
1925- Equals(True),
1926- FileExists(),
1927- FileExists(),
1928- Equals([((command, ), {})]),
1929- )),
1930- result)
1931-
1932- def test_rndc_command_execute_command(self):
1933- command = factory.make_string()
1934- result = rndc_command.delay(command)
1935-
1936- self.assertThat(
1937- (result.successful(), self.rndc_recorder.calls),
1938- MatchesListwise(
1939- (
1940- Equals(True),
1941- Equals([((command,), {})]),
1942- )))
1943-
1944- def test_rndc_command_can_be_retried(self):
1945- # The rndc_command task can be retried.
1946- # Simulate a temporary failure.
1947- number_of_failures = RNDC_COMMAND_MAX_RETRY
1948- raised_exception = CalledProcessError(
1949- factory.make_name('exception'), random.randint(100, 200))
1950- simulate_failures = MultiFakeMethod(
1951- [FakeMethod(failure=raised_exception)] * number_of_failures +
1952- [FakeMethod()])
1953- self.patch(tasks, 'execute_rndc_command', simulate_failures)
1954- command = factory.make_string()
1955- result = rndc_command.delay(command, retry=True)
1956- assertTaskRetried(
1957- self, result, RNDC_COMMAND_MAX_RETRY + 1,
1958- 'provisioningserver.tasks.rndc_command')
1959-
1960- def test_rndc_command_is_retried_a_limited_number_of_times(self):
1961- # If we simulate RNDC_COMMAND_MAX_RETRY + 1 failures, the
1962- # task fails.
1963- number_of_failures = RNDC_COMMAND_MAX_RETRY + 1
1964- raised_exception = ExternalProcessError(
1965- random.randint(100, 200), factory.make_name('exception'))
1966- simulate_failures = MultiFakeMethod(
1967- [FakeMethod(failure=raised_exception)] * number_of_failures +
1968- [FakeMethod()])
1969- self.patch(tasks, 'execute_rndc_command', simulate_failures)
1970- command = factory.make_string()
1971- self.assertRaises(
1972- ExternalProcessError, rndc_command.delay,
1973- command, retry=True)
1974-
1975- def test_write_full_dns_config_sets_up_config(self):
1976- # write_full_dns_config writes the config file, writes
1977- # the zone files, and reloads the dns service.
1978- domain = factory.make_string()
1979- network = IPNetwork('192.168.0.3/24')
1980- ip = factory.pick_ip_in_network(network)
1981- dns_ip = factory.pick_ip_in_network(network)
1982- zones = [
1983- DNSForwardZoneConfig(
1984- domain, serial=random.randint(1, 100),
1985- mapping={factory.make_string(): [ip]},
1986- dns_ip=dns_ip,
1987- ),
1988- DNSReverseZoneConfig(
1989- domain, serial=random.randint(1, 100), network=network),
1990- ]
1991- command = factory.make_string()
1992- result = write_full_dns_config.delay(
1993- zones=zones,
1994- callback=rndc_command.subtask(args=[command]),
1995- upstream_dns=factory.make_ipv4_address())
1996-
1997- forward_file_name = 'zone.%s' % domain
1998- reverse_file_name = 'zone.0.168.192.in-addr.arpa'
1999- self.assertThat(
2000- (
2001- result.successful(),
2002- self.rndc_recorder.calls,
2003- os.path.join(self.dns_conf_dir, forward_file_name),
2004- os.path.join(self.dns_conf_dir, reverse_file_name),
2005- os.path.join(self.dns_conf_dir, MAAS_NAMED_CONF_NAME),
2006- os.path.join(
2007- self.dns_conf_dir, MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME),
2008- ),
2009- MatchesListwise(
2010- (
2011- Equals(True),
2012- Equals([((command,), {})]),
2013- FileExists(),
2014- FileExists(),
2015- FileExists(),
2016- FileExists(),
2017- )))