Merge lp:~rvb/maas/1.2-bug-1058998-hostnames into lp:maas/1.2

Proposed by Raphaël Badin
Status: Merged
Approved by: Raphaël Badin
Approved revision: no longer in the source branch.
Merged at revision: 1302
Proposed branch: lp:~rvb/maas/1.2-bug-1058998-hostnames
Merge into: lp:maas/1.2
Diff against target: 244 lines (+91/-47)
4 files modified
src/maasserver/forms.py (+1/-1)
src/maasserver/models/node.py (+37/-10)
src/maasserver/tests/test_api.py (+6/-3)
src/maasserver/tests/test_node.py (+47/-33)
To merge this branch: bzr merge lp:~rvb/maas/1.2-bug-1058998-hostnames
Reviewer Review Type Date Requested Status
Raphaël Badin (community) Approve
Review via email: mp+134620@code.launchpad.net

Commit message

Backport of maas r 1330

Description of the change

To post a comment you must log in.
Revision history for this message
Raphaël Badin (rvb) wrote :

Self-approving this, it's a simple backport.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/forms.py'
2--- src/maasserver/forms.py 2012-11-09 18:12:36 +0000
3+++ src/maasserver/forms.py 2012-11-16 08:17:26 +0000
4@@ -403,7 +403,7 @@
5 hostname == "" or
6 IP_BASED_HOSTNAME_REGEXP.match(stripped_hostname) != None)
7 if generate_hostname:
8- node.set_mac_based_hostname(self.cleaned_data['mac_addresses'][0])
9+ node.set_random_hostname()
10 return node
11
12
13
14=== modified file 'src/maasserver/models/node.py'
15--- src/maasserver/models/node.py 2012-11-08 09:14:58 +0000
16+++ src/maasserver/models/node.py 2012-11-16 08:17:26 +0000
17@@ -18,8 +18,14 @@
18 "update_hardware_details",
19 ]
20
21+from itertools import (
22+ imap,
23+ islice,
24+ repeat,
25+ )
26 import math
27 import os
28+import random
29 from string import whitespace
30 from uuid import uuid1
31
32@@ -397,6 +403,16 @@
33 node.save()
34
35
36+# Non-ambiguous characters (i.e. without 'ilousvz1250').
37+non_ambiguous_characters = imap(
38+ random.choice, repeat('abcdefghjkmnpqrtwxy346789'))
39+
40+
41+def generate_hostname(size):
42+ """Generate a hostname using only non-ambiguous characters."""
43+ return "".join(islice(non_ambiguous_characters, size))
44+
45+
46 class Node(CleanSave, TimestampedModel):
47 """A `Node` represents a physical machine used by the MAAS Server.
48
49@@ -655,19 +671,30 @@
50
51 super(Node, self).delete()
52
53- def set_mac_based_hostname(self, mac_address):
54- """Set default `hostname` based on `mac_address`
55-
56- The hostname will include the `enlistment_domain` if set.
57+ def set_random_hostname(self):
58+ """Set 5 character `hostname` using non-ambiguous characters.
59+
60+ Using 5 letters from the set 'abcdefghjkmnpqrtwxy346789' we get
61+ 9,765,625 combinations (pow(25, 5)).
62+
63+ Note that having a hostname starting with a number is perfectly
64+ valid, see
65+ http://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
66 """
67- mac_hostname = mac_address.replace(':', '').lower()
68 domain = Config.objects.get_config("enlistment_domain")
69 domain = domain.strip("." + whitespace)
70- if len(domain) > 0:
71- self.hostname = "node-%s.%s" % (mac_hostname, domain)
72- else:
73- self.hostname = "node-%s" % mac_hostname
74- self.save()
75+ while True:
76+ new_hostname = generate_hostname(5)
77+ if len(domain) > 0:
78+ self.hostname = "%s.%s" % (new_hostname, domain)
79+ else:
80+ self.hostname = "%s" % new_hostname
81+ try:
82+ self.save()
83+ except ValidationError:
84+ pass
85+ else:
86+ break
87
88 def get_effective_power_type(self):
89 """Get power-type to use for this node.
90
91=== modified file 'src/maasserver/tests/test_api.py'
92--- src/maasserver/tests/test_api.py 2012-11-08 09:54:37 +0000
93+++ src/maasserver/tests/test_api.py 2012-11-16 08:17:26 +0000
94@@ -106,7 +106,10 @@
95 TestCase,
96 )
97 from maasserver.tests.test_forms import make_interface_settings
98-from maasserver.utils import map_enum
99+from maasserver.utils import (
100+ map_enum,
101+ strip_domain,
102+ )
103 from maasserver.utils.orm import get_one
104 from maasserver.worker_user import get_worker_user
105 from maastesting.celery import CeleryFixture
106@@ -555,11 +558,11 @@
107 {
108 'op': 'new',
109 'architecture': architecture,
110- 'mac_addresses': ['aa:BB:cc:dd:ee:ff', '22:bb:cc:dd:ee:ff'],
111+ 'mac_addresses': [factory.getRandomMACAddress()],
112 })
113 node = Node.objects.get(
114 system_id=json.loads(response.content)['system_id'])
115- self.assertEqual('node-aabbccddeeff.local', node.hostname)
116+ self.assertEqual(5, len(strip_domain(node.hostname)))
117
118 def test_POST_fails_without_operation(self):
119 # If there is no operation ('op=operation_name') specified in the
120
121=== modified file 'src/maasserver/tests/test_node.py'
122--- src/maasserver/tests/test_node.py 2012-11-08 09:14:58 +0000
123+++ src/maasserver/tests/test_node.py 2012-11-16 08:17:26 +0000
124@@ -13,6 +13,7 @@
125 __all__ = []
126
127 from datetime import timedelta
128+import random
129
130 from django.conf import settings
131 from django.core.exceptions import (
132@@ -36,7 +37,10 @@
133 Node,
134 node as node_module,
135 )
136-from maasserver.models.node import NODE_TRANSITIONS
137+from maasserver.models.node import (
138+ generate_hostname,
139+ NODE_TRANSITIONS,
140+ )
141 from maasserver.models.user import create_auth_token
142 from maasserver.testing import reload_object
143 from maasserver.testing.factory import factory
144@@ -45,6 +49,7 @@
145 ignore_unused,
146 map_enum,
147 )
148+from maastesting.testcase import TestCase as DjangoLessTestCase
149 from metadataserver.models import (
150 NodeCommissionResult,
151 NodeUserData,
152@@ -52,12 +57,34 @@
153 from provisioningserver.enum import POWER_TYPE
154 from provisioningserver.power.poweraction import PowerAction
155 from testtools.matchers import (
156+ AllMatch,
157+ Contains,
158 Equals,
159 FileContains,
160+ MatchesAll,
161 MatchesListwise,
162+ Not,
163 )
164
165
166+class UtilitiesTest(DjangoLessTestCase):
167+
168+ def test_generate_hostname_does_not_contain_ambiguous_chars(self):
169+ ambiguous_chars = 'ilousvz1250'
170+ hostnames = [generate_hostname(5) for i in range(200)]
171+ does_not_contain_chars_matcher = (
172+ MatchesAll(*[Not(Contains(char)) for char in ambiguous_chars]))
173+ self.assertThat(
174+ hostnames, AllMatch(does_not_contain_chars_matcher))
175+
176+ def test_generate_hostname_uses_size(self):
177+ sizes = [
178+ random.randint(1, 10), random.randint(1, 10),
179+ random.randint(1, 10)]
180+ hostnames = [generate_hostname(size) for size in sizes]
181+ self.assertEqual(sizes, [len(hostname) for hostname in hostnames])
182+
183+
184 class NodeTest(TestCase):
185
186 def test_system_id(self):
187@@ -182,38 +209,25 @@
188 node.delete()
189 self.assertEqual(2, mocked_apply_async.call_count)
190
191- def test_set_mac_based_hostname_default_enlistment_domain(self):
192- # The enlistment domain defaults to `local`.
193- node = factory.make_node()
194- node.set_mac_based_hostname('AA:BB:CC:DD:EE:FF')
195- hostname = 'node-aabbccddeeff.local'
196- self.assertEqual(hostname, node.hostname)
197-
198- def test_set_mac_based_hostname_alt_enlistment_domain(self):
199- # A non-default enlistment domain can be specified.
200- Config.objects.set_config("enlistment_domain", "example.com")
201- node = factory.make_node()
202- node.set_mac_based_hostname('AA:BB:CC:DD:EE:FF')
203- hostname = 'node-aabbccddeeff.example.com'
204- self.assertEqual(hostname, node.hostname)
205-
206- def test_set_mac_based_hostname_cleaning_enlistment_domain(self):
207- # Leading and trailing dots and whitespace are cleaned from the
208- # configured enlistment domain before it's joined to the hostname.
209- Config.objects.set_config("enlistment_domain", " .example.com. ")
210- node = factory.make_node()
211- node.set_mac_based_hostname('AA:BB:CC:DD:EE:FF')
212- hostname = 'node-aabbccddeeff.example.com'
213- self.assertEqual(hostname, node.hostname)
214-
215- def test_set_mac_based_hostname_no_enlistment_domain(self):
216- # The enlistment domain can be set to the empty string and
217- # set_mac_based_hostname sets a hostname with no domain.
218- Config.objects.set_config("enlistment_domain", "")
219- node = factory.make_node()
220- node.set_mac_based_hostname('AA:BB:CC:DD:EE:FF')
221- hostname = 'node-aabbccddeeff'
222- self.assertEqual(hostname, node.hostname)
223+ def test_set_random_hostname_set_hostname(self):
224+ # Blank out enlistment_domain.
225+ Config.objects.set_config("enlistment_domain", '')
226+ node = factory.make_node('test' * 10)
227+ node.set_random_hostname()
228+ self.assertEqual(5, len(node.hostname))
229+
230+ def test_set_random_hostname_checks_hostname_existence(self):
231+ Config.objects.set_config("enlistment_domain", '')
232+ existing_node = factory.make_node(hostname='hostname')
233+
234+ hostnames = [existing_node.hostname, "new_hostname"]
235+ self.patch(
236+ node_module, "generate_hostname",
237+ lambda size: hostnames.pop(0))
238+
239+ node = factory.make_node()
240+ node.set_random_hostname()
241+ self.assertEqual('new_hostname', node.hostname)
242
243 def test_get_effective_power_type_defaults_to_config(self):
244 power_types = list(map_enum(POWER_TYPE).values())

Subscribers

People subscribed via source and target branches

to status/vote changes: