Merge lp:~jtv/maas/unify-get_name_and_vlan_from_cluster_interface into lp:~maas-committers/maas/trunk
- unify-get_name_and_vlan_from_cluster_interface
- Merge into trunk
Proposed by
Jeroen T. Vermeulen
Status: | Merged |
---|---|
Approved by: | Jeroen T. Vermeulen |
Approved revision: | no longer in the source branch. |
Merged at revision: | 3374 |
Proposed branch: | lp:~jtv/maas/unify-get_name_and_vlan_from_cluster_interface |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
535 lines (+164/-142) 12 files modified
src/maasserver/forms.py (+6/-3) src/maasserver/migrations/0099_convert_cluster_interfaces_to_networks.py (+7/-18) src/maasserver/models/network.py (+0/-16) src/maasserver/models/tests/test_network.py (+0/-39) src/maasserver/testing/factory.py (+0/-23) src/maasserver/testing/tests/test_factory.py (+2/-39) src/maasserver/tests/test_forms_nodegroupinterface.py (+5/-2) src/maasserver/utils/interfaces.py (+19/-0) src/maasserver/utils/tests/test_interfaces.py (+59/-1) src/maastesting/factory.py (+22/-0) src/maastesting/tests/test_factory.py (+20/-0) src/maastesting/utils.py (+24/-1) |
To merge this branch: | bzr merge lp:~jtv/maas/unify-get_name_and_vlan_from_cluster_interface |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gavin Panella (community) | Approve | ||
Review via email: mp+241738@code.launchpad.net |
Commit message
Unify our two implementations of get_name_
Description of the change
This was harder than I expected: there were some other helpers that needed moving. Fixing the actual bug should be easy by comparison.
Along the way I made the function independent of the model, otherwise it wouldn't be advisable to use it in migration code. It was a simple matter of externalising retrieval of two fields, and making those the parameters. But otherwise, nothing substantial changes.
Jeroen
To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote : | # |
Thanks. Made some changes "inspired by" your notes, as they say in Hollywood nowadays. :-)
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 2014-11-13 02:17:06 +0000 | |||
3 | +++ src/maasserver/forms.py 2014-11-14 14:24:50 +0000 | |||
4 | @@ -125,7 +125,6 @@ | |||
5 | 125 | Tag, | 125 | Tag, |
6 | 126 | Zone, | 126 | Zone, |
7 | 127 | ) | 127 | ) |
8 | 128 | from maasserver.models.network import get_name_and_vlan_from_cluster_interface | ||
9 | 129 | from maasserver.models.node import ( | 128 | from maasserver.models.node import ( |
10 | 130 | fqdn_is_duplicate, | 129 | fqdn_is_duplicate, |
11 | 131 | nodegroup_fqdn, | 130 | nodegroup_fqdn, |
12 | @@ -138,7 +137,10 @@ | |||
13 | 138 | ) | 137 | ) |
14 | 139 | from maasserver.utils import strip_domain | 138 | from maasserver.utils import strip_domain |
15 | 140 | from maasserver.utils.forms import compose_invalid_choice_text | 139 | from maasserver.utils.forms import compose_invalid_choice_text |
17 | 141 | from maasserver.utils.interfaces import make_name_from_interface | 140 | from maasserver.utils.interfaces import ( |
18 | 141 | get_name_and_vlan_from_cluster_interface, | ||
19 | 142 | make_name_from_interface, | ||
20 | 143 | ) | ||
21 | 142 | from maasserver.utils.orm import get_one | 144 | from maasserver.utils.orm import get_one |
22 | 143 | from maasserver.utils.osystems import ( | 145 | from maasserver.utils.osystems import ( |
23 | 144 | get_distro_series_initial, | 146 | get_distro_series_initial, |
24 | @@ -1409,7 +1411,8 @@ | |||
25 | 1409 | # Can be None or empty string, do nothing if so. | 1411 | # Can be None or empty string, do nothing if so. |
26 | 1410 | return | 1412 | return |
27 | 1411 | 1413 | ||
29 | 1412 | name, vlan_tag = get_name_and_vlan_from_cluster_interface(interface) | 1414 | name, vlan_tag = get_name_and_vlan_from_cluster_interface( |
30 | 1415 | interface.name, interface.interface) | ||
31 | 1413 | ipnetwork = make_network(interface.ip, interface.subnet_mask) | 1416 | ipnetwork = make_network(interface.ip, interface.subnet_mask) |
32 | 1414 | network = Network( | 1417 | network = Network( |
33 | 1415 | name=name, | 1418 | name=name, |
34 | 1416 | 1419 | ||
35 | === modified file 'src/maasserver/migrations/0099_convert_cluster_interfaces_to_networks.py' | |||
36 | --- src/maasserver/migrations/0099_convert_cluster_interfaces_to_networks.py 2014-08-25 02:47:21 +0000 | |||
37 | +++ src/maasserver/migrations/0099_convert_cluster_interfaces_to_networks.py 2014-11-14 14:24:50 +0000 | |||
38 | @@ -5,28 +5,15 @@ | |||
39 | 5 | transaction, | 5 | transaction, |
40 | 6 | ) | 6 | ) |
41 | 7 | from django.db.utils import IntegrityError | 7 | from django.db.utils import IntegrityError |
42 | 8 | from maasserver.utils.interfaces import ( | ||
43 | 9 | get_name_and_vlan_from_cluster_interface, | ||
44 | 10 | ) | ||
45 | 8 | from netaddr import IPNetwork | 11 | from netaddr import IPNetwork |
46 | 9 | from south.db import db | 12 | from south.db import db |
47 | 10 | from south.utils import datetime_utils as datetime | 13 | from south.utils import datetime_utils as datetime |
48 | 11 | from south.v2 import DataMigration | 14 | from south.v2 import DataMigration |
49 | 12 | 15 | ||
50 | 13 | 16 | ||
51 | 14 | def get_name_and_vlan_from_cluster_interface(cluster_interface): | ||
52 | 15 | """Given a `NodeGroupInterface`, return a name suitable for a `Network`. | ||
53 | 16 | |||
54 | 17 | :return: a tuple of the new name and vlan tag. vlan tag may be None | ||
55 | 18 | """ | ||
56 | 19 | name = cluster_interface.interface | ||
57 | 20 | nodegroup_name = cluster_interface.nodegroup.name | ||
58 | 21 | vlan_tag = None | ||
59 | 22 | if '.' in name: | ||
60 | 23 | _, vlan_tag = name.split('.', 1) | ||
61 | 24 | name = name.replace('.', '-') | ||
62 | 25 | name = name.replace(':', '-') | ||
63 | 26 | network_name = "-".join((nodegroup_name, name)) | ||
64 | 27 | return network_name, vlan_tag | ||
65 | 28 | |||
66 | 29 | |||
67 | 30 | class Migration(DataMigration): | 17 | class Migration(DataMigration): |
68 | 31 | 18 | ||
69 | 32 | def forwards(self, orm): | 19 | def forwards(self, orm): |
70 | @@ -36,8 +23,10 @@ | |||
71 | 36 | # Can be None or empty string, do nothing if so. | 23 | # Can be None or empty string, do nothing if so. |
72 | 37 | continue | 24 | continue |
73 | 38 | 25 | ||
76 | 39 | name, vlan_tag = get_name_and_vlan_from_cluster_interface(interface) | 26 | name, vlan_tag = get_name_and_vlan_from_cluster_interface( |
77 | 40 | ipnetwork = IPNetwork("%s/%s" % (interface.ip, interface.subnet_mask)) | 27 | interface.nodegroup.name, interface.interface) |
78 | 28 | ipnetwork = IPNetwork( | ||
79 | 29 | "%s/%s" % (interface.ip, interface.subnet_mask)) | ||
80 | 41 | network = orm['maasserver.Network']( | 30 | network = orm['maasserver.Network']( |
81 | 42 | name=name, | 31 | name=name, |
82 | 43 | ip=unicode(ipnetwork.network), | 32 | ip=unicode(ipnetwork.network), |
83 | 44 | 33 | ||
84 | === modified file 'src/maasserver/models/network.py' | |||
85 | --- src/maasserver/models/network.py 2014-09-17 09:20:05 +0000 | |||
86 | +++ src/maasserver/models/network.py 2014-11-14 14:24:50 +0000 | |||
87 | @@ -186,22 +186,6 @@ | |||
88 | 186 | return specifier_class(spec) | 186 | return specifier_class(spec) |
89 | 187 | 187 | ||
90 | 188 | 188 | ||
91 | 189 | def get_name_and_vlan_from_cluster_interface(cluster_interface): | ||
92 | 190 | """Given a `NodeGroupInterface`, return a name suitable for a `Network`. | ||
93 | 191 | |||
94 | 192 | :return: a tuple of the new name and vlan tag. vlan tag may be None | ||
95 | 193 | """ | ||
96 | 194 | name = cluster_interface.interface | ||
97 | 195 | nodegroup_name = cluster_interface.nodegroup.name | ||
98 | 196 | vlan_tag = None | ||
99 | 197 | if '.' in name: | ||
100 | 198 | _, vlan_tag = name.split('.', 1) | ||
101 | 199 | name = name.replace('.', '-') | ||
102 | 200 | name = name.replace(':', '-') | ||
103 | 201 | network_name = "-".join((nodegroup_name, name)) | ||
104 | 202 | return network_name, vlan_tag | ||
105 | 203 | |||
106 | 204 | |||
107 | 205 | class NetworkManager(Manager): | 189 | class NetworkManager(Manager): |
108 | 206 | """Manager for :class:`Network` model class. | 190 | """Manager for :class:`Network` model class. |
109 | 207 | 191 | ||
110 | 208 | 192 | ||
111 | === modified file 'src/maasserver/models/tests/test_network.py' | |||
112 | --- src/maasserver/models/tests/test_network.py 2014-09-19 03:12:47 +0000 | |||
113 | +++ src/maasserver/models/tests/test_network.py 2014-11-14 14:24:50 +0000 | |||
114 | @@ -20,7 +20,6 @@ | |||
115 | 20 | from django.db.models.query import QuerySet | 20 | from django.db.models.query import QuerySet |
116 | 21 | from maasserver.models import Network | 21 | from maasserver.models import Network |
117 | 22 | from maasserver.models.network import ( | 22 | from maasserver.models.network import ( |
118 | 23 | get_name_and_vlan_from_cluster_interface, | ||
119 | 24 | get_specifier_type, | 23 | get_specifier_type, |
120 | 25 | IPSpecifier, | 24 | IPSpecifier, |
121 | 26 | NameSpecifier, | 25 | NameSpecifier, |
122 | @@ -30,7 +29,6 @@ | |||
123 | 30 | from maasserver.testing.factory import factory | 29 | from maasserver.testing.factory import factory |
124 | 31 | from maasserver.testing.orm import reload_object | 30 | from maasserver.testing.orm import reload_object |
125 | 32 | from maasserver.testing.testcase import MAASServerTestCase | 31 | from maasserver.testing.testcase import MAASServerTestCase |
126 | 33 | from mock import sentinel | ||
127 | 34 | from netaddr import ( | 32 | from netaddr import ( |
128 | 35 | IPAddress, | 33 | IPAddress, |
129 | 36 | IPNetwork, | 34 | IPNetwork, |
130 | @@ -132,43 +130,6 @@ | |||
131 | 132 | self.assertRaises(ValidationError, parse_network_spec, '10.4.4.4') | 130 | self.assertRaises(ValidationError, parse_network_spec, '10.4.4.4') |
132 | 133 | 131 | ||
133 | 134 | 132 | ||
134 | 135 | class TestGetNameAndVlanFromClusterInterface(MAASServerTestCase): | ||
135 | 136 | """Tests for `get_name_and_vlan_from_cluster_interface`.""" | ||
136 | 137 | |||
137 | 138 | def make_interface_name(self, basename): | ||
138 | 139 | interface = sentinel.interface | ||
139 | 140 | interface.nodegroup = sentinel.nodegroup | ||
140 | 141 | interface.nodegroup.name = factory.make_name('name') | ||
141 | 142 | interface.interface = basename | ||
142 | 143 | return interface | ||
143 | 144 | |||
144 | 145 | def test_returns_simple_name_unaltered(self): | ||
145 | 146 | interface = self.make_interface_name(factory.make_name('iface')) | ||
146 | 147 | name, vlan_tag = get_name_and_vlan_from_cluster_interface(interface) | ||
147 | 148 | expected_name = '%s-%s' % ( | ||
148 | 149 | interface.nodegroup.name, interface.interface) | ||
149 | 150 | self.assertEqual((expected_name, None), (name, vlan_tag)) | ||
150 | 151 | |||
151 | 152 | def test_substitutes_colon(self): | ||
152 | 153 | interface = self.make_interface_name('eth0:0') | ||
153 | 154 | name, vlan_tag = get_name_and_vlan_from_cluster_interface(interface) | ||
154 | 155 | expected_name = '%s-eth0-0' % interface.nodegroup.name | ||
155 | 156 | self.assertEqual((expected_name, None), (name, vlan_tag)) | ||
156 | 157 | |||
157 | 158 | def test_returns_with_vlan_tag(self): | ||
158 | 159 | interface = self.make_interface_name('eth0.5') | ||
159 | 160 | name, vlan_tag = get_name_and_vlan_from_cluster_interface(interface) | ||
160 | 161 | expected_name = '%s-eth0-5' % interface.nodegroup.name | ||
161 | 162 | self.assertEqual((expected_name, '5'), (name, vlan_tag)) | ||
162 | 163 | |||
163 | 164 | def test_returns_name_with_crazy_colon_and_vlan(self): | ||
164 | 165 | # For truly twisted network admins. | ||
165 | 166 | interface = self.make_interface_name('eth0:2.3') | ||
166 | 167 | name, vlan_tag = get_name_and_vlan_from_cluster_interface(interface) | ||
167 | 168 | expected_name = '%s-eth0-2-3' % interface.nodegroup.name | ||
168 | 169 | self.assertEqual((expected_name, '3'), (name, vlan_tag)) | ||
169 | 170 | |||
170 | 171 | |||
171 | 172 | class TestNetworkManager(MAASServerTestCase): | 133 | class TestNetworkManager(MAASServerTestCase): |
172 | 173 | 134 | ||
173 | 174 | def test_get_from_spec_validates_first(self): | 135 | def test_get_from_spec_validates_first(self): |
174 | 175 | 136 | ||
175 | === modified file 'src/maasserver/testing/factory.py' | |||
176 | --- src/maasserver/testing/factory.py 2014-11-13 02:17:06 +0000 | |||
177 | +++ src/maasserver/testing/factory.py 2014-11-14 14:24:50 +0000 | |||
178 | @@ -748,29 +748,6 @@ | |||
179 | 748 | 748 | ||
180 | 749 | make_zone = make_Zone | 749 | make_zone = make_Zone |
181 | 750 | 750 | ||
182 | 751 | def make_vlan_tag(self, allow_none=False, but_not=None): | ||
183 | 752 | """Create a random VLAN tag. | ||
184 | 753 | |||
185 | 754 | :param allow_none: Whether `None` ("no VLAN") can be allowed as an | ||
186 | 755 | outcome. If `True`, `None` will be included in the possible | ||
187 | 756 | results with a deliberately over-represented probability, in order | ||
188 | 757 | to help trip up bugs that might only show up once in about 4094 | ||
189 | 758 | calls otherwise. | ||
190 | 759 | :param but_not: A list of tags that should not be returned. Any zero | ||
191 | 760 | or `None` entries will be ignored. | ||
192 | 761 | """ | ||
193 | 762 | if but_not is None: | ||
194 | 763 | but_not = [] | ||
195 | 764 | if allow_none and random.randint(0, 1) == 0: | ||
196 | 765 | return None | ||
197 | 766 | else: | ||
198 | 767 | for _ in range(100): | ||
199 | 768 | vlan_tag = random.randint(1, 0xffe) | ||
200 | 769 | if vlan_tag not in but_not: | ||
201 | 770 | return vlan_tag | ||
202 | 771 | raise maastesting.factory.TooManyRandomRetries( | ||
203 | 772 | "Could not find an available VLAN tag.") | ||
204 | 773 | |||
205 | 774 | def make_Network(self, name=None, network=None, vlan_tag=NO_VALUE, | 751 | def make_Network(self, name=None, network=None, vlan_tag=NO_VALUE, |
206 | 775 | description=None, sortable_name=False, | 752 | description=None, sortable_name=False, |
207 | 776 | disjoint_from=None, default_gateway=None, | 753 | disjoint_from=None, default_gateway=None, |
208 | 777 | 754 | ||
209 | === modified file 'src/maasserver/testing/tests/test_factory.py' | |||
210 | --- src/maasserver/testing/tests/test_factory.py 2014-09-10 16:20:31 +0000 | |||
211 | +++ src/maasserver/testing/tests/test_factory.py 2014-11-14 14:24:50 +0000 | |||
212 | @@ -24,28 +24,7 @@ | |||
213 | 24 | from maasserver.testing.orm import reload_object | 24 | from maasserver.testing.orm import reload_object |
214 | 25 | from maasserver.testing.testcase import MAASServerTestCase | 25 | from maasserver.testing.testcase import MAASServerTestCase |
215 | 26 | from maastesting.factory import TooManyRandomRetries | 26 | from maastesting.factory import TooManyRandomRetries |
238 | 27 | 27 | from maastesting.utils import FakeRandInt | |
217 | 28 | |||
218 | 29 | class FakeRandInt: | ||
219 | 30 | """Fake `randint` which forced limitations on its range. | ||
220 | 31 | |||
221 | 32 | This lets you set a forced minimum, and/or a forced maximum, on the range | ||
222 | 33 | of any call. For example, if you pass `forced_maximum=3`, then a call | ||
223 | 34 | will never return more than 3. If you don't set a maximum, or if the | ||
224 | 35 | call's maximum argument is less than the forced maximum, then the call's | ||
225 | 36 | maximum will be respected. | ||
226 | 37 | """ | ||
227 | 38 | def __init__(self, real_randint, forced_minimum=None, forced_maximum=None): | ||
228 | 39 | self.real_randint = real_randint | ||
229 | 40 | self.minimum = forced_minimum | ||
230 | 41 | self.maximum = forced_maximum | ||
231 | 42 | |||
232 | 43 | def __call__(self, minimum, maximum): | ||
233 | 44 | if self.minimum is not None: | ||
234 | 45 | minimum = max(minimum, self.minimum) | ||
235 | 46 | if self.maximum is not None: | ||
236 | 47 | maximum = min(maximum, self.maximum) | ||
237 | 48 | return self.real_randint(minimum, maximum) | ||
239 | 49 | 28 | ||
240 | 50 | 29 | ||
241 | 51 | class TestFactory(MAASServerTestCase): | 30 | class TestFactory(MAASServerTestCase): |
242 | @@ -127,23 +106,6 @@ | |||
243 | 127 | node = reload_object(node) | 106 | node = reload_object(node) |
244 | 128 | self.assertEqual(previous_zone, node.zone) | 107 | self.assertEqual(previous_zone, node.zone) |
245 | 129 | 108 | ||
246 | 130 | def test_make_vlan_tag_excludes_None_by_default(self): | ||
247 | 131 | # Artificially limit randint to a very narrow range, to guarantee | ||
248 | 132 | # some repetition in its output, and virtually guarantee that we test | ||
249 | 133 | # both outcomes of the flip-a-coin call in make_vlan_tag. | ||
250 | 134 | self.patch(random, 'randint', FakeRandInt(random.randint, 0, 1)) | ||
251 | 135 | outcomes = {factory.make_vlan_tag() for _ in range(1000)} | ||
252 | 136 | self.assertEqual({1}, outcomes) | ||
253 | 137 | |||
254 | 138 | def test_make_vlan_tags_includes_None_if_allow_none(self): | ||
255 | 139 | self.patch(random, 'randint', FakeRandInt(random.randint, 0, 1)) | ||
256 | 140 | self.assertEqual( | ||
257 | 141 | {None, 1}, | ||
258 | 142 | { | ||
259 | 143 | factory.make_vlan_tag(allow_none=True) | ||
260 | 144 | for _ in range(1000) | ||
261 | 145 | }) | ||
262 | 146 | |||
263 | 147 | def test_make_Networks_lowers_names_if_sortable_name(self): | 109 | def test_make_Networks_lowers_names_if_sortable_name(self): |
264 | 148 | networks = factory.make_Networks(10, sortable_name=True) | 110 | networks = factory.make_Networks(10, sortable_name=True) |
265 | 149 | self.assertEqual( | 111 | self.assertEqual( |
266 | @@ -183,4 +145,5 @@ | |||
267 | 183 | def test_make_Networks_gives_up_if_random_tags_keep_clashing(self): | 145 | def test_make_Networks_gives_up_if_random_tags_keep_clashing(self): |
268 | 184 | self.patch(factory, 'make_Network') | 146 | self.patch(factory, 'make_Network') |
269 | 185 | self.patch(random, 'randint', lambda *args: 1) | 147 | self.patch(random, 'randint', lambda *args: 1) |
270 | 148 | self.patch(factory, 'pick_bool', lambda *args: False) | ||
271 | 186 | self.assertRaises(TooManyRandomRetries, factory.make_Networks, 2) | 149 | self.assertRaises(TooManyRandomRetries, factory.make_Networks, 2) |
272 | 187 | 150 | ||
273 | === modified file 'src/maasserver/tests/test_forms_nodegroupinterface.py' | |||
274 | --- src/maasserver/tests/test_forms_nodegroupinterface.py 2014-10-30 11:08:43 +0000 | |||
275 | +++ src/maasserver/tests/test_forms_nodegroupinterface.py 2014-11-14 14:24:50 +0000 | |||
276 | @@ -29,10 +29,12 @@ | |||
277 | 29 | Network, | 29 | Network, |
278 | 30 | NodeGroupInterface, | 30 | NodeGroupInterface, |
279 | 31 | ) | 31 | ) |
280 | 32 | from maasserver.models.network import get_name_and_vlan_from_cluster_interface | ||
281 | 33 | from maasserver.models.staticipaddress import StaticIPAddress | 32 | from maasserver.models.staticipaddress import StaticIPAddress |
282 | 34 | from maasserver.testing.factory import factory | 33 | from maasserver.testing.factory import factory |
283 | 35 | from maasserver.testing.testcase import MAASServerTestCase | 34 | from maasserver.testing.testcase import MAASServerTestCase |
284 | 35 | from maasserver.utils.interfaces import ( | ||
285 | 36 | get_name_and_vlan_from_cluster_interface, | ||
286 | 37 | ) | ||
287 | 36 | from maastesting.matchers import MockCalledOnceWith | 38 | from maastesting.matchers import MockCalledOnceWith |
288 | 37 | from netaddr import ( | 39 | from netaddr import ( |
289 | 38 | IPAddress, | 40 | IPAddress, |
290 | @@ -369,7 +371,8 @@ | |||
291 | 369 | form = NodeGroupInterfaceForm(data=int_settings, instance=interface) | 371 | form = NodeGroupInterfaceForm(data=int_settings, instance=interface) |
292 | 370 | form.save() | 372 | form.save() |
293 | 371 | [network] = Network.objects.all() | 373 | [network] = Network.objects.all() |
295 | 372 | expected, _ = get_name_and_vlan_from_cluster_interface(interface) | 374 | expected, _ = get_name_and_vlan_from_cluster_interface( |
296 | 375 | interface.name, interface.interface) | ||
297 | 373 | self.assertEqual(expected, network.name) | 376 | self.assertEqual(expected, network.name) |
298 | 374 | 377 | ||
299 | 375 | def test_sets_vlan_tag(self): | 378 | def test_sets_vlan_tag(self): |
300 | 376 | 379 | ||
301 | === modified file 'src/maasserver/utils/interfaces.py' | |||
302 | --- src/maasserver/utils/interfaces.py 2014-08-13 21:49:35 +0000 | |||
303 | +++ src/maasserver/utils/interfaces.py 2014-11-14 14:24:50 +0000 | |||
304 | @@ -13,6 +13,7 @@ | |||
305 | 13 | 13 | ||
306 | 14 | __metaclass__ = type | 14 | __metaclass__ = type |
307 | 15 | __all__ = [ | 15 | __all__ = [ |
308 | 16 | 'get_name_and_vlan_from_cluster_interface', | ||
309 | 16 | 'make_name_from_interface', | 17 | 'make_name_from_interface', |
310 | 17 | ] | 18 | ] |
311 | 18 | 19 | ||
312 | @@ -34,3 +35,21 @@ | |||
313 | 34 | else: | 35 | else: |
314 | 35 | base_name = interface | 36 | base_name = interface |
315 | 36 | return re.sub(u'[^\w:.-]', '--', base_name) | 37 | return re.sub(u'[^\w:.-]', '--', base_name) |
316 | 38 | |||
317 | 39 | |||
318 | 40 | def get_name_and_vlan_from_cluster_interface(cluster_name, interface): | ||
319 | 41 | """Return a name suitable for a `Network` managed by a cluster interface. | ||
320 | 42 | |||
321 | 43 | :param interface: Network interface name, e.g. `eth0:1`. | ||
322 | 44 | :param cluster_name: Name of the cluster. | ||
323 | 45 | :return: a tuple of the new name and the interface's VLAN tag. The VLAN | ||
324 | 46 | tag may be None. | ||
325 | 47 | """ | ||
326 | 48 | name = interface | ||
327 | 49 | vlan_tag = None | ||
328 | 50 | if '.' in name: | ||
329 | 51 | _, vlan_tag = name.split('.', 1) | ||
330 | 52 | name = name.replace('.', '-') | ||
331 | 53 | name = name.replace(':', '-') | ||
332 | 54 | network_name = "-".join((cluster_name, name)) | ||
333 | 55 | return network_name, vlan_tag | ||
334 | 37 | 56 | ||
335 | === modified file 'src/maasserver/utils/tests/test_interfaces.py' | |||
336 | --- src/maasserver/utils/tests/test_interfaces.py 2014-07-04 12:10:01 +0000 | |||
337 | +++ src/maasserver/utils/tests/test_interfaces.py 2014-11-14 14:24:50 +0000 | |||
338 | @@ -14,7 +14,12 @@ | |||
339 | 14 | __metaclass__ = type | 14 | __metaclass__ = type |
340 | 15 | __all__ = [] | 15 | __all__ = [] |
341 | 16 | 16 | ||
343 | 17 | from maasserver.utils.interfaces import make_name_from_interface | 17 | from random import randint |
344 | 18 | |||
345 | 19 | from maasserver.utils.interfaces import ( | ||
346 | 20 | get_name_and_vlan_from_cluster_interface, | ||
347 | 21 | make_name_from_interface, | ||
348 | 22 | ) | ||
349 | 18 | from maastesting.factory import factory | 23 | from maastesting.factory import factory |
350 | 19 | from maastesting.testcase import MAASTestCase | 24 | from maastesting.testcase import MAASTestCase |
351 | 20 | 25 | ||
352 | @@ -38,3 +43,56 @@ | |||
353 | 38 | self.assertNotEqual( | 43 | self.assertNotEqual( |
354 | 39 | make_name_from_interface(''), | 44 | make_name_from_interface(''), |
355 | 40 | make_name_from_interface('')) | 45 | make_name_from_interface('')) |
356 | 46 | |||
357 | 47 | |||
358 | 48 | class TestGetNameAndVlanFromClusterInterface(MAASTestCase): | ||
359 | 49 | """Tests for `get_name_and_vlan_from_cluster_interface`.""" | ||
360 | 50 | |||
361 | 51 | def make_interface(self): | ||
362 | 52 | """Return a simple network interface name.""" | ||
363 | 53 | return 'eth%d' % randint(0, 99) | ||
364 | 54 | |||
365 | 55 | def test_returns_simple_name_unaltered(self): | ||
366 | 56 | cluster = factory.make_name('cluster') | ||
367 | 57 | interface = factory.make_name('iface') | ||
368 | 58 | expected_name = '%s-%s' % (cluster, interface) | ||
369 | 59 | self.assertEqual( | ||
370 | 60 | (expected_name, None), | ||
371 | 61 | get_name_and_vlan_from_cluster_interface(cluster, interface)) | ||
372 | 62 | |||
373 | 63 | def test_substitutes_colon(self): | ||
374 | 64 | cluster = factory.make_name('cluster') | ||
375 | 65 | base_interface = self.make_interface() | ||
376 | 66 | alias = randint(0, 99) | ||
377 | 67 | interface = '%s:%d' % (base_interface, alias) | ||
378 | 68 | expected_name = '%s-%s-%d' % (cluster, base_interface, alias) | ||
379 | 69 | self.assertEqual( | ||
380 | 70 | (expected_name, None), | ||
381 | 71 | get_name_and_vlan_from_cluster_interface(cluster, interface)) | ||
382 | 72 | |||
383 | 73 | def test_returns_with_vlan_tag(self): | ||
384 | 74 | cluster = factory.make_name('cluster') | ||
385 | 75 | base_interface = self.make_interface() | ||
386 | 76 | vlan_tag = factory.make_vlan_tag() | ||
387 | 77 | interface = '%s.%d' % (base_interface, vlan_tag) | ||
388 | 78 | expected_name = '%s-%s-%d' % (cluster, base_interface, vlan_tag) | ||
389 | 79 | self.assertEqual( | ||
390 | 80 | (expected_name, '%d' % vlan_tag), | ||
391 | 81 | get_name_and_vlan_from_cluster_interface(cluster, interface)) | ||
392 | 82 | |||
393 | 83 | def test_returns_name_with_crazy_colon_and_vlan(self): | ||
394 | 84 | # For truly twisted network admins. | ||
395 | 85 | cluster = factory.make_name('cluster') | ||
396 | 86 | base_interface = self.make_interface() | ||
397 | 87 | vlan_tag = factory.make_vlan_tag() | ||
398 | 88 | alias = randint(0, 99) | ||
399 | 89 | interface = '%s:%d.%d' % (base_interface, alias, vlan_tag) | ||
400 | 90 | expected_name = '%s-%s-%d-%d' % ( | ||
401 | 91 | cluster, | ||
402 | 92 | base_interface, | ||
403 | 93 | alias, | ||
404 | 94 | vlan_tag, | ||
405 | 95 | ) | ||
406 | 96 | self.assertEqual( | ||
407 | 97 | (expected_name, '%d' % vlan_tag), | ||
408 | 98 | get_name_and_vlan_from_cluster_interface(cluster, interface)) | ||
409 | 41 | 99 | ||
410 | === modified file 'src/maastesting/factory.py' | |||
411 | --- src/maastesting/factory.py 2014-11-07 16:11:53 +0000 | |||
412 | +++ src/maastesting/factory.py 2014-11-14 14:24:50 +0000 | |||
413 | @@ -133,6 +133,28 @@ | |||
414 | 133 | assert port_min >= 0 and port_max <= 65535 | 133 | assert port_min >= 0 and port_max <= 65535 |
415 | 134 | return random.randint(port_min, port_max) | 134 | return random.randint(port_min, port_max) |
416 | 135 | 135 | ||
417 | 136 | def make_vlan_tag(self, allow_none=False, but_not=None): | ||
418 | 137 | """Create a random VLAN tag. | ||
419 | 138 | |||
420 | 139 | :param allow_none: Whether `None` ("no VLAN") can be allowed as an | ||
421 | 140 | outcome. If `True`, `None` will be included in the possible | ||
422 | 141 | results with a deliberately over-represented probability, in order | ||
423 | 142 | to help trip up bugs that might only show up once in about 4094 | ||
424 | 143 | calls otherwise. | ||
425 | 144 | :param but_not: A list of tags that should not be returned. Any zero | ||
426 | 145 | or `None` entries will be ignored. | ||
427 | 146 | """ | ||
428 | 147 | if but_not is None: | ||
429 | 148 | but_not = [] | ||
430 | 149 | if allow_none and self.pick_bool(): | ||
431 | 150 | return None | ||
432 | 151 | else: | ||
433 | 152 | for _ in range(100): | ||
434 | 153 | vlan_tag = random.randint(1, 0xffe) | ||
435 | 154 | if vlan_tag not in but_not: | ||
436 | 155 | return vlan_tag | ||
437 | 156 | raise TooManyRandomRetries("Could not find an available VLAN tag.") | ||
438 | 157 | |||
439 | 136 | def make_ipv4_address(self): | 158 | def make_ipv4_address(self): |
440 | 137 | octets = islice(self.random_octets, 4) | 159 | octets = islice(self.random_octets, 4) |
441 | 138 | return '%d.%d.%d.%d' % tuple(octets) | 160 | return '%d.%d.%d.%d' % tuple(octets) |
442 | 139 | 161 | ||
443 | === modified file 'src/maastesting/tests/test_factory.py' | |||
444 | --- src/maastesting/tests/test_factory.py 2014-09-17 08:31:08 +0000 | |||
445 | +++ src/maastesting/tests/test_factory.py 2014-11-14 14:24:50 +0000 | |||
446 | @@ -1,4 +1,5 @@ | |||
447 | 1 | # Copyright 2012-2014 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2012-2014 Canonical Ltd. This software is licensed under the |
448 | 2 | |||
449 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 3 | # GNU Affero General Public License version 3 (see the file LICENSE). |
450 | 3 | 4 | ||
451 | 4 | """Test the factory where appropriate. Don't overdo this.""" | 5 | """Test the factory where appropriate. Don't overdo this.""" |
452 | @@ -17,6 +18,7 @@ | |||
453 | 17 | from datetime import datetime | 18 | from datetime import datetime |
454 | 18 | from itertools import count | 19 | from itertools import count |
455 | 19 | import os.path | 20 | import os.path |
456 | 21 | import random | ||
457 | 20 | from random import randint | 22 | from random import randint |
458 | 21 | import subprocess | 23 | import subprocess |
459 | 22 | 24 | ||
460 | @@ -25,6 +27,7 @@ | |||
461 | 25 | TooManyRandomRetries, | 27 | TooManyRandomRetries, |
462 | 26 | ) | 28 | ) |
463 | 27 | from maastesting.testcase import MAASTestCase | 29 | from maastesting.testcase import MAASTestCase |
464 | 30 | from maastesting.utils import FakeRandInt | ||
465 | 28 | from netaddr import ( | 31 | from netaddr import ( |
466 | 29 | IPAddress, | 32 | IPAddress, |
467 | 30 | IPNetwork, | 33 | IPNetwork, |
468 | @@ -52,6 +55,23 @@ | |||
469 | 52 | def test_pick_port_returns_int(self): | 55 | def test_pick_port_returns_int(self): |
470 | 53 | self.assertIsInstance(factory.pick_port(), int) | 56 | self.assertIsInstance(factory.pick_port(), int) |
471 | 54 | 57 | ||
472 | 58 | def test_make_vlan_tag_excludes_None_by_default(self): | ||
473 | 59 | # Artificially limit randint to a very narrow range, to guarantee | ||
474 | 60 | # some repetition in its output, and virtually guarantee that we test | ||
475 | 61 | # both outcomes of the flip-a-coin call in make_vlan_tag. | ||
476 | 62 | self.patch(random, 'randint', FakeRandInt(random.randint, 0, 1)) | ||
477 | 63 | outcomes = {factory.make_vlan_tag() for _ in range(1000)} | ||
478 | 64 | self.assertEqual({1}, outcomes) | ||
479 | 65 | |||
480 | 66 | def test_make_vlan_tag_includes_None_if_allow_none(self): | ||
481 | 67 | self.patch(random, 'randint', FakeRandInt(random.randint, 0, 1)) | ||
482 | 68 | self.assertEqual( | ||
483 | 69 | {None, 1}, | ||
484 | 70 | { | ||
485 | 71 | factory.make_vlan_tag(allow_none=True) | ||
486 | 72 | for _ in range(1000) | ||
487 | 73 | }) | ||
488 | 74 | |||
489 | 55 | def test_make_ipv4_address(self): | 75 | def test_make_ipv4_address(self): |
490 | 56 | ip_address = factory.make_ipv4_address() | 76 | ip_address = factory.make_ipv4_address() |
491 | 57 | self.assertIsInstance(ip_address, unicode) | 77 | self.assertIsInstance(ip_address, unicode) |
492 | 58 | 78 | ||
493 | === modified file 'src/maastesting/utils.py' | |||
494 | --- src/maastesting/utils.py 2014-08-22 13:32:56 +0000 | |||
495 | +++ src/maastesting/utils.py 2014-11-14 14:24:50 +0000 | |||
496 | @@ -1,4 +1,4 @@ | |||
498 | 1 | # Copyright 2012 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2012-2014 Canonical Ltd. This software is licensed under the |
499 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
500 | 3 | 3 | ||
501 | 4 | """Testing utilities.""" | 4 | """Testing utilities.""" |
502 | @@ -17,6 +17,7 @@ | |||
503 | 17 | "content_from_file", | 17 | "content_from_file", |
504 | 18 | "extract_word_list", | 18 | "extract_word_list", |
505 | 19 | "get_write_time", | 19 | "get_write_time", |
506 | 20 | "FakeRandInt", | ||
507 | 20 | "preexec_fn", | 21 | "preexec_fn", |
508 | 21 | "run_isolated", | 22 | "run_isolated", |
509 | 22 | "sample_binary_data", | 23 | "sample_binary_data", |
510 | @@ -130,3 +131,25 @@ | |||
511 | 130 | # (1) Provided, of course, that man know only about ASCII and | 131 | # (1) Provided, of course, that man know only about ASCII and |
512 | 131 | # UTF. | 132 | # UTF. |
513 | 132 | sample_binary_data = codecs.BOM64_LE + codecs.BOM64_BE + b'\x00\xff\x00' | 133 | sample_binary_data = codecs.BOM64_LE + codecs.BOM64_BE + b'\x00\xff\x00' |
514 | 134 | |||
515 | 135 | |||
516 | 136 | class FakeRandInt: | ||
517 | 137 | """Fake `randint` with forced limitations on its range. | ||
518 | 138 | |||
519 | 139 | This lets you set a forced minimum, and/or a forced maximum, on the range | ||
520 | 140 | of any call. For example, if you pass `forced_maximum=3`, then a call | ||
521 | 141 | will never return more than 3. If you don't set a maximum, or if the | ||
522 | 142 | call's maximum argument is less than the forced maximum, then the call's | ||
523 | 143 | maximum will be respected. | ||
524 | 144 | """ | ||
525 | 145 | def __init__(self, real_randint, forced_minimum=None, forced_maximum=None): | ||
526 | 146 | self.real_randint = real_randint | ||
527 | 147 | self.minimum = forced_minimum | ||
528 | 148 | self.maximum = forced_maximum | ||
529 | 149 | |||
530 | 150 | def __call__(self, minimum, maximum): | ||
531 | 151 | if self.minimum is not None: | ||
532 | 152 | minimum = max(minimum, self.minimum) | ||
533 | 153 | if self.maximum is not None: | ||
534 | 154 | maximum = min(maximum, self.maximum) | ||
535 | 155 | return self.real_randint(minimum, maximum) |
Looks fine. I don't like FakeRandInt much, but I don't have a better suggestion offhand, and you've only moved it anyway.