Merge lp:~julian-edwards/maas/dns-in-networks into lp:~maas-committers/maas/trunk

Proposed by Julian Edwards
Status: Merged
Approved by: Julian Edwards
Approved revision: no longer in the source branch.
Merged at revision: 2922
Proposed branch: lp:~julian-edwards/maas/dns-in-networks
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 552 lines (+406/-4)
9 files modified
src/maasserver/api/networks.py (+2/-1)
src/maasserver/api/tests/test_network.py (+3/-0)
src/maasserver/api/tests/test_networks.py (+3/-1)
src/maasserver/forms.py (+1/-0)
src/maasserver/migrations/0109_networks_dns_servers.py (+377/-0)
src/maasserver/models/network.py (+5/-0)
src/maasserver/templates/maasserver/network_detail.html (+4/-0)
src/maasserver/testing/factory.py (+8/-2)
src/maasserver/views/tests/test_networks.py (+3/-0)
To merge this branch: bzr merge lp:~julian-edwards/maas/dns-in-networks
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+233464@code.launchpad.net

Commit message

Add dns_servers as a field in Networks. It's purely book-keeping for the cloud installer so nothing special is done with it here.

Description of the change

The UI changes are to put it in the Network detail view only; the listing view is already quite wide and this is not strictly a network property.

To post a comment you must log in.
Revision history for this message
Julian Edwards (julian-edwards) wrote :

Don't be afraid of the size of the branch, most of it is the migration.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

About time to line-break that ‘fields’ tuple in networks.py properly, I think...

I'd be happy to know I was worried about nothing, but will this new field support both empty strings and nulls? It seems like a recipe for confusion, e.g. if we start doing things like “if no DNS servers have been set on this network that we manage, substitute the MAAS DNS server.” Does that go for nulls, or for blanks as well? Which do you get in API results? Probably worth being clear about.

Personally I would say “space-separated” rather than “space separated”; avoids grammatical ambiguity early on.

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

Thanks for reviewing!

On Friday 05 September 2014 08:06:48 you wrote:
> Review: Approve
>
> About time to line-break that ‘fields’ tuple in networks.py properly, I
> think...

I can't see anything wrong with it.

> I'd be happy to know I was worried about nothing, but will this new field
> support both empty strings and nulls? It seems like a recipe for
> confusion, e.g. if we start doing things like “if no DNS servers have been
> set on this network that we manage, substitute the MAAS DNS server.” Does
> that go for nulls, or for blanks as well? Which do you get in API results?
> Probably worth being clear about.

AFAIK it stores blanks as NULL with those field settings.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/networks.py'
2--- src/maasserver/api/networks.py 2014-09-05 01:36:31 +0000
3+++ src/maasserver/api/networks.py 2014-09-05 07:17:23 +0000
4@@ -46,7 +46,8 @@
5
6 model = Network
7 fields = (
8- 'name', 'ip', 'netmask', 'vlan_tag', 'description', 'default_gateway')
9+ 'name', 'ip', 'netmask', 'vlan_tag', 'description', 'default_gateway',
10+ 'dns_servers')
11
12 # Creation happens on the NetworksHandler.
13 create = None
14
15=== modified file 'src/maasserver/api/tests/test_network.py'
16--- src/maasserver/api/tests/test_network.py 2014-09-05 06:48:30 +0000
17+++ src/maasserver/api/tests/test_network.py 2014-09-05 07:17:23 +0000
18@@ -56,6 +56,7 @@
19 network.vlan_tag,
20 network.description,
21 network.default_gateway,
22+ network.dns_servers,
23 ),
24 (
25 parsed_result['name'],
26@@ -64,6 +65,7 @@
27 parsed_result['vlan_tag'],
28 parsed_result['description'],
29 parsed_result['default_gateway'],
30+ parsed_result['dns_servers'],
31 ))
32
33 def test_GET_returns_404_for_unknown_network(self):
34@@ -82,6 +84,7 @@
35 'vlan_tag': factory.make_vlan_tag(),
36 'description': "Changed description",
37 'default_gateway': factory.getRandomIPAddress(),
38+ 'dns_servers': factory.getRandomIPAddress(),
39 }
40
41 response = self.client_put(self.get_url(network.name), new_values)
42
43=== modified file 'src/maasserver/api/tests/test_networks.py'
44--- src/maasserver/api/tests/test_networks.py 2014-09-05 06:48:30 +0000
45+++ src/maasserver/api/tests/test_networks.py 2014-09-05 07:17:23 +0000
46@@ -38,6 +38,8 @@
47 'netmask': '%s' % net.netmask,
48 'vlan_tag': factory.make_vlan_tag(),
49 'description': factory.make_string(),
50+ 'default_gateway': factory.getRandomIPAddress(),
51+ 'dns_servers': factory.getRandomIPAddress(),
52 }
53 response = self.client.post(reverse('networks_handler'), params)
54 self.assertEqual(httplib.OK, response.status_code)
55@@ -63,7 +65,7 @@
56 [returned_network] = parsed_result
57 fields = {
58 'name', 'ip', 'netmask', 'vlan_tag', 'description',
59- 'default_gateway'}
60+ 'default_gateway', 'dns_servers'}
61 self.assertEqual(
62 fields.union({'resource_uri'}),
63 set(returned_network.keys()))
64
65=== modified file 'src/maasserver/forms.py'
66--- src/maasserver/forms.py 2014-09-05 01:32:07 +0000
67+++ src/maasserver/forms.py 2014-09-05 07:17:23 +0000
68@@ -1915,6 +1915,7 @@
69 'netmask',
70 'vlan_tag',
71 'default_gateway',
72+ 'dns_servers',
73 )
74
75 mac_addresses = NodeMACAddressChoiceField(
76
77=== added file 'src/maasserver/migrations/0109_networks_dns_servers.py'
78--- src/maasserver/migrations/0109_networks_dns_servers.py 1970-01-01 00:00:00 +0000
79+++ src/maasserver/migrations/0109_networks_dns_servers.py 2014-09-05 07:17:23 +0000
80@@ -0,0 +1,377 @@
81+from django.db import models
82+from south.db import db
83+# -*- coding: utf-8 -*-
84+from south.utils import datetime_utils as datetime
85+from south.v2 import SchemaMigration
86+
87+
88+class Migration(SchemaMigration):
89+
90+ def forwards(self, orm):
91+ # Adding field 'Network.dns_servers'
92+ db.add_column(u'maasserver_network', 'dns_servers',
93+ self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
94+ keep_default=False)
95+
96+
97+ def backwards(self, orm):
98+ # Deleting field 'Network.dns_servers'
99+ db.delete_column(u'maasserver_network', 'dns_servers')
100+
101+
102+ models = {
103+ u'auth.group': {
104+ 'Meta': {'object_name': 'Group'},
105+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
106+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
107+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
108+ },
109+ u'auth.permission': {
110+ 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
111+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
112+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
113+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
114+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
115+ },
116+ u'auth.user': {
117+ 'Meta': {'object_name': 'User'},
118+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
119+ 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
120+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
121+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
122+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
123+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
124+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
125+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
126+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
127+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
128+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
129+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
130+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
131+ },
132+ u'contenttypes.contenttype': {
133+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
134+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
135+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
136+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
137+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
138+ },
139+ u'maasserver.bootimage': {
140+ 'Meta': {'unique_together': "((u'nodegroup', u'osystem', u'architecture', u'subarchitecture', u'release', u'purpose', u'label'),)", 'object_name': 'BootImage'},
141+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
142+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
143+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
144+ 'label': ('django.db.models.fields.CharField', [], {'default': "u'release'", 'max_length': '255'}),
145+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
146+ 'osystem': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
147+ 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
148+ 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
149+ 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
150+ 'supported_subarches': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
151+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
152+ 'xinstall_path': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
153+ 'xinstall_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '30', 'null': 'True', 'blank': 'True'})
154+ },
155+ u'maasserver.bootresource': {
156+ 'Meta': {'unique_together': "((u'name', u'architecture'),)", 'object_name': 'BootResource'},
157+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
158+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
159+ 'extra': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
160+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
161+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
162+ 'rtype': ('django.db.models.fields.IntegerField', [], {'max_length': '10'}),
163+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
164+ },
165+ u'maasserver.bootresourcefile': {
166+ 'Meta': {'unique_together': "((u'resource_set', u'filetype'),)", 'object_name': 'BootResourceFile'},
167+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
168+ 'extra': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
169+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
170+ 'filetype': ('django.db.models.fields.CharField', [], {'default': "u'root-tgz'", 'max_length': '20'}),
171+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
172+ 'largefile': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.LargeFile']"}),
173+ 'resource_set': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'files'", 'to': u"orm['maasserver.BootResourceSet']"}),
174+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
175+ },
176+ u'maasserver.bootresourceset': {
177+ 'Meta': {'unique_together': "((u'resource', u'version'),)", 'object_name': 'BootResourceSet'},
178+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
179+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
180+ 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
181+ 'resource': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'sets'", 'to': u"orm['maasserver.BootResource']"}),
182+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
183+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '255'})
184+ },
185+ u'maasserver.bootsource': {
186+ 'Meta': {'object_name': 'BootSource'},
187+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
188+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
189+ 'keyring_data': ('maasserver.fields.EditableBinaryField', [], {'blank': 'True'}),
190+ 'keyring_filename': ('django.db.models.fields.FilePathField', [], {'max_length': '100', 'blank': 'True'}),
191+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
192+ 'url': ('django.db.models.fields.URLField', [], {'unique': 'True', 'max_length': '200'})
193+ },
194+ u'maasserver.bootsourceselection': {
195+ 'Meta': {'object_name': 'BootSourceSelection'},
196+ 'arches': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
197+ 'boot_source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.BootSource']"}),
198+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
199+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
200+ 'labels': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
201+ 'os': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
202+ 'release': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
203+ 'subarches': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
204+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
205+ },
206+ u'maasserver.candidatename': {
207+ 'Meta': {'unique_together': "((u'name', u'position'),)", 'object_name': 'CandidateName'},
208+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
209+ 'name': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
210+ 'position': ('django.db.models.fields.IntegerField', [], {})
211+ },
212+ u'maasserver.componenterror': {
213+ 'Meta': {'object_name': 'ComponentError'},
214+ 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
215+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
216+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
217+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
218+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
219+ },
220+ u'maasserver.config': {
221+ 'Meta': {'object_name': 'Config'},
222+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
223+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
224+ 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
225+ },
226+ u'maasserver.dhcplease': {
227+ 'Meta': {'object_name': 'DHCPLease'},
228+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
229+ 'ip': ('maasserver.fields.MAASIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
230+ 'mac': ('maasserver.fields.MACAddressField', [], {}),
231+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
232+ },
233+ u'maasserver.downloadprogress': {
234+ 'Meta': {'object_name': 'DownloadProgress'},
235+ 'bytes_downloaded': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
236+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
237+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'blank': 'True'}),
238+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
239+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
240+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
241+ 'size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
242+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
243+ },
244+ u'maasserver.event': {
245+ 'Meta': {'object_name': 'Event'},
246+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
247+ 'description': ('django.db.models.fields.TextField', [], {'default': "u''", 'blank': 'True'}),
248+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
249+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
250+ 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.EventType']"}),
251+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
252+ },
253+ u'maasserver.eventtype': {
254+ 'Meta': {'object_name': 'EventType'},
255+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
256+ 'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
257+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
258+ 'level': ('django.db.models.fields.IntegerField', [], {}),
259+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
260+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
261+ },
262+ u'maasserver.filestorage': {
263+ 'Meta': {'unique_together': "((u'filename', u'owner'),)", 'object_name': 'FileStorage'},
264+ 'content': ('metadataserver.fields.BinaryField', [], {'blank': 'True'}),
265+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
266+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
267+ 'key': ('django.db.models.fields.CharField', [], {'default': "u'35907f12-34bb-11e4-bb2b-002215205ce8'", 'unique': 'True', 'max_length': '36'}),
268+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
269+ },
270+ u'maasserver.largefile': {
271+ 'Meta': {'object_name': 'LargeFile'},
272+ 'content': ('maasserver.fields.LargeObjectField', [], {}),
273+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
274+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
275+ 'sha256': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}),
276+ 'total_size': ('django.db.models.fields.BigIntegerField', [], {}),
277+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
278+ },
279+ u'maasserver.licensekey': {
280+ 'Meta': {'unique_together': "((u'osystem', u'distro_series'),)", 'object_name': 'LicenseKey'},
281+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
282+ 'distro_series': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
283+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
284+ 'license_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
285+ 'osystem': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
286+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
287+ },
288+ u'maasserver.macaddress': {
289+ 'Meta': {'ordering': "(u'created',)", 'object_name': 'MACAddress'},
290+ 'cluster_interface': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['maasserver.NodeGroupInterface']", 'null': 'True', 'blank': 'True'}),
291+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
292+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
293+ 'ip_addresses': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.StaticIPAddress']", 'symmetrical': 'False', 'through': u"orm['maasserver.MACStaticIPAddressLink']", 'blank': 'True'}),
294+ 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
295+ 'networks': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Network']", 'symmetrical': 'False', 'blank': 'True'}),
296+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
297+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
298+ },
299+ u'maasserver.macstaticipaddresslink': {
300+ 'Meta': {'unique_together': "((u'ip_address', u'mac_address'),)", 'object_name': 'MACStaticIPAddressLink'},
301+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
302+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
303+ 'ip_address': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.StaticIPAddress']", 'unique': 'True'}),
304+ 'mac_address': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.MACAddress']"}),
305+ 'nic_alias': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
306+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
307+ },
308+ u'maasserver.network': {
309+ 'Meta': {'object_name': 'Network'},
310+ 'default_gateway': ('maasserver.fields.MAASIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
311+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
312+ 'dns_servers': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
313+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
314+ 'ip': ('maasserver.fields.MAASIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
315+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
316+ 'netmask': ('maasserver.fields.MAASIPAddressField', [], {'max_length': '39'}),
317+ 'vlan_tag': ('django.db.models.fields.PositiveSmallIntegerField', [], {'unique': 'True', 'null': 'True', 'blank': 'True'})
318+ },
319+ u'maasserver.node': {
320+ 'Meta': {'object_name': 'Node'},
321+ 'agent_name': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
322+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '31'}),
323+ 'boot_type': ('django.db.models.fields.CharField', [], {'default': "u'fastpath'", 'max_length': '20'}),
324+ 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
325+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
326+ 'disable_ipv4': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
327+ 'distro_series': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
328+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
329+ 'error_description': ('django.db.models.fields.TextField', [], {'default': "u''", 'blank': 'True'}),
330+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'unique': 'True', 'max_length': '255', 'blank': 'True'}),
331+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
332+ 'license_key': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}),
333+ 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
334+ 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
335+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
336+ 'osystem': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
337+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
338+ 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
339+ 'power_state': ('django.db.models.fields.CharField', [], {'default': "u'unknown'", 'max_length': '10'}),
340+ 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
341+ 'routers': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'macaddr'", 'null': 'True', 'blank': 'True'}),
342+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
343+ 'storage': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
344+ 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-358bdb4c-34bb-11e4-bb2b-002215205ce8'", 'unique': 'True', 'max_length': '41'}),
345+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
346+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'null': 'True'}),
347+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
348+ 'zone': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Zone']", 'on_delete': 'models.SET_DEFAULT'})
349+ },
350+ u'maasserver.nodegroup': {
351+ 'Meta': {'object_name': 'NodeGroup'},
352+ 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
353+ 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'unique': 'True'}),
354+ 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
355+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
356+ 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
357+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
358+ 'maas_url': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
359+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
360+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
361+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
362+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
363+ },
364+ u'maasserver.nodegroupinterface': {
365+ 'Meta': {'unique_together': "((u'nodegroup', u'name'),)", 'object_name': 'NodeGroupInterface'},
366+ 'broadcast_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
367+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
368+ 'foreign_dhcp_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
369+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
370+ 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
371+ 'ip': ('maasserver.fields.MAASIPAddressField', [], {'max_length': '39'}),
372+ 'ip_range_high': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
373+ 'ip_range_low': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
374+ 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
375+ 'name': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
376+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
377+ 'router_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
378+ 'static_ip_range_high': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
379+ 'static_ip_range_low': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
380+ 'subnet_mask': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
381+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
382+ },
383+ u'maasserver.sshkey': {
384+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
385+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
386+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
387+ 'key': ('django.db.models.fields.TextField', [], {}),
388+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
389+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
390+ },
391+ u'maasserver.sslkey': {
392+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSLKey'},
393+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
394+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
395+ 'key': ('django.db.models.fields.TextField', [], {}),
396+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
397+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
398+ },
399+ u'maasserver.staticipaddress': {
400+ 'Meta': {'object_name': 'StaticIPAddress'},
401+ 'alloc_type': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
402+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
403+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
404+ 'ip': ('maasserver.fields.MAASIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
405+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
406+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
407+ },
408+ u'maasserver.tag': {
409+ 'Meta': {'object_name': 'Tag'},
410+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
411+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
412+ 'definition': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
413+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
414+ 'kernel_opts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
415+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
416+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
417+ },
418+ u'maasserver.userprofile': {
419+ 'Meta': {'object_name': 'UserProfile'},
420+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
421+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
422+ },
423+ u'maasserver.zone': {
424+ 'Meta': {'ordering': "[u'name']", 'object_name': 'Zone'},
425+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
426+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
427+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
428+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
429+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
430+ },
431+ u'piston.consumer': {
432+ 'Meta': {'object_name': 'Consumer'},
433+ 'description': ('django.db.models.fields.TextField', [], {}),
434+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
435+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
436+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
437+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
438+ 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
439+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': u"orm['auth.User']"})
440+ },
441+ u'piston.token': {
442+ 'Meta': {'object_name': 'Token'},
443+ 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
444+ 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
445+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Consumer']"}),
446+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
447+ 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
448+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
449+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
450+ 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1409893937L'}),
451+ 'token_type': ('django.db.models.fields.IntegerField', [], {}),
452+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': u"orm['auth.User']"}),
453+ 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
454+ }
455+ }
456+
457+ complete_apps = ['maasserver']
458\ No newline at end of file
459
460=== modified file 'src/maasserver/models/network.py'
461--- src/maasserver/models/network.py 2014-09-01 01:45:04 +0000
462+++ src/maasserver/models/network.py 2014-09-05 07:17:23 +0000
463@@ -248,6 +248,11 @@
464 blank=True, editable=True, null=True,
465 help_text="Default gateway for this network (e.g. 192.168.1.1).")
466
467+ dns_servers = CharField(
468+ blank=True, editable=True, null=True, max_length=255,
469+ help_text="Space separated list of DNS server addresses that this "
470+ "network may use.")
471+
472 vlan_tag = PositiveSmallIntegerField(
473 editable=True, null=True, blank=True, unique=True,
474 help_text="A 12-bit field specifying the VLAN to which the frame "
475
476=== modified file 'src/maasserver/templates/maasserver/network_detail.html'
477--- src/maasserver/templates/maasserver/network_detail.html 2014-09-05 01:23:06 +0000
478+++ src/maasserver/templates/maasserver/network_detail.html 2014-09-05 07:17:23 +0000
479@@ -44,6 +44,10 @@
480 <span>{{ network.default_gateway|default_if_none:"" }}</span>
481 </li>
482 <li class="block size2">
483+ <h4>DNS servers</h4>
484+ <span>{{ network.dns_servers|default_if_none:"" }}</span>
485+ </li>
486+ <li class="block size2">
487 <h4>Attached nodes</h4>
488 <span id="nodecount">
489 <a title="View nodes attached to network {{ network.name }}"
490
491=== modified file 'src/maasserver/testing/factory.py'
492--- src/maasserver/testing/factory.py 2014-09-05 06:36:56 +0000
493+++ src/maasserver/testing/factory.py 2014-09-05 07:17:23 +0000
494@@ -864,7 +864,8 @@
495
496 def make_Network(self, name=None, network=None, vlan_tag=NO_VALUE,
497 description=None, sortable_name=False,
498- disjoint_from=None, default_gateway=None):
499+ disjoint_from=None, default_gateway=None,
500+ dns_servers=None):
501 """Create a `Network`.
502
503 :param network: An `IPNetwork`. If given, the `ip` and `netmask`
504@@ -899,6 +900,10 @@
505 network = self.getRandomNetwork(disjoint_from=disjoint_from)
506 if default_gateway is None and self.pick_bool():
507 default_gateway = self.pick_ip_in_network(network)
508+ if dns_servers is None and self.pick_bool():
509+ dns_servers = " ".join(
510+ self.getRandomIPAddress()
511+ for _ in range(random.choice((1, 2))))
512 ip = unicode(network.ip)
513 netmask = unicode(network.netmask)
514 if description is None:
515@@ -907,7 +912,8 @@
516 vlan_tag = self.make_vlan_tag()
517 network = Network(
518 name=name, ip=ip, netmask=netmask, vlan_tag=vlan_tag,
519- description=description, default_gateway=default_gateway)
520+ description=description, default_gateway=default_gateway,
521+ dns_servers=dns_servers)
522 network.save()
523 return network
524
525
526=== modified file 'src/maasserver/views/tests/test_networks.py'
527--- src/maasserver/views/tests/test_networks.py 2014-09-05 06:48:30 +0000
528+++ src/maasserver/views/tests/test_networks.py 2014-09-05 07:17:23 +0000
529@@ -200,6 +200,7 @@
530 'netmask': "%s" % network.netmask,
531 'vlan_tag': factory.make_vlan_tag(),
532 'default_gateway': factory.getRandomIPAddress(),
533+ 'dns_servers': factory.getRandomIPAddress(),
534 }
535 response = self.client.post(reverse('network-add'), definition)
536 self.assertEqual(httplib.FOUND, response.status_code)
537@@ -315,6 +316,7 @@
538 factory.make_mac_address()
539 for _ in range(3)]
540 new_gateway = factory.getRandomIPAddress()
541+ new_dns_servers = factory.getRandomIPAddress()
542 response = self.client.post(
543 reverse('network-edit', args=[network.name]),
544 data={
545@@ -322,6 +324,7 @@
546 'description': new_description,
547 'default_gateway': new_gateway,
548 'mac_addresses': new_macs,
549+ 'dns_servers': new_dns_servers,
550 })
551 self.assertEqual(
552 reverse('network-list'), extract_redirect(response),