summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDmitry Tantsur <divius.inside@gmail.com>2018-09-04 14:05:45 +0200
committerDmitry Tantsur <divius.inside@gmail.com>2018-09-05 12:25:11 +0200
commita638cec0665ffd94023218afb9bede183b962308 (patch)
tree68f8392b463ea6a1f30f93fb3d3849aa670e31ca
parenta34d0e095174e11d3886edf13ef18e1f8d105c6b (diff)
Allow filtering by arbitrary predicate and conductor_group
BREAKING: changed order of arguments in reserve_node. BREAKING: changed exceptions inheriting ReservationFailed. Change-Id: I79cc9b2794d8332cdb818af0b7effb28d4e9a786 Story: #2003584 Task: #24890
Notes
Notes (review): Code-Review+2: Dmitry Tantsur <divius.inside@gmail.com> Workflow+1: Dmitry Tantsur <divius.inside@gmail.com> Verified+2: Zuul Submitted-by: Zuul Submitted-at: Thu, 06 Sep 2018 10:41:36 +0000 Reviewed-on: https://review.openstack.org/599578 Project: openstack/metalsmith Branch: refs/heads/master
-rw-r--r--metalsmith/_cmd.py6
-rw-r--r--metalsmith/_os_api.py23
-rw-r--r--metalsmith/_provisioner.py32
-rw-r--r--metalsmith/_scheduler.py66
-rw-r--r--metalsmith/exceptions.py64
-rw-r--r--metalsmith/test/test_cmd.py42
-rw-r--r--metalsmith/test/test_os_api.py18
-rw-r--r--metalsmith/test/test_provisioner.py65
-rw-r--r--metalsmith/test/test_scheduler.py21
-rw-r--r--roles/metalsmith_deployment/README.rst7
-rw-r--r--roles/metalsmith_deployment/defaults/main.yml1
-rw-r--r--roles/metalsmith_deployment/tasks/main.yml4
12 files changed, 236 insertions, 113 deletions
diff --git a/metalsmith/_cmd.py b/metalsmith/_cmd.py
index 76ccde6..7ddfa16 100644
--- a/metalsmith/_cmd.py
+++ b/metalsmith/_cmd.py
@@ -56,7 +56,9 @@ def _do_deploy(api, args, formatter):
56 if args.user_name: 56 if args.user_name:
57 config.add_user(args.user_name, sudo=args.passwordless_sudo) 57 config.add_user(args.user_name, sudo=args.passwordless_sudo)
58 58
59 node = api.reserve_node(args.resource_class, capabilities=capabilities, 59 node = api.reserve_node(resource_class=args.resource_class,
60 conductor_group=args.conductor_group,
61 capabilities=capabilities,
60 candidates=args.candidate) 62 candidates=args.candidate)
61 instance = api.provision_node(node, 63 instance = api.provision_node(node,
62 image=args.image, 64 image=args.image,
@@ -138,6 +140,8 @@ def _parse_args(args, config):
138 'Node\'s name or UUID') 140 'Node\'s name or UUID')
139 deploy.add_argument('--resource-class', 141 deploy.add_argument('--resource-class',
140 help='node resource class to deploy') 142 help='node resource class to deploy')
143 deploy.add_argument('--conductor-group',
144 help='conductor group to pick the node from')
141 deploy.add_argument('--candidate', action='append', 145 deploy.add_argument('--candidate', action='append',
142 help='A candidate node to use for scheduling (can be ' 146 help='A candidate node to use for scheduling (can be '
143 'specified several times)') 147 'specified several times)')
diff --git a/metalsmith/_os_api.py b/metalsmith/_os_api.py
index 71d9d33..933771c 100644
--- a/metalsmith/_os_api.py
+++ b/metalsmith/_os_api.py
@@ -24,9 +24,6 @@ from metalsmith import _utils
24 24
25 25
26LOG = logging.getLogger(__name__) 26LOG = logging.getLogger(__name__)
27NODE_FIELDS = ['name', 'uuid', 'instance_info', 'instance_uuid', 'maintenance',
28 'maintenance_reason', 'properties', 'provision_state', 'extra',
29 'last_error']
30HOSTNAME_FIELD = 'metalsmith_hostname' 27HOSTNAME_FIELD = 'metalsmith_hostname'
31 28
32 29
@@ -57,7 +54,9 @@ class API(object):
57 """Various OpenStack API's.""" 54 """Various OpenStack API's."""
58 55
59 IRONIC_VERSION = '1' 56 IRONIC_VERSION = '1'
60 IRONIC_MICRO_VERSION = '1.28' 57 # TODO(dtantsur): use openstacksdk and stop hardcoding this here.
58 # 1.46 (Rocky) adds conductor_group.
59 IRONIC_MICRO_VERSION = '1.46'
61 60
62 _node_list = None 61 _node_list = None
63 62
@@ -139,7 +138,7 @@ class API(object):
139 if by_hostname is not None: 138 if by_hostname is not None:
140 return by_hostname 139 return by_hostname
141 140
142 return self.ironic.node.get(node, fields=NODE_FIELDS) 141 return self.ironic.node.get(node)
143 elif hasattr(node, 'node'): 142 elif hasattr(node, 'node'):
144 # Instance object 143 # Instance object
145 node = node.node 144 node = node.node
@@ -147,7 +146,7 @@ class API(object):
147 node = node 146 node = node
148 147
149 if refresh: 148 if refresh:
150 return self.ironic.node.get(node.uuid, fields=NODE_FIELDS) 149 return self.ironic.node.get(node.uuid)
151 else: 150 else:
152 return node 151 return node
153 152
@@ -161,14 +160,14 @@ class API(object):
161 def list_node_ports(self, node): 160 def list_node_ports(self, node):
162 return self.ironic.node.list_ports(_node_id(node), limit=0) 161 return self.ironic.node.list_ports(_node_id(node), limit=0)
163 162
164 def list_nodes(self, resource_class=None, maintenance=False, 163 def list_nodes(self, maintenance=False, associated=False,
165 associated=False, provision_state='available', 164 provision_state='available', **filters):
166 fields=None): 165 if 'fields' not in filters:
167 return self.ironic.node.list(limit=0, resource_class=resource_class, 166 filters['detail'] = True
168 maintenance=maintenance, 167 return self.ironic.node.list(limit=0, maintenance=maintenance,
169 associated=associated, 168 associated=associated,
170 provision_state=provision_state, 169 provision_state=provision_state,
171 fields=fields or NODE_FIELDS) 170 **filters)
172 171
173 def node_action(self, node, action, **kwargs): 172 def node_action(self, node, action, **kwargs):
174 self.ironic.node.set_provision_state(_node_id(node), action, **kwargs) 173 self.ironic.node.set_provision_state(_node_id(node), action, **kwargs)
diff --git a/metalsmith/_provisioner.py b/metalsmith/_provisioner.py
index 8db2d76..2514e1d 100644
--- a/metalsmith/_provisioner.py
+++ b/metalsmith/_provisioner.py
@@ -50,8 +50,8 @@ class Provisioner(object):
50 self._api = _os_api.API(session=session, cloud_region=cloud_region) 50 self._api = _os_api.API(session=session, cloud_region=cloud_region)
51 self._dry_run = dry_run 51 self._dry_run = dry_run
52 52
53 def reserve_node(self, resource_class=None, capabilities=None, 53 def reserve_node(self, resource_class=None, conductor_group=None,
54 candidates=None): 54 capabilities=None, candidates=None, predicate=None):
55 """Find and reserve a suitable node. 55 """Find and reserve a suitable node.
56 56
57 Example:: 57 Example::
@@ -61,11 +61,17 @@ class Provisioner(object):
61 61
62 :param resource_class: Requested resource class. If ``None``, a node 62 :param resource_class: Requested resource class. If ``None``, a node
63 with any resource class can be chosen. 63 with any resource class can be chosen.
64 :param conductor_group: Conductor group to pick the nodes from.
65 Value ``None`` means any group, use empty string "" for nodes
66 from the default group.
64 :param capabilities: Requested capabilities as a dict. 67 :param capabilities: Requested capabilities as a dict.
65 :param candidates: List of nodes (UUIDs, names or `Node` objects) 68 :param candidates: List of nodes (UUIDs, names or `Node` objects)
66 to pick from. The filters (for resource class and capabilities) 69 to pick from. The filters (for resource class and capabilities)
67 are still applied to the provided list. The order in which 70 are still applied to the provided list. The order in which
68 the nodes are considered is retained. 71 the nodes are considered is retained.
72 :param predicate: Custom predicate to run on nodes. A callable that
73 accepts a node and returns ``True`` if it should be included,
74 ``False`` otherwise. Any exceptions are propagated to the caller.
69 :return: reserved `Node` object. 75 :return: reserved `Node` object.
70 :raises: :py:class:`metalsmith.exceptions.ReservationFailed` 76 :raises: :py:class:`metalsmith.exceptions.ReservationFailed`
71 """ 77 """
@@ -76,22 +82,28 @@ class Provisioner(object):
76 if resource_class: 82 if resource_class:
77 nodes = [node for node in nodes 83 nodes = [node for node in nodes
78 if node.resource_class == resource_class] 84 if node.resource_class == resource_class]
85 if conductor_group is not None:
86 nodes = [node for node in nodes
87 if node.conductor_group == conductor_group]
79 else: 88 else:
80 nodes = self._api.list_nodes(resource_class=resource_class) 89 nodes = self._api.list_nodes(resource_class=resource_class,
90 conductor_group=conductor_group)
81 # Ensure parallel executions don't try nodes in the same sequence 91 # Ensure parallel executions don't try nodes in the same sequence
82 random.shuffle(nodes) 92 random.shuffle(nodes)
83 93
84 if not nodes: 94 if not nodes:
85 raise exceptions.ResourceClassNotFound(resource_class, 95 raise exceptions.NodesNotFound(resource_class, conductor_group)
86 capabilities)
87 96
88 LOG.debug('Ironic nodes: %s', nodes) 97 LOG.debug('Ironic nodes: %s', nodes)
89 98
90 filters = [_scheduler.CapabilitiesFilter(resource_class, capabilities), 99 filters = [_scheduler.CapabilitiesFilter(capabilities),
91 _scheduler.ValidationFilter(self._api, 100 _scheduler.ValidationFilter(self._api)]
92 resource_class, capabilities)] 101 if predicate is not None:
93 reserver = _scheduler.IronicReserver(self._api, resource_class, 102 # NOTE(dtantsur): run the provided predicate before the validation,
94 capabilities) 103 # since validation requires network interactions.
104 filters.insert(-1, predicate)
105
106 reserver = _scheduler.IronicReserver(self._api)
95 node = _scheduler.schedule_node(nodes, filters, reserver, 107 node = _scheduler.schedule_node(nodes, filters, reserver,
96 dry_run=self._dry_run) 108 dry_run=self._dry_run)
97 if capabilities: 109 if capabilities:
diff --git a/metalsmith/_scheduler.py b/metalsmith/_scheduler.py
index 62f2c87..ad52b62 100644
--- a/metalsmith/_scheduler.py
+++ b/metalsmith/_scheduler.py
@@ -117,8 +117,7 @@ def schedule_node(nodes, filters, reserver, dry_run=False):
117class CapabilitiesFilter(Filter): 117class CapabilitiesFilter(Filter):
118 """Filter that checks capabilities.""" 118 """Filter that checks capabilities."""
119 119
120 def __init__(self, resource_class, capabilities): 120 def __init__(self, capabilities):
121 self._resource_class = resource_class
122 self._capabilities = capabilities 121 self._capabilities = capabilities
123 self._counter = collections.Counter() 122 self._counter = collections.Counter()
124 123
@@ -159,20 +158,16 @@ class CapabilitiesFilter(Filter):
159 message = ("No available nodes found with capabilities %(req)s, " 158 message = ("No available nodes found with capabilities %(req)s, "
160 "existing capabilities: %(exist)s" % 159 "existing capabilities: %(exist)s" %
161 {'req': requested, 'exist': existing or 'none'}) 160 {'req': requested, 'exist': existing or 'none'})
162 raise exceptions.CapabilitiesNotFound(message, 161 raise exceptions.CapabilitiesNotFound(message, self._capabilities)
163 self._resource_class,
164 self._capabilities)
165 162
166 163
167class ValidationFilter(Filter): 164class ValidationFilter(Filter):
168 """Filter that runs validation on nodes.""" 165 """Filter that runs validation on nodes."""
169 166
170 def __init__(self, api, resource_class, capabilities): 167 def __init__(self, api):
171 self._api = api 168 self._api = api
172 # These are only used for better exceptions 169 self._messages = []
173 self._resource_class = resource_class 170 self._failed_nodes = []
174 self._capabilities = capabilities
175 self._failed_validation = []
176 171
177 def __call__(self, node): 172 def __call__(self, node):
178 try: 173 try:
@@ -181,45 +176,44 @@ class ValidationFilter(Filter):
181 message = ('Node %(node)s failed validation: %(err)s' % 176 message = ('Node %(node)s failed validation: %(err)s' %
182 {'node': _utils.log_node(node), 'err': exc}) 177 {'node': _utils.log_node(node), 'err': exc})
183 LOG.warning(message) 178 LOG.warning(message)
184 self._failed_validation.append(message) 179 self._messages.append(message)
180 self._failed_nodes.append(node)
185 return False 181 return False
186 182
187 return True 183 return True
188 184
189 def fail(self): 185 def fail(self):
190 errors = ", ".join(self._failed_validation) 186 errors = ", ".join(self._messages)
191 message = "All available nodes have failed validation: %s" % errors 187 message = "All available nodes have failed validation: %s" % errors
192 raise exceptions.ValidationFailed(message, 188 raise exceptions.ValidationFailed(message, self._failed_nodes)
193 self._resource_class,
194 self._capabilities)
195 189
196 190
197class IronicReserver(Reserver): 191class IronicReserver(Reserver):
198 192
199 def __init__(self, api, resource_class, capabilities): 193 def __init__(self, api):
200 self._api = api 194 self._api = api
201 # These are only used for better exceptions 195 self._failed_nodes = []
202 self._resource_class = resource_class
203 self._capabilities = capabilities
204 196
205 def __call__(self, node): 197 def __call__(self, node):
206 result = self._api.reserve_node(node, instance_uuid=node.uuid) 198 try:
207 199 result = self._api.reserve_node(node, instance_uuid=node.uuid)
208 # Try validation again to be sure nothing has changed 200
209 validator = ValidationFilter(self._api, self._resource_class, 201 # Try validation again to be sure nothing has changed
210 self._capabilities) 202 validator = ValidationFilter(self._api)
211 if not validator(result): 203 if not validator(result):
212 LOG.warning('Validation of node %s failed after reservation', 204 LOG.warning('Validation of node %s failed after reservation',
213 _utils.log_node(node)) 205 _utils.log_node(node))
214 try: 206 try:
215 self._api.release_node(node) 207 self._api.release_node(node)
216 except Exception: 208 except Exception:
217 LOG.exception('Failed to release the reserved node %s', 209 LOG.exception('Failed to release the reserved node %s',
218 _utils.log_node(node)) 210 _utils.log_node(node))
219 validator.fail() 211 validator.fail()
220 212
221 return result 213 return result
214 except Exception:
215 self._failed_nodes.append(node)
216 raise
222 217
223 def fail(self): 218 def fail(self):
224 raise exceptions.AllNodesReserved(self._resource_class, 219 raise exceptions.AllNodesReserved(self._failed_nodes)
225 self._capabilities)
diff --git a/metalsmith/exceptions.py b/metalsmith/exceptions.py
index 1a935eb..aac1d74 100644
--- a/metalsmith/exceptions.py
+++ b/metalsmith/exceptions.py
@@ -19,42 +19,64 @@ class Error(Exception):
19 19
20 20
21class ReservationFailed(Error): 21class ReservationFailed(Error):
22 """Failed to reserve a suitable node.""" 22 """Failed to reserve a suitable node.
23
24 This is the base class for all reservation failures.
25 """
23 26
24 def __init__(self, message, requested_resource_class,
25 requested_capabilities):
26 super(ReservationFailed, self).__init__(message)
27 self.requested_resource_class = requested_resource_class
28 self.requested_capabilities = requested_capabilities
29 27
28class NodesNotFound(ReservationFailed):
29 """Initial nodes lookup returned an empty list.
30 30
31class ResourceClassNotFound(ReservationFailed): 31 :ivar requested_resource_class: Requested resource class.
32 """No nodes match the given resource class.""" 32 :ivar requested_conductor_group: Requested conductor group to pick nodes
33 from.
34 """
33 35
34 def __init__(self, requested_resource_class, requested_capabilities): 36 def __init__(self, resource_class, conductor_group):
35 message = ("No available nodes found with resource class %s" % 37 message = "No available nodes%(rc)s found%(cg)s" % {
36 requested_resource_class) 38 'rc': 'with resource class %s' % resource_class
37 super(ResourceClassNotFound, self).__init__(message, 39 if resource_class else '',
38 requested_resource_class, 40 'cg': 'in conductor group %s' % (conductor_group or '<default>')
39 requested_capabilities) 41 if conductor_group is not None else ''
42 }
43 self.requested_resource_class = resource_class
44 self.requested_conductor_group = conductor_group
45 super(NodesNotFound, self).__init__(message)
40 46
41 47
42class CapabilitiesNotFound(ReservationFailed): 48class CapabilitiesNotFound(ReservationFailed):
43 """Requested capabilities do not match any nodes.""" 49 """Requested capabilities do not match any nodes.
50
51 :ivar requested_capabilities: Requested node's capabilities.
52 """
53
54 def __init__(self, message, capabilities):
55 self.requested_capabilities = capabilities
56 super(CapabilitiesNotFound, self).__init__(message)
44 57
45 58
46class ValidationFailed(ReservationFailed): 59class ValidationFailed(ReservationFailed):
47 """Validation failed for all requested nodes.""" 60 """Validation failed for all requested nodes.
61
62 :ivar nodes: List of nodes that were checked.
63 """
64
65 def __init__(self, message, nodes):
66 self.nodes = nodes
67 super(ValidationFailed, self).__init__(message)
48 68
49 69
50class AllNodesReserved(ReservationFailed): 70class AllNodesReserved(ReservationFailed):
51 """All nodes are already reserved.""" 71 """All nodes are already reserved.
52 72
53 def __init__(self, requested_resource_class, requested_capabilities): 73 :ivar nodes: List of nodes that were checked.
74 """
75
76 def __init__(self, nodes):
77 self.nodes = nodes
54 message = 'All the candidate nodes are already reserved' 78 message = 'All the candidate nodes are already reserved'
55 super(AllNodesReserved, self).__init__(message, 79 super(AllNodesReserved, self).__init__(message)
56 requested_resource_class,
57 requested_capabilities)
58 80
59 81
60class InvalidImage(Error): 82class InvalidImage(Error):
diff --git a/metalsmith/test/test_cmd.py b/metalsmith/test/test_cmd.py
index 3c616a6..8ada457 100644
--- a/metalsmith/test/test_cmd.py
+++ b/metalsmith/test/test_cmd.py
@@ -55,6 +55,7 @@ class TestDeploy(testtools.TestCase):
55 dry_run=False) 55 dry_run=False)
56 mock_pr.return_value.reserve_node.assert_called_once_with( 56 mock_pr.return_value.reserve_node.assert_called_once_with(
57 resource_class='compute', 57 resource_class='compute',
58 conductor_group=None,
58 capabilities={}, 59 capabilities={},
59 candidates=None 60 candidates=None
60 ) 61 )
@@ -101,6 +102,7 @@ class TestDeploy(testtools.TestCase):
101 dry_run=False) 102 dry_run=False)
102 mock_pr.return_value.reserve_node.assert_called_once_with( 103 mock_pr.return_value.reserve_node.assert_called_once_with(
103 resource_class='compute', 104 resource_class='compute',
105 conductor_group=None,
104 capabilities={}, 106 capabilities={},
105 candidates=None 107 candidates=None
106 ) 108 )
@@ -174,6 +176,7 @@ class TestDeploy(testtools.TestCase):
174 dry_run=True) 176 dry_run=True)
175 mock_pr.return_value.reserve_node.assert_called_once_with( 177 mock_pr.return_value.reserve_node.assert_called_once_with(
176 resource_class='compute', 178 resource_class='compute',
179 conductor_group=None,
177 capabilities={}, 180 capabilities={},
178 candidates=None 181 candidates=None
179 ) 182 )
@@ -197,6 +200,7 @@ class TestDeploy(testtools.TestCase):
197 dry_run=False) 200 dry_run=False)
198 mock_pr.return_value.reserve_node.assert_called_once_with( 201 mock_pr.return_value.reserve_node.assert_called_once_with(
199 resource_class='compute', 202 resource_class='compute',
203 conductor_group=None,
200 capabilities={}, 204 capabilities={},
201 candidates=None 205 candidates=None
202 ) 206 )
@@ -228,6 +232,7 @@ class TestDeploy(testtools.TestCase):
228 dry_run=False) 232 dry_run=False)
229 mock_pr.return_value.reserve_node.assert_called_once_with( 233 mock_pr.return_value.reserve_node.assert_called_once_with(
230 resource_class='compute', 234 resource_class='compute',
235 conductor_group=None,
231 capabilities={}, 236 capabilities={},
232 candidates=None 237 candidates=None
233 ) 238 )
@@ -261,6 +266,7 @@ class TestDeploy(testtools.TestCase):
261 dry_run=False) 266 dry_run=False)
262 mock_pr.return_value.reserve_node.assert_called_once_with( 267 mock_pr.return_value.reserve_node.assert_called_once_with(
263 resource_class='compute', 268 resource_class='compute',
269 conductor_group=None,
264 capabilities={}, 270 capabilities={},
265 candidates=None 271 candidates=None
266 ) 272 )
@@ -292,6 +298,7 @@ class TestDeploy(testtools.TestCase):
292 dry_run=False) 298 dry_run=False)
293 mock_pr.return_value.reserve_node.assert_called_once_with( 299 mock_pr.return_value.reserve_node.assert_called_once_with(
294 resource_class='compute', 300 resource_class='compute',
301 conductor_group=None,
295 capabilities={}, 302 capabilities={},
296 candidates=None 303 candidates=None
297 ) 304 )
@@ -323,6 +330,7 @@ class TestDeploy(testtools.TestCase):
323 dry_run=False) 330 dry_run=False)
324 mock_pr.return_value.reserve_node.assert_called_once_with( 331 mock_pr.return_value.reserve_node.assert_called_once_with(
325 resource_class='compute', 332 resource_class='compute',
333 conductor_group=None,
326 capabilities={}, 334 capabilities={},
327 candidates=None 335 candidates=None
328 ) 336 )
@@ -379,6 +387,7 @@ class TestDeploy(testtools.TestCase):
379 dry_run=False) 387 dry_run=False)
380 mock_pr.return_value.reserve_node.assert_called_once_with( 388 mock_pr.return_value.reserve_node.assert_called_once_with(
381 resource_class='compute', 389 resource_class='compute',
390 conductor_group=None,
382 capabilities={'foo': 'bar', 'answer': '42'}, 391 capabilities={'foo': 'bar', 'answer': '42'},
383 candidates=None 392 candidates=None
384 ) 393 )
@@ -405,6 +414,7 @@ class TestDeploy(testtools.TestCase):
405 dry_run=False) 414 dry_run=False)
406 mock_pr.return_value.reserve_node.assert_called_once_with( 415 mock_pr.return_value.reserve_node.assert_called_once_with(
407 resource_class='compute', 416 resource_class='compute',
417 conductor_group=None,
408 capabilities={}, 418 capabilities={},
409 candidates=None 419 candidates=None
410 ) 420 )
@@ -430,6 +440,7 @@ class TestDeploy(testtools.TestCase):
430 dry_run=False) 440 dry_run=False)
431 mock_pr.return_value.reserve_node.assert_called_once_with( 441 mock_pr.return_value.reserve_node.assert_called_once_with(
432 resource_class='compute', 442 resource_class='compute',
443 conductor_group=None,
433 capabilities={}, 444 capabilities={},
434 candidates=None 445 candidates=None
435 ) 446 )
@@ -458,6 +469,7 @@ class TestDeploy(testtools.TestCase):
458 dry_run=False) 469 dry_run=False)
459 mock_pr.return_value.reserve_node.assert_called_once_with( 470 mock_pr.return_value.reserve_node.assert_called_once_with(
460 resource_class='compute', 471 resource_class='compute',
472 conductor_group=None,
461 capabilities={}, 473 capabilities={},
462 candidates=None 474 candidates=None
463 ) 475 )
@@ -483,6 +495,7 @@ class TestDeploy(testtools.TestCase):
483 dry_run=False) 495 dry_run=False)
484 mock_pr.return_value.reserve_node.assert_called_once_with( 496 mock_pr.return_value.reserve_node.assert_called_once_with(
485 resource_class='compute', 497 resource_class='compute',
498 conductor_group=None,
486 capabilities={}, 499 capabilities={},
487 candidates=None 500 candidates=None
488 ) 501 )
@@ -504,6 +517,7 @@ class TestDeploy(testtools.TestCase):
504 dry_run=False) 517 dry_run=False)
505 mock_pr.return_value.reserve_node.assert_called_once_with( 518 mock_pr.return_value.reserve_node.assert_called_once_with(
506 resource_class='compute', 519 resource_class='compute',
520 conductor_group=None,
507 capabilities={}, 521 capabilities={},
508 candidates=None 522 candidates=None
509 ) 523 )
@@ -527,6 +541,7 @@ class TestDeploy(testtools.TestCase):
527 dry_run=False) 541 dry_run=False)
528 mock_pr.return_value.reserve_node.assert_called_once_with( 542 mock_pr.return_value.reserve_node.assert_called_once_with(
529 resource_class='compute', 543 resource_class='compute',
544 conductor_group=None,
530 capabilities={}, 545 capabilities={},
531 candidates=None 546 candidates=None
532 ) 547 )
@@ -550,6 +565,7 @@ class TestDeploy(testtools.TestCase):
550 dry_run=False) 565 dry_run=False)
551 mock_pr.return_value.reserve_node.assert_called_once_with( 566 mock_pr.return_value.reserve_node.assert_called_once_with(
552 resource_class='compute', 567 resource_class='compute',
568 conductor_group=None,
553 capabilities={}, 569 capabilities={},
554 candidates=None 570 candidates=None
555 ) 571 )
@@ -572,6 +588,7 @@ class TestDeploy(testtools.TestCase):
572 dry_run=False) 588 dry_run=False)
573 mock_pr.return_value.reserve_node.assert_called_once_with( 589 mock_pr.return_value.reserve_node.assert_called_once_with(
574 resource_class=None, 590 resource_class=None,
591 conductor_group=None,
575 capabilities={}, 592 capabilities={},
576 candidates=['node1', 'node2'] 593 candidates=['node1', 'node2']
577 ) 594 )
@@ -585,6 +602,29 @@ class TestDeploy(testtools.TestCase):
585 netboot=False, 602 netboot=False,
586 wait=1800) 603 wait=1800)
587 604
605 def test_args_conductor_group(self, mock_os_conf, mock_pr):
606 args = ['deploy', '--conductor-group', 'loc1', '--image', 'myimg',
607 '--resource-class', 'compute']
608 _cmd.main(args)
609 mock_pr.assert_called_once_with(
610 cloud_region=mock_os_conf.return_value.get_one.return_value,
611 dry_run=False)
612 mock_pr.return_value.reserve_node.assert_called_once_with(
613 resource_class='compute',
614 conductor_group='loc1',
615 capabilities={},
616 candidates=None
617 )
618 mock_pr.return_value.provision_node.assert_called_once_with(
619 mock_pr.return_value.reserve_node.return_value,
620 image='myimg',
621 nics=None,
622 root_disk_size=None,
623 config=mock.ANY,
624 hostname=None,
625 netboot=False,
626 wait=1800)
627
588 def test_args_custom_wait(self, mock_os_conf, mock_pr): 628 def test_args_custom_wait(self, mock_os_conf, mock_pr):
589 args = ['deploy', '--network', 'mynet', '--image', 'myimg', 629 args = ['deploy', '--network', 'mynet', '--image', 'myimg',
590 '--wait', '3600', '--resource-class', 'compute'] 630 '--wait', '3600', '--resource-class', 'compute']
@@ -594,6 +634,7 @@ class TestDeploy(testtools.TestCase):
594 dry_run=False) 634 dry_run=False)
595 mock_pr.return_value.reserve_node.assert_called_once_with( 635 mock_pr.return_value.reserve_node.assert_called_once_with(
596 resource_class='compute', 636 resource_class='compute',
637 conductor_group=None,
597 capabilities={}, 638 capabilities={},
598 candidates=None 639 candidates=None
599 ) 640 )
@@ -616,6 +657,7 @@ class TestDeploy(testtools.TestCase):
616 dry_run=False) 657 dry_run=False)
617 mock_pr.return_value.reserve_node.assert_called_once_with( 658 mock_pr.return_value.reserve_node.assert_called_once_with(
618 resource_class='compute', 659 resource_class='compute',
660 conductor_group=None,
619 capabilities={}, 661 capabilities={},
620 candidates=None 662 candidates=None
621 ) 663 )
diff --git a/metalsmith/test/test_os_api.py b/metalsmith/test/test_os_api.py
index 51f410d..0fac915 100644
--- a/metalsmith/test/test_os_api.py
+++ b/metalsmith/test/test_os_api.py
@@ -54,8 +54,7 @@ class TestNodes(testtools.TestCase):
54 54
55 def test_get_node_by_uuid(self): 55 def test_get_node_by_uuid(self):
56 res = self.api.get_node('uuid1') 56 res = self.api.get_node('uuid1')
57 self.cli.node.get.assert_called_once_with('uuid1', 57 self.cli.node.get.assert_called_once_with('uuid1')
58 fields=_os_api.NODE_FIELDS)
59 self.assertIs(res, self.cli.node.get.return_value) 58 self.assertIs(res, self.cli.node.get.return_value)
60 59
61 def test_get_node_by_hostname(self): 60 def test_get_node_by_hostname(self):
@@ -66,8 +65,7 @@ class TestNodes(testtools.TestCase):
66 ] 65 ]
67 res = self.api.get_node('host1', accept_hostname=True) 66 res = self.api.get_node('host1', accept_hostname=True)
68 # Loading details 67 # Loading details
69 self.cli.node.get.assert_called_once_with('uuid1', 68 self.cli.node.get.assert_called_once_with('uuid1')
70 fields=_os_api.NODE_FIELDS)
71 self.assertIs(res, self.cli.node.get.return_value) 69 self.assertIs(res, self.cli.node.get.return_value)
72 70
73 def test_get_node_by_hostname_not_found(self): 71 def test_get_node_by_hostname_not_found(self):
@@ -78,8 +76,7 @@ class TestNodes(testtools.TestCase):
78 ] 76 ]
79 res = self.api.get_node('host1', accept_hostname=True) 77 res = self.api.get_node('host1', accept_hostname=True)
80 # Loading details 78 # Loading details
81 self.cli.node.get.assert_called_once_with('host1', 79 self.cli.node.get.assert_called_once_with('host1')
82 fields=_os_api.NODE_FIELDS)
83 self.assertIs(res, self.cli.node.get.return_value) 80 self.assertIs(res, self.cli.node.get.return_value)
84 81
85 def test_get_node_by_node(self): 82 def test_get_node_by_node(self):
@@ -90,8 +87,7 @@ class TestNodes(testtools.TestCase):
90 def test_get_node_by_node_with_refresh(self): 87 def test_get_node_by_node_with_refresh(self):
91 res = self.api.get_node(mock.Mock(spec=['uuid'], uuid='uuid1'), 88 res = self.api.get_node(mock.Mock(spec=['uuid'], uuid='uuid1'),
92 refresh=True) 89 refresh=True)
93 self.cli.node.get.assert_called_once_with('uuid1', 90 self.cli.node.get.assert_called_once_with('uuid1')
94 fields=_os_api.NODE_FIELDS)
95 self.assertIs(res, self.cli.node.get.return_value) 91 self.assertIs(res, self.cli.node.get.return_value)
96 92
97 def test_get_node_by_instance(self): 93 def test_get_node_by_instance(self):
@@ -104,8 +100,7 @@ class TestNodes(testtools.TestCase):
104 inst = _instance.Instance(mock.Mock(), 100 inst = _instance.Instance(mock.Mock(),
105 mock.Mock(spec=['uuid'], uuid='uuid1')) 101 mock.Mock(spec=['uuid'], uuid='uuid1'))
106 res = self.api.get_node(inst, refresh=True) 102 res = self.api.get_node(inst, refresh=True)
107 self.cli.node.get.assert_called_once_with('uuid1', 103 self.cli.node.get.assert_called_once_with('uuid1')
108 fields=_os_api.NODE_FIELDS)
109 self.assertIs(res, self.cli.node.get.return_value) 104 self.assertIs(res, self.cli.node.get.return_value)
110 105
111 def test_find_node_by_hostname(self): 106 def test_find_node_by_hostname(self):
@@ -116,8 +111,7 @@ class TestNodes(testtools.TestCase):
116 ] 111 ]
117 res = self.api.find_node_by_hostname('host1') 112 res = self.api.find_node_by_hostname('host1')
118 # Loading details 113 # Loading details
119 self.cli.node.get.assert_called_once_with('uuid1', 114 self.cli.node.get.assert_called_once_with('uuid1')
120 fields=_os_api.NODE_FIELDS)
121 self.assertIs(res, self.cli.node.get.return_value) 115 self.assertIs(res, self.cli.node.get.return_value)
122 116
123 def test_find_node_by_hostname_cached(self): 117 def test_find_node_by_hostname_cached(self):
diff --git a/metalsmith/test/test_provisioner.py b/metalsmith/test/test_provisioner.py
index 276192d..b4216cf 100644
--- a/metalsmith/test/test_provisioner.py
+++ b/metalsmith/test/test_provisioner.py
@@ -25,13 +25,18 @@ from metalsmith import exceptions
25from metalsmith import sources 25from metalsmith import sources
26 26
27 27
28NODE_FIELDS = ['name', 'uuid', 'instance_info', 'instance_uuid', 'maintenance',
29 'maintenance_reason', 'properties', 'provision_state', 'extra',
30 'last_error']
31
32
28class Base(testtools.TestCase): 33class Base(testtools.TestCase):
29 34
30 def setUp(self): 35 def setUp(self):
31 super(Base, self).setUp() 36 super(Base, self).setUp()
32 self.pr = _provisioner.Provisioner(mock.Mock()) 37 self.pr = _provisioner.Provisioner(mock.Mock())
33 self._reset_api_mock() 38 self._reset_api_mock()
34 self.node = mock.Mock(spec=_os_api.NODE_FIELDS + ['to_dict'], 39 self.node = mock.Mock(spec=NODE_FIELDS + ['to_dict'],
35 uuid='000', instance_uuid=None, 40 uuid='000', instance_uuid=None,
36 properties={'local_gb': 100}, 41 properties={'local_gb': 100},
37 instance_info={}, 42 instance_info={},
@@ -59,8 +64,8 @@ class TestReserveNode(Base):
59 def test_no_nodes(self): 64 def test_no_nodes(self):
60 self.api.list_nodes.return_value = [] 65 self.api.list_nodes.return_value = []
61 66
62 self.assertRaises(exceptions.ResourceClassNotFound, 67 self.assertRaises(exceptions.NodesNotFound,
63 self.pr.reserve_node, 'control') 68 self.pr.reserve_node, resource_class='control')
64 self.assertFalse(self.api.reserve_node.called) 69 self.assertFalse(self.api.reserve_node.called)
65 70
66 def test_simple_ok(self): 71 def test_simple_ok(self):
@@ -99,12 +104,30 @@ class TestReserveNode(Base):
99 self.api.list_nodes.return_value = nodes 104 self.api.list_nodes.return_value = nodes
100 self.api.reserve_node.side_effect = lambda n, instance_uuid: n 105 self.api.reserve_node.side_effect = lambda n, instance_uuid: n
101 106
102 node = self.pr.reserve_node('control', {'answer': '42'}) 107 node = self.pr.reserve_node('control', capabilities={'answer': '42'})
103 108
104 self.assertIs(node, expected) 109 self.assertIs(node, expected)
105 self.api.update_node.assert_called_once_with( 110 self.api.update_node.assert_called_once_with(
106 node, {'/instance_info/capabilities': {'answer': '42'}}) 111 node, {'/instance_info/capabilities': {'answer': '42'}})
107 112
113 def test_custom_predicate(self):
114 nodes = [
115 mock.Mock(spec=['uuid', 'name', 'properties'],
116 properties={'local_gb': 100}),
117 mock.Mock(spec=['uuid', 'name', 'properties'],
118 properties={'local_gb': 150}),
119 mock.Mock(spec=['uuid', 'name', 'properties'],
120 properties={'local_gb': 200}),
121 ]
122 self.api.list_nodes.return_value = nodes[:]
123 self.api.reserve_node.side_effect = lambda n, instance_uuid: n
124
125 node = self.pr.reserve_node(
126 predicate=lambda node: 100 < node.properties['local_gb'] < 200)
127
128 self.assertEqual(node, nodes[1])
129 self.assertFalse(self.api.update_node.called)
130
108 def test_provided_node(self): 131 def test_provided_node(self):
109 nodes = [ 132 nodes = [
110 mock.Mock(spec=['uuid', 'name', 'properties'], 133 mock.Mock(spec=['uuid', 'name', 'properties'],
@@ -153,6 +176,28 @@ class TestReserveNode(Base):
153 self.api.update_node.assert_called_once_with( 176 self.api.update_node.assert_called_once_with(
154 node, {'/instance_info/capabilities': {'cat': 'meow'}}) 177 node, {'/instance_info/capabilities': {'cat': 'meow'}})
155 178
179 def test_nodes_filtered_by_conductor_group(self):
180 nodes = [
181 mock.Mock(spec=['uuid', 'name', 'properties', 'conductor_group'],
182 properties={'local_gb': 100}, conductor_group='loc1'),
183 mock.Mock(spec=['uuid', 'name', 'properties', 'conductor_group'],
184 properties={'local_gb': 100, 'capabilities': 'cat:meow'},
185 conductor_group=''),
186 mock.Mock(spec=['uuid', 'name', 'properties', 'conductor_group'],
187 properties={'local_gb': 100, 'capabilities': 'cat:meow'},
188 conductor_group='loc1'),
189 ]
190 self.api.reserve_node.side_effect = lambda n, instance_uuid: n
191
192 node = self.pr.reserve_node(conductor_group='loc1',
193 candidates=nodes,
194 capabilities={'cat': 'meow'})
195
196 self.assertEqual(node, nodes[2])
197 self.assertFalse(self.api.list_nodes.called)
198 self.api.update_node.assert_called_once_with(
199 node, {'/instance_info/capabilities': {'cat': 'meow'}})
200
156 201
157CLEAN_UP = { 202CLEAN_UP = {
158 '/extra/metalsmith_created_ports': _os_api.REMOVE, 203 '/extra/metalsmith_created_ports': _os_api.REMOVE,
@@ -848,7 +893,7 @@ class TestWaitForState(Base):
848 893
849 def test_success_one_node(self, mock_sleep): 894 def test_success_one_node(self, mock_sleep):
850 nodes = [ 895 nodes = [
851 mock.Mock(spec=_os_api.NODE_FIELDS, provision_state=state) 896 mock.Mock(spec=NODE_FIELDS, provision_state=state)
852 for state in ('deploying', 'deploy wait', 'deploying', 'active') 897 for state in ('deploying', 'deploy wait', 'deploying', 'active')
853 ] 898 ]
854 self.api.get_node.side_effect = nodes 899 self.api.get_node.side_effect = nodes
@@ -862,7 +907,7 @@ class TestWaitForState(Base):
862 907
863 def test_success_several_nodes(self, mock_sleep): 908 def test_success_several_nodes(self, mock_sleep):
864 nodes = [ 909 nodes = [
865 mock.Mock(spec=_os_api.NODE_FIELDS, provision_state=state) 910 mock.Mock(spec=NODE_FIELDS, provision_state=state)
866 for state in ('deploying', 'deploy wait', # iteration 1 911 for state in ('deploying', 'deploy wait', # iteration 1
867 'deploying', 'active', # iteration 2 912 'deploying', 'active', # iteration 2
868 'active') # iteration 3 913 'active') # iteration 3
@@ -879,7 +924,7 @@ class TestWaitForState(Base):
879 924
880 def test_one_node_failed(self, mock_sleep): 925 def test_one_node_failed(self, mock_sleep):
881 nodes = [ 926 nodes = [
882 mock.Mock(spec=_os_api.NODE_FIELDS, provision_state=state) 927 mock.Mock(spec=NODE_FIELDS, provision_state=state)
883 for state in ('deploying', 'deploy wait', # iteration 1 928 for state in ('deploying', 'deploy wait', # iteration 1
884 'deploying', 'deploy failed', # iteration 2 929 'deploying', 'deploy failed', # iteration 2
885 'active') # iteration 3 930 'active') # iteration 3
@@ -898,7 +943,7 @@ class TestWaitForState(Base):
898 def test_timeout(self, mock_sleep): 943 def test_timeout(self, mock_sleep):
899 def _fake_get(*args, **kwargs): 944 def _fake_get(*args, **kwargs):
900 while True: 945 while True:
901 yield mock.Mock(spec=_os_api.NODE_FIELDS, 946 yield mock.Mock(spec=NODE_FIELDS,
902 provision_state='deploying') 947 provision_state='deploying')
903 948
904 self.api.get_node.side_effect = _fake_get() 949 self.api.get_node.side_effect = _fake_get()
@@ -913,7 +958,7 @@ class TestWaitForState(Base):
913 958
914 def test_custom_delay(self, mock_sleep): 959 def test_custom_delay(self, mock_sleep):
915 nodes = [ 960 nodes = [
916 mock.Mock(spec=_os_api.NODE_FIELDS, provision_state=state) 961 mock.Mock(spec=NODE_FIELDS, provision_state=state)
917 for state in ('deploying', 'deploy wait', 'deploying', 'active') 962 for state in ('deploying', 'deploy wait', 'deploying', 'active')
918 ] 963 ]
919 self.api.get_node.side_effect = nodes 964 self.api.get_node.side_effect = nodes
@@ -930,7 +975,7 @@ class TestListInstances(Base):
930 def setUp(self): 975 def setUp(self):
931 super(TestListInstances, self).setUp() 976 super(TestListInstances, self).setUp()
932 self.nodes = [ 977 self.nodes = [
933 mock.Mock(spec=_os_api.NODE_FIELDS, provision_state=state, 978 mock.Mock(spec=NODE_FIELDS, provision_state=state,
934 instance_info={'metalsmith_hostname': '1234'}) 979 instance_info={'metalsmith_hostname': '1234'})
935 for state in ('active', 'active', 'deploying', 'wait call-back', 980 for state in ('active', 'active', 'deploying', 'wait call-back',
936 'deploy failed', 'available') 981 'deploy failed', 'available')
diff --git a/metalsmith/test/test_scheduler.py b/metalsmith/test/test_scheduler.py
index e177f73..5bd18b6 100644
--- a/metalsmith/test/test_scheduler.py
+++ b/metalsmith/test/test_scheduler.py
@@ -113,35 +113,35 @@ class TestScheduleNode(testtools.TestCase):
113class TestCapabilitiesFilter(testtools.TestCase): 113class TestCapabilitiesFilter(testtools.TestCase):
114 114
115 def test_fail_no_capabilities(self): 115 def test_fail_no_capabilities(self):
116 fltr = _scheduler.CapabilitiesFilter('rsc', {'profile': 'compute'}) 116 fltr = _scheduler.CapabilitiesFilter({'profile': 'compute'})
117 self.assertRaisesRegex(exceptions.CapabilitiesNotFound, 117 self.assertRaisesRegex(exceptions.CapabilitiesNotFound,
118 'No available nodes found with capabilities ' 118 'No available nodes found with capabilities '
119 'profile=compute, existing capabilities: none', 119 'profile=compute, existing capabilities: none',
120 fltr.fail) 120 fltr.fail)
121 121
122 def test_nothing_requested_nothing_found(self): 122 def test_nothing_requested_nothing_found(self):
123 fltr = _scheduler.CapabilitiesFilter('rsc', {}) 123 fltr = _scheduler.CapabilitiesFilter({})
124 node = mock.Mock(properties={}, spec=['properties', 'name', 'uuid']) 124 node = mock.Mock(properties={}, spec=['properties', 'name', 'uuid'])
125 self.assertTrue(fltr(node)) 125 self.assertTrue(fltr(node))
126 126
127 def test_matching_node(self): 127 def test_matching_node(self):
128 fltr = _scheduler.CapabilitiesFilter('rsc', {'profile': 'compute', 128 fltr = _scheduler.CapabilitiesFilter({'profile': 'compute',
129 'foo': 'bar'}) 129 'foo': 'bar'})
130 node = mock.Mock( 130 node = mock.Mock(
131 properties={'capabilities': 'foo:bar,profile:compute,answer:42'}, 131 properties={'capabilities': 'foo:bar,profile:compute,answer:42'},
132 spec=['properties', 'name', 'uuid']) 132 spec=['properties', 'name', 'uuid'])
133 self.assertTrue(fltr(node)) 133 self.assertTrue(fltr(node))
134 134
135 def test_not_matching_node(self): 135 def test_not_matching_node(self):
136 fltr = _scheduler.CapabilitiesFilter('rsc', {'profile': 'compute', 136 fltr = _scheduler.CapabilitiesFilter({'profile': 'compute',
137 'foo': 'bar'}) 137 'foo': 'bar'})
138 node = mock.Mock( 138 node = mock.Mock(
139 properties={'capabilities': 'foo:bar,answer:42'}, 139 properties={'capabilities': 'foo:bar,answer:42'},
140 spec=['properties', 'name', 'uuid']) 140 spec=['properties', 'name', 'uuid'])
141 self.assertFalse(fltr(node)) 141 self.assertFalse(fltr(node))
142 142
143 def test_fail_message(self): 143 def test_fail_message(self):
144 fltr = _scheduler.CapabilitiesFilter('rsc', {'profile': 'compute'}) 144 fltr = _scheduler.CapabilitiesFilter({'profile': 'compute'})
145 node = mock.Mock( 145 node = mock.Mock(
146 properties={'capabilities': 'profile:control'}, 146 properties={'capabilities': 'profile:control'},
147 spec=['properties', 'name', 'uuid']) 147 spec=['properties', 'name', 'uuid'])
@@ -153,7 +153,7 @@ class TestCapabilitiesFilter(testtools.TestCase):
153 fltr.fail) 153 fltr.fail)
154 154
155 def test_malformed_capabilities(self): 155 def test_malformed_capabilities(self):
156 fltr = _scheduler.CapabilitiesFilter('rsc', {'profile': 'compute'}) 156 fltr = _scheduler.CapabilitiesFilter({'profile': 'compute'})
157 for cap in ['foo,profile:control', 42, 'a:b:c']: 157 for cap in ['foo,profile:control', 42, 'a:b:c']:
158 node = mock.Mock(properties={'capabilities': cap}, 158 node = mock.Mock(properties={'capabilities': cap},
159 spec=['properties', 'name', 'uuid']) 159 spec=['properties', 'name', 'uuid'])
@@ -169,8 +169,7 @@ class TestValidationFilter(testtools.TestCase):
169 def setUp(self): 169 def setUp(self):
170 super(TestValidationFilter, self).setUp() 170 super(TestValidationFilter, self).setUp()
171 self.api = mock.Mock(spec=['validate_node']) 171 self.api = mock.Mock(spec=['validate_node'])
172 self.fltr = _scheduler.ValidationFilter(self.api, 'rsc', 172 self.fltr = _scheduler.ValidationFilter(self.api)
173 {'profile': 'compute'})
174 173
175 def test_pass(self): 174 def test_pass(self):
176 node = mock.Mock(spec=['uuid', 'name']) 175 node = mock.Mock(spec=['uuid', 'name'])
@@ -195,7 +194,7 @@ class TestIronicReserver(testtools.TestCase):
195 self.node = mock.Mock(spec=['uuid', 'name']) 194 self.node = mock.Mock(spec=['uuid', 'name'])
196 self.api = mock.Mock(spec=['reserve_node', 'release_node']) 195 self.api = mock.Mock(spec=['reserve_node', 'release_node'])
197 self.api.reserve_node.side_effect = lambda node, instance_uuid: node 196 self.api.reserve_node.side_effect = lambda node, instance_uuid: node
198 self.reserver = _scheduler.IronicReserver(self.api, 'rsc', {}) 197 self.reserver = _scheduler.IronicReserver(self.api)
199 198
200 def test_fail(self, mock_validation): 199 def test_fail(self, mock_validation):
201 self.assertRaisesRegex(exceptions.AllNodesReserved, 200 self.assertRaisesRegex(exceptions.AllNodesReserved,
diff --git a/roles/metalsmith_deployment/README.rst b/roles/metalsmith_deployment/README.rst
index 7ac968b..d62a7fc 100644
--- a/roles/metalsmith_deployment/README.rst
+++ b/roles/metalsmith_deployment/README.rst
@@ -17,6 +17,8 @@ The following optional variables provide the defaults for Instance_ attributes:
17 the default for ``candidates``. 17 the default for ``candidates``.
18``metalsmith_capabilities`` 18``metalsmith_capabilities``
19 the default for ``capabilities``. 19 the default for ``capabilities``.
20``metalsmith_conductor_group``
21 the default for ``conductor_group``.
20``metalsmith_extra_args`` 22``metalsmith_extra_args``
21 the default for ``extra_args``. 23 the default for ``extra_args``.
22``metalsmith_image`` 24``metalsmith_image``
@@ -43,6 +45,11 @@ Each instances has the following attributes:
43 list of nodes (UUIDs or names) to be considered for deployment. 45 list of nodes (UUIDs or names) to be considered for deployment.
44``capabilities`` (defaults to ``metalsmith_capabilities``) 46``capabilities`` (defaults to ``metalsmith_capabilities``)
45 node capabilities to request when scheduling. 47 node capabilities to request when scheduling.
48``conductor_group`` (defaults to ``metalsmith_conductor_group``)
49 conductor group to pick nodes from.
50
51 .. note:: Currently it's not possible to specify the default group.
52
46``extra_args`` (defaults to ``metalsmith_extra_args``) 53``extra_args`` (defaults to ``metalsmith_extra_args``)
47 additional arguments to pass to the ``metalsmith`` CLI on all calls. 54 additional arguments to pass to the ``metalsmith`` CLI on all calls.
48``image`` (defaults to ``metalsmith_image``) 55``image`` (defaults to ``metalsmith_image``)
diff --git a/roles/metalsmith_deployment/defaults/main.yml b/roles/metalsmith_deployment/defaults/main.yml
index 6915d17..9e2bf34 100644
--- a/roles/metalsmith_deployment/defaults/main.yml
+++ b/roles/metalsmith_deployment/defaults/main.yml
@@ -1,6 +1,7 @@
1# Optional parameters 1# Optional parameters
2metalsmith_candidates: [] 2metalsmith_candidates: []
3metalsmith_capabilities: {} 3metalsmith_capabilities: {}
4metalsmith_conductor_group:
4metalsmith_extra_args: 5metalsmith_extra_args:
5metalsmith_netboot: false 6metalsmith_netboot: false
6metalsmith_nics: [] 7metalsmith_nics: []
diff --git a/roles/metalsmith_deployment/tasks/main.yml b/roles/metalsmith_deployment/tasks/main.yml
index f6ab396..6273b9b 100644
--- a/roles/metalsmith_deployment/tasks/main.yml
+++ b/roles/metalsmith_deployment/tasks/main.yml
@@ -28,6 +28,9 @@
28 {% if resource_class %} 28 {% if resource_class %}
29 --resource-class {{ resource_class }} 29 --resource-class {{ resource_class }}
30 {% endif %} 30 {% endif %}
31 {% if conductor_group %}
32 --conductor-group {{ conductor_group }}
33 {% endif %}
31 {% for node in candidates %} 34 {% for node in candidates %}
32 --candidate {{ node }} 35 --candidate {{ node }}
33 {% endfor %} 36 {% endfor %}
@@ -35,6 +38,7 @@
35 vars: 38 vars:
36 candidates: "{{ instance.candidates | default(metalsmith_candidates) }}" 39 candidates: "{{ instance.candidates | default(metalsmith_candidates) }}"
37 capabilities: "{{ instance.capabilities | default(metalsmith_capabilities) }}" 40 capabilities: "{{ instance.capabilities | default(metalsmith_capabilities) }}"
41 conductor_group: "{{ instance.conductor_group | default(metalsmith_conductor_group) }}"
38 extra_args: "{{ instance.extra_args | default(metalsmith_extra_args) }}" 42 extra_args: "{{ instance.extra_args | default(metalsmith_extra_args) }}"
39 image: "{{ instance.image | default(metalsmith_image) }}" 43 image: "{{ instance.image | default(metalsmith_image) }}"
40 netboot: "{{ instance.netboot | default(metalsmith_netboot) }}" 44 netboot: "{{ instance.netboot | default(metalsmith_netboot) }}"