Browse Source

Accept optional fixed_ip for nics

Covers a popular case of nodes having a pre-defined IP.

Also moves NICs code to a separate module for clarity.

Change-Id: Id8272cc30728ca68e7ce2efd4f3a2f9887ef7449
Story: #2002171
Task: #26340
Dmitry Tantsur 7 months ago
parent
commit
47fe222b6d

+ 10
- 1
metalsmith/_cmd.py View File

@@ -35,10 +35,17 @@ def _is_http(smth):
35 35
 
36 36
 class NICAction(argparse.Action):
37 37
     def __call__(self, parser, namespace, values, option_string=None):
38
-        assert option_string in ('--port', '--network')
38
+        assert option_string in ('--port', '--network', '--ip')
39 39
         nics = getattr(namespace, self.dest, None) or []
40 40
         if option_string == '--network':
41 41
             nics.append({'network': values})
42
+        elif option_string == '--ip':
43
+            try:
44
+                network, ip = values.split(':', 1)
45
+            except ValueError:
46
+                raise argparse.ArgumentError(
47
+                    self, '--ip format is NETWORK:IP, got %s' % values)
48
+            nics.append({'network': network, 'fixed_ip': ip})
42 49
         else:
43 50
             nics.append({'port': values})
44 51
         setattr(namespace, self.dest, nics)
@@ -177,6 +184,8 @@ def _parse_args(args, config):
177 184
                         dest='nics', action=NICAction)
178 185
     deploy.add_argument('--port', help='port to attach (name or UUID)',
179 186
                         dest='nics', action=NICAction)
187
+    deploy.add_argument('--ip', help='attach IP from the network',
188
+                        dest='nics', metavar='NETWORK:IP', action=NICAction)
180 189
     deploy.add_argument('--netboot', action='store_true',
181 190
                         help='boot from network instead of local disk')
182 191
     deploy.add_argument('--root-size', type=int,

+ 168
- 0
metalsmith/_nics.py View File

@@ -0,0 +1,168 @@
1
+# Copyright 2018 Red Hat, Inc.
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain a copy of the License at
6
+#
7
+#    http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12
+# implied.
13
+# See the License for the specific language governing permissions and
14
+# limitations under the License.
15
+
16
+import collections
17
+import logging
18
+
19
+from metalsmith import _utils
20
+from metalsmith import exceptions
21
+
22
+
23
+LOG = logging.getLogger(__name__)
24
+
25
+
26
+class NICs(object):
27
+    """Requested NICs."""
28
+
29
+    def __init__(self, api, node, nics):
30
+        if nics is None:
31
+            nics = []
32
+
33
+        if not isinstance(nics, collections.Sequence):
34
+            raise TypeError("NICs must be a list of dicts")
35
+
36
+        for nic in nics:
37
+            if not isinstance(nic, collections.Mapping):
38
+                raise TypeError("Each NIC must be a dict got %s" % nic)
39
+
40
+        self._node = node
41
+        self._api = api
42
+        self._nics = nics
43
+        self._validated = None
44
+        self.created_ports = []
45
+        self.attached_ports = []
46
+
47
+    def validate(self):
48
+        """Validate provided NIC records."""
49
+        if self._validated is not None:
50
+            return
51
+
52
+        result = []
53
+        for nic in self._nics:
54
+            if 'port' in nic:
55
+                result.append(('port', self._get_port(nic)))
56
+            elif 'network' in nic:
57
+                result.append(('network', self._get_network(nic)))
58
+            else:
59
+                raise exceptions.InvalidNIC(
60
+                    'Unknown NIC record type, export "port" or "network", '
61
+                    'got %s' % nic)
62
+
63
+        self._validated = result
64
+
65
+    def create_and_attach_ports(self):
66
+        """Attach ports to the node, creating them if requested."""
67
+        self.validate()
68
+
69
+        for nic_type, nic in self._validated:
70
+            if nic_type == 'network':
71
+                port = self._api.connection.network.create_port(**nic)
72
+                self.created_ports.append(port.id)
73
+                LOG.info('Created port %(port)s for node %(node)s with '
74
+                         '%(nic)s', {'port': _utils.log_res(port),
75
+                                     'node': _utils.log_node(self._node),
76
+                                     'nic': nic})
77
+            else:
78
+                port = nic
79
+
80
+            self._api.attach_port_to_node(self._node.uuid, port.id)
81
+            LOG.info('Attached port %(port)s to node %(node)s',
82
+                     {'port': _utils.log_res(port),
83
+                      'node': _utils.log_node(self._node)})
84
+            self.attached_ports.append(port.id)
85
+
86
+    def detach_and_delete_ports(self):
87
+        """Detach attached port and delete previously created ones."""
88
+        detach_and_delete_ports(self._api, self._node, self.created_ports,
89
+                                self.attached_ports)
90
+
91
+    def _get_port(self, nic):
92
+        """Validate and get the NIC information for a port.
93
+
94
+        :param nic: NIC information in the form ``{"port": "<port ident>"}``.
95
+        :returns: `Port` object to use.
96
+        """
97
+        unexpected = set(nic) - {'port'}
98
+        if unexpected:
99
+            raise exceptions.InvalidNIC(
100
+                'Unexpected fields for a port: %s' % ', '.join(unexpected))
101
+
102
+        try:
103
+            port = self._api.connection.network.find_port(
104
+                nic['port'], ignore_missing=False)
105
+        except Exception as exc:
106
+            raise exceptions.InvalidNIC(
107
+                'Cannot find port %(port)s: %(error)s' %
108
+                {'port': nic['port'], 'error': exc})
109
+
110
+        return port
111
+
112
+    def _get_network(self, nic):
113
+        """Validate and get the NIC information for a network.
114
+
115
+        :param nic: NIC information in the form ``{"network": "<net ident>"}``
116
+            or ``{"network": "<net ident>", "fixed_ip": "<desired IP>"}``.
117
+        :returns: keyword arguments to use when creating a port.
118
+        """
119
+        unexpected = set(nic) - {'network', 'fixed_ip'}
120
+        if unexpected:
121
+            raise exceptions.InvalidNIC(
122
+                'Unexpected fields for a network: %s' % ', '.join(unexpected))
123
+
124
+        try:
125
+            network = self._api.connection.network.find_network(
126
+                nic['network'], ignore_missing=False)
127
+        except Exception as exc:
128
+            raise exceptions.InvalidNIC(
129
+                'Cannot find network %(net)s: %(error)s' %
130
+                {'net': nic['network'], 'error': exc})
131
+
132
+        port_args = {'network_id': network.id}
133
+        if nic.get('fixed_ip'):
134
+            port_args['fixed_ips'] = [{'ip_address': nic['fixed_ip']}]
135
+
136
+        return port_args
137
+
138
+
139
+def detach_and_delete_ports(api, node, created_ports, attached_ports):
140
+    """Detach attached port and delete previously created ones.
141
+
142
+    :param api: `Api` instance.
143
+    :param node: `Node` object to detach ports from.
144
+    :param created_ports: List of IDs of previously created ports.
145
+    :param attached_ports: List of IDs of previously attached_ports.
146
+    """
147
+    for port_id in set(attached_ports + created_ports):
148
+        LOG.debug('Detaching port %(port)s from node %(node)s',
149
+                  {'port': port_id, 'node': node.uuid})
150
+        try:
151
+            api.detach_port_from_node(node, port_id)
152
+        except Exception as exc:
153
+            LOG.debug('Failed to remove VIF %(vif)s from node %(node)s, '
154
+                      'assuming already removed: %(exc)s',
155
+                      {'vif': port_id, 'node': _utils.log_node(node),
156
+                       'exc': exc})
157
+
158
+    for port_id in created_ports:
159
+        LOG.debug('Deleting port %s', port_id)
160
+        try:
161
+            api.connection.network.delete_port(port_id,
162
+                                               ignore_missing=False)
163
+        except Exception as exc:
164
+            LOG.warning('Failed to delete neutron port %(port)s: %(exc)s',
165
+                        {'port': port_id, 'exc': exc})
166
+        else:
167
+            LOG.info('Deleted port %(port)s for node %(node)s',
168
+                     {'port': port_id, 'node': _utils.log_node(node)})

+ 29
- 102
metalsmith/_provisioner.py View File

@@ -24,6 +24,7 @@ import six
24 24
 
25 25
 from metalsmith import _config
26 26
 from metalsmith import _instance
27
+from metalsmith import _nics
27 28
 from metalsmith import _os_api
28 29
 from metalsmith import _scheduler
29 30
 from metalsmith import _utils
@@ -218,6 +219,8 @@ class Provisioner(object):
218 219
             Each item is a dict with a key describing the type of the NIC:
219 220
             either a port (``{"port": "<port name or ID>"}``) or a network
220 221
             to create a port on (``{"network": "<network name or ID>"}``).
222
+            A network record can optionally feature a ``fixed_ip`` argument
223
+            to use this specific fixed IP from a suitable subnet.
221 224
         :param root_size_gb: The size of the root partition. By default
222 225
             the value of the local_gb property is used.
223 226
         :param swap_size_mb: The size of the swap partition. It's an error
@@ -253,8 +256,7 @@ class Provisioner(object):
253 256
             root_size_gb = root_disk_size
254 257
 
255 258
         node = self._check_node_for_deploy(node)
256
-        created_ports = []
257
-        attached_ports = []
259
+        nics = _nics.NICs(self._api, node, nics)
258 260
 
259 261
         try:
260 262
             hostname = self._check_hostname(node, hostname)
@@ -262,7 +264,7 @@ class Provisioner(object):
262 264
 
263 265
             image._validate(self.connection)
264 266
 
265
-            nics = self._get_nics(nics or [])
267
+            nics.validate()
266 268
 
267 269
             if capabilities is None:
268 270
                 capabilities = node.instance_info.get('capabilities') or {}
@@ -272,15 +274,14 @@ class Provisioner(object):
272 274
                             _utils.log_node(node))
273 275
                 return node
274 276
 
275
-            self._create_and_attach_ports(node, nics,
276
-                                          created_ports, attached_ports)
277
+            nics.create_and_attach_ports()
277 278
 
278 279
             capabilities['boot_option'] = 'netboot' if netboot else 'local'
279 280
 
280 281
             updates = {'/instance_info/root_gb': root_size_gb,
281 282
                        '/instance_info/capabilities': capabilities,
282
-                       '/extra/%s' % _CREATED_PORTS: created_ports,
283
-                       '/extra/%s' % _ATTACHED_PORTS: attached_ports,
283
+                       '/extra/%s' % _CREATED_PORTS: nics.created_ports,
284
+                       '/extra/%s' % _ATTACHED_PORTS: nics.attached_ports,
284 285
                        '/instance_info/%s' % _os_api.HOSTNAME_FIELD: hostname}
285 286
             updates.update(image._node_updates(self.connection))
286 287
             if traits is not None:
@@ -304,8 +305,7 @@ class Provisioner(object):
304 305
             try:
305 306
                 LOG.error('Deploy attempt failed on node %s, cleaning up',
306 307
                           _utils.log_node(node))
307
-                self._clean_up(node, created_ports=created_ports,
308
-                               attached_ports=attached_ports)
308
+                self._clean_up(node, nics=nics)
309 309
             except Exception:
310 310
                 LOG.exception('Clean up failed')
311 311
 
@@ -326,97 +326,6 @@ class Provisioner(object):
326 326
 
327 327
         return instance
328 328
 
329
-    def _get_nics(self, nics):
330
-        """Validate and get the NICs."""
331
-        _utils.validate_nics(nics)
332
-
333
-        result = []
334
-        for nic in nics:
335
-            nic_type, nic_id = next(iter(nic.items()))
336
-            if nic_type == 'network':
337
-                try:
338
-                    network = self.connection.network.find_network(
339
-                        nic_id, ignore_missing=False)
340
-                except Exception as exc:
341
-                    raise exceptions.InvalidNIC(
342
-                        'Cannot find network %(net)s: %(error)s' %
343
-                        {'net': nic_id, 'error': exc})
344
-                else:
345
-                    result.append((nic_type, network))
346
-            else:
347
-                try:
348
-                    port = self.connection.network.find_port(
349
-                        nic_id, ignore_missing=False)
350
-                except Exception as exc:
351
-                    raise exceptions.InvalidNIC(
352
-                        'Cannot find port %(port)s: %(error)s' %
353
-                        {'port': nic_id, 'error': exc})
354
-                else:
355
-                    result.append((nic_type, port))
356
-
357
-        return result
358
-
359
-    def _create_and_attach_ports(self, node, nics, created_ports,
360
-                                 attached_ports):
361
-        """Create and attach ports on given networks."""
362
-        for nic_type, nic in nics:
363
-            if nic_type == 'network':
364
-                port = self.connection.network.create_port(network_id=nic.id)
365
-                created_ports.append(port.id)
366
-                LOG.info('Created port %(port)s for node %(node)s on '
367
-                         'network %(net)s',
368
-                         {'port': _utils.log_res(port),
369
-                          'node': _utils.log_node(node),
370
-                          'net': _utils.log_res(nic)})
371
-            else:
372
-                port = nic
373
-
374
-            self._api.attach_port_to_node(node.uuid, port.id)
375
-            LOG.info('Attached port %(port)s to node %(node)s',
376
-                     {'port': _utils.log_res(port),
377
-                      'node': _utils.log_node(node)})
378
-            attached_ports.append(port.id)
379
-
380
-    def _delete_ports(self, node, created_ports=None, attached_ports=None):
381
-        if created_ports is None:
382
-            created_ports = node.extra.get(_CREATED_PORTS, [])
383
-        if attached_ports is None:
384
-            attached_ports = node.extra.get(_ATTACHED_PORTS, [])
385
-
386
-        for port_id in set(attached_ports + created_ports):
387
-            LOG.debug('Detaching port %(port)s from node %(node)s',
388
-                      {'port': port_id, 'node': node.uuid})
389
-            try:
390
-                self._api.detach_port_from_node(node, port_id)
391
-            except Exception as exc:
392
-                LOG.debug('Failed to remove VIF %(vif)s from node %(node)s, '
393
-                          'assuming already removed: %(exc)s',
394
-                          {'vif': port_id, 'node': _utils.log_node(node),
395
-                           'exc': exc})
396
-
397
-        for port_id in created_ports:
398
-            LOG.debug('Deleting port %s', port_id)
399
-            try:
400
-                self.connection.network.delete_port(port_id,
401
-                                                    ignore_missing=False)
402
-            except Exception as exc:
403
-                LOG.warning('Failed to delete neutron port %(port)s: %(exc)s',
404
-                            {'port': port_id, 'exc': exc})
405
-            else:
406
-                LOG.info('Deleted port %(port)s for node %(node)s',
407
-                         {'port': port_id, 'node': _utils.log_node(node)})
408
-
409
-        update = {'/extra/%s' % item: _os_api.REMOVE
410
-                  for item in (_CREATED_PORTS, _ATTACHED_PORTS)}
411
-        update['/instance_info/%s' % _os_api.HOSTNAME_FIELD] = _os_api.REMOVE
412
-        LOG.debug('Updating node %(node)s with %(updates)s',
413
-                  {'node': _utils.log_node(node), 'updates': update})
414
-        try:
415
-            self._api.update_node(node, update)
416
-        except Exception as exc:
417
-            LOG.debug('Failed to clear node %(node)s extra: %(exc)s',
418
-                      {'node': _utils.log_node(node), 'exc': exc})
419
-
420 329
     def wait_for_provisioning(self, nodes, timeout=None, delay=15):
421 330
         """Wait for nodes to be provisioned.
422 331
 
@@ -495,8 +404,26 @@ class Provisioner(object):
495 404
             LOG.debug('All nodes reached state %s', state)
496 405
             return finished_nodes
497 406
 
498
-    def _clean_up(self, node, created_ports=None, attached_ports=None):
499
-        self._delete_ports(node, created_ports, attached_ports)
407
+    def _clean_up(self, node, nics=None):
408
+        if nics is None:
409
+            created_ports = node.extra.get(_CREATED_PORTS, [])
410
+            attached_ports = node.extra.get(_ATTACHED_PORTS, [])
411
+            _nics.detach_and_delete_ports(self._api, node, created_ports,
412
+                                          attached_ports)
413
+        else:
414
+            nics.detach_and_delete_ports()
415
+
416
+        update = {'/extra/%s' % item: _os_api.REMOVE
417
+                  for item in (_CREATED_PORTS, _ATTACHED_PORTS)}
418
+        update['/instance_info/%s' % _os_api.HOSTNAME_FIELD] = _os_api.REMOVE
419
+        LOG.debug('Updating node %(node)s with %(updates)s',
420
+                  {'node': _utils.log_node(node), 'updates': update})
421
+        try:
422
+            self._api.update_node(node, update)
423
+        except Exception as exc:
424
+            LOG.debug('Failed to clear node %(node)s extra: %(exc)s',
425
+                      {'node': _utils.log_node(node), 'exc': exc})
426
+
500 427
         LOG.debug('Releasing lock on node %s', _utils.log_node(node))
501 428
         self._api.release_node(node)
502 429
 

+ 0
- 21
metalsmith/_utils.py View File

@@ -13,7 +13,6 @@
13 13
 # See the License for the specific language governing permissions and
14 14
 # limitations under the License.
15 15
 
16
-import collections
17 16
 import re
18 17
 
19 18
 import six
@@ -94,26 +93,6 @@ def is_hostname_safe(hostname):
94 93
     return _HOSTNAME_RE.match(hostname) is not None
95 94
 
96 95
 
97
-def validate_nics(nics):
98
-    """Validate NICs."""
99
-    if not isinstance(nics, collections.Sequence):
100
-        raise TypeError("NICs must be a list of dicts")
101
-
102
-    unknown_nic_types = set()
103
-    for nic in nics:
104
-        if not isinstance(nic, collections.Mapping) or len(nic) != 1:
105
-            raise TypeError("Each NIC must be a dict with one item, "
106
-                            "got %s" % nic)
107
-
108
-        nic_type = next(iter(nic))
109
-        if nic_type not in ('port', 'network'):
110
-            unknown_nic_types.add(nic_type)
111
-
112
-    if unknown_nic_types:
113
-        raise ValueError("Unexpected NIC type(s) %s, supported values are "
114
-                         "'port' and 'network'" % ', '.join(unknown_nic_types))
115
-
116
-
117 96
 def parse_checksums(checksums):
118 97
     """Parse standard checksums file."""
119 98
     result = {}

+ 14
- 0
metalsmith/test/test_cmd.py View File

@@ -331,6 +331,20 @@ class TestDeploy(testtools.TestCase):
331 331
                     {'nics': [{'network': 'net1'}, {'port': 'port1'},
332 332
                               {'port': 'port2'}, {'network': 'net2'}]})
333 333
 
334
+    def test_args_ips(self, mock_pr):
335
+        args = ['deploy', '--image', 'myimg', '--resource-class', 'compute',
336
+                '--ip', 'private:10.0.0.2', '--ip', 'public:8.0.8.0']
337
+        self._check(mock_pr, args, {},
338
+                    {'nics': [{'network': 'private', 'fixed_ip': '10.0.0.2'},
339
+                              {'network': 'public', 'fixed_ip': '8.0.8.0'}]})
340
+
341
+    def test_args_bad_ip(self, mock_pr):
342
+        args = ['deploy', '--image', 'myimg', '--resource-class', 'compute',
343
+                '--ip', 'private:10.0.0.2', '--ip', 'public']
344
+        self.assertRaises(SystemExit, _cmd.main, args)
345
+        self.assertFalse(mock_pr.return_value.reserve_node.called)
346
+        self.assertFalse(mock_pr.return_value.provision_node.called)
347
+
334 348
     def test_args_hostname(self, mock_pr):
335 349
         args = ['deploy', '--network', 'mynet', '--image', 'myimg',
336 350
                 '--hostname', 'host', '--resource-class', 'compute']

+ 58
- 4
metalsmith/test/test_provisioner.py View File

@@ -336,6 +336,26 @@ class TestProvisionNode(Base):
336 336
         self.assertFalse(self.api.release_node.called)
337 337
         self.assertFalse(self.conn.network.delete_port.called)
338 338
 
339
+    def test_ok_without_nics(self):
340
+        self.updates['/extra/metalsmith_created_ports'] = []
341
+        self.updates['/extra/metalsmith_attached_ports'] = []
342
+        inst = self.pr.provision_node(self.node, 'image')
343
+
344
+        self.assertEqual(inst.uuid, self.node.uuid)
345
+        self.assertEqual(inst.node, self.node)
346
+
347
+        self.assertFalse(self.conn.network.create_port.called)
348
+        self.assertFalse(self.conn.network.find_port.called)
349
+        self.assertFalse(self.api.attach_port_to_node.called)
350
+        self.api.update_node.assert_called_once_with(self.node, self.updates)
351
+        self.api.validate_node.assert_called_once_with(self.node,
352
+                                                       validate_deploy=True)
353
+        self.api.node_action.assert_called_once_with(self.node, 'active',
354
+                                                     configdrive=mock.ANY)
355
+        self.assertFalse(self.wait_mock.called)
356
+        self.assertFalse(self.api.release_node.called)
357
+        self.assertFalse(self.conn.network.delete_port.called)
358
+
339 359
     def test_ok_with_source(self):
340 360
         inst = self.pr.provision_node(self.node, sources.GlanceImage('image'),
341 361
                                       [{'network': 'network'}])
@@ -470,6 +490,28 @@ class TestProvisionNode(Base):
470 490
         self.assertFalse(self.api.release_node.called)
471 491
         self.assertFalse(self.conn.network.delete_port.called)
472 492
 
493
+    def test_with_ip(self):
494
+        inst = self.pr.provision_node(self.node, 'image',
495
+                                      [{'network': 'network',
496
+                                        'fixed_ip': '10.0.0.2'}])
497
+
498
+        self.assertEqual(inst.uuid, self.node.uuid)
499
+        self.assertEqual(inst.node, self.node)
500
+
501
+        self.conn.network.create_port.assert_called_once_with(
502
+            network_id=self.conn.network.find_network.return_value.id,
503
+            fixed_ips=[{'ip_address': '10.0.0.2'}])
504
+        self.api.attach_port_to_node.assert_called_once_with(
505
+            self.node.uuid, self.conn.network.create_port.return_value.id)
506
+        self.api.update_node.assert_called_once_with(self.node, self.updates)
507
+        self.api.validate_node.assert_called_once_with(self.node,
508
+                                                       validate_deploy=True)
509
+        self.api.node_action.assert_called_once_with(self.node, 'active',
510
+                                                     configdrive=mock.ANY)
511
+        self.assertFalse(self.wait_mock.called)
512
+        self.assertFalse(self.api.release_node.called)
513
+        self.assertFalse(self.conn.network.delete_port.called)
514
+
473 515
     def test_whole_disk(self):
474 516
         self.image.kernel_id = None
475 517
         self.image.ramdisk_id = None
@@ -1128,20 +1170,19 @@ abcd  and-not-image-again
1128 1170
         self.assertFalse(self.conn.network.create_port.called)
1129 1171
         self.assertFalse(self.api.attach_port_to_node.called)
1130 1172
         self.assertFalse(self.api.node_action.called)
1131
-        self.api.release_node.assert_called_once_with(self.node)
1132 1173
 
1133 1174
     def test_invalid_nic(self):
1134
-        for item in ('string', ['string'], [{1: 2, 3: 4}]):
1175
+        for item in ('string', ['string']):
1135 1176
             self.assertRaisesRegex(TypeError, 'must be a dict',
1136 1177
                                    self.pr.provision_node,
1137 1178
                                    self.node, 'image', item)
1138 1179
         self.assertFalse(self.conn.network.create_port.called)
1139 1180
         self.assertFalse(self.api.attach_port_to_node.called)
1140 1181
         self.assertFalse(self.api.node_action.called)
1141
-        self.api.release_node.assert_called_with(self.node)
1142 1182
 
1143 1183
     def test_invalid_nic_type(self):
1144
-        self.assertRaisesRegex(ValueError, r'Unexpected NIC type\(s\) foo',
1184
+        self.assertRaisesRegex(exceptions.InvalidNIC,
1185
+                               'Unknown NIC record type',
1145 1186
                                self.pr.provision_node,
1146 1187
                                self.node, 'image', [{'foo': 'bar'}])
1147 1188
         self.assertFalse(self.conn.network.create_port.called)
@@ -1149,6 +1190,19 @@ abcd  and-not-image-again
1149 1190
         self.assertFalse(self.api.node_action.called)
1150 1191
         self.api.release_node.assert_called_once_with(self.node)
1151 1192
 
1193
+    def test_invalid_nic_type_fields(self):
1194
+        for item in ({'port': '1234', 'foo': 'bar'},
1195
+                     {'port': '1234', 'network': '4321'},
1196
+                     {'network': '4321', 'foo': 'bar'}):
1197
+            self.assertRaisesRegex(exceptions.InvalidNIC,
1198
+                                   'Unexpected fields',
1199
+                                   self.pr.provision_node,
1200
+                                   self.node, 'image', [item])
1201
+        self.assertFalse(self.conn.network.create_port.called)
1202
+        self.assertFalse(self.api.attach_port_to_node.called)
1203
+        self.assertFalse(self.api.node_action.called)
1204
+        self.api.release_node.assert_called_with(self.node)
1205
+
1152 1206
     def test_invalid_hostname(self):
1153 1207
         self.assertRaisesRegex(ValueError, 'n_1 cannot be used as a hostname',
1154 1208
                                self.pr.provision_node,

+ 10
- 0
roles/metalsmith_deployment/README.rst View File

@@ -88,6 +88,16 @@ Each instances has the following attributes:
88 88
               - network: private
89 89
               - network: ctlplane
90 90
 
91
+        can optionally take a fixed IP to assign:
92
+
93
+        .. code-block:: yaml
94
+
95
+            nics:
96
+              - network: private
97
+                fixed_ip: 10.0.0.2
98
+              - network: ctlplane
99
+                fixed_ip: 192.168.42.30
100
+
91 101
     ``port``
92 102
         uses the provided pre-created port:
93 103
 

Loading…
Cancel
Save