Allow specifying a list of nodes to pick from
Makes resource class no longer mandatory. Change-Id: If14d5846e7b50a867950ae439985bbe877998bc7 Story: #2002171 Task: #20034
This commit is contained in:
parent
2d6ccf26d8
commit
5aacd7bbdb
|
@ -56,7 +56,8 @@ def _do_deploy(api, args, formatter):
|
|||
if args.user_name:
|
||||
config.add_user(args.user_name, sudo=args.passwordless_sudo)
|
||||
|
||||
node = api.reserve_node(args.resource_class, capabilities=capabilities)
|
||||
node = api.reserve_node(args.resource_class, capabilities=capabilities,
|
||||
candidates=args.candidate)
|
||||
instance = api.provision_node(node,
|
||||
image=args.image,
|
||||
nics=args.nics,
|
||||
|
@ -130,8 +131,11 @@ def _parse_args(args, config):
|
|||
deploy.add_argument('--ssh-public-key', help='SSH public key to load')
|
||||
deploy.add_argument('--hostname', help='Host name to use, defaults to '
|
||||
'Node\'s name or UUID')
|
||||
deploy.add_argument('--resource-class', required=True,
|
||||
deploy.add_argument('--resource-class',
|
||||
help='node resource class to deploy')
|
||||
deploy.add_argument('--candidate', action='append',
|
||||
help='A candidate node to use for scheduling (can be '
|
||||
'specified several times)')
|
||||
deploy.add_argument('--user-name', help='Name of the admin user to create')
|
||||
deploy.add_argument('--passwordless-sudo', action='store_true',
|
||||
help='allow password-less sudo for the user')
|
||||
|
|
|
@ -49,7 +49,8 @@ class Provisioner(object):
|
|||
self._api = _os_api.API(session=session, cloud_region=cloud_region)
|
||||
self._dry_run = dry_run
|
||||
|
||||
def reserve_node(self, resource_class, capabilities=None):
|
||||
def reserve_node(self, resource_class=None, capabilities=None,
|
||||
candidates=None):
|
||||
"""Find and reserve a suitable node.
|
||||
|
||||
Example::
|
||||
|
@ -57,20 +58,32 @@ class Provisioner(object):
|
|||
node = provisioner.reserve_node("compute",
|
||||
capabilities={"boot_mode": "uefi"})
|
||||
|
||||
:param resource_class: Requested resource class.
|
||||
:param resource_class: Requested resource class. If ``None``, a node
|
||||
with any resource class can be chosen.
|
||||
:param capabilities: Requested capabilities as a dict.
|
||||
:param candidates: List of nodes (UUIDs, names or `Node` objects)
|
||||
to pick from. The filters (for resource class and capabilities)
|
||||
are still applied to the provided list. The order in which
|
||||
the nodes are considered is retained.
|
||||
:return: reserved `Node` object.
|
||||
:raises: :py:class:`metalsmith.exceptions.ReservationFailed`
|
||||
"""
|
||||
capabilities = capabilities or {}
|
||||
|
||||
nodes = self._api.list_nodes(resource_class=resource_class)
|
||||
if candidates:
|
||||
nodes = [self._api.get_node(node) for node in candidates]
|
||||
if resource_class:
|
||||
nodes = [node for node in nodes
|
||||
if node.resource_class == resource_class]
|
||||
else:
|
||||
nodes = self._api.list_nodes(resource_class=resource_class)
|
||||
# Ensure parallel executions don't try nodes in the same sequence
|
||||
random.shuffle(nodes)
|
||||
|
||||
if not nodes:
|
||||
raise exceptions.ResourceClassNotFound(resource_class,
|
||||
capabilities)
|
||||
|
||||
# Make sure parallel executions don't try nodes in the same sequence
|
||||
random.shuffle(nodes)
|
||||
LOG.debug('Ironic nodes: %s', nodes)
|
||||
|
||||
filters = [_scheduler.CapabilitiesFilter(resource_class, capabilities),
|
||||
|
|
|
@ -55,7 +55,8 @@ class TestDeploy(testtools.TestCase):
|
|||
dry_run=False)
|
||||
mock_pr.return_value.reserve_node.assert_called_once_with(
|
||||
resource_class='compute',
|
||||
capabilities={}
|
||||
capabilities={},
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
mock_pr.return_value.reserve_node.return_value,
|
||||
|
@ -100,7 +101,8 @@ class TestDeploy(testtools.TestCase):
|
|||
dry_run=False)
|
||||
mock_pr.return_value.reserve_node.assert_called_once_with(
|
||||
resource_class='compute',
|
||||
capabilities={}
|
||||
capabilities={},
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
mock_pr.return_value.reserve_node.return_value,
|
||||
|
@ -172,7 +174,8 @@ class TestDeploy(testtools.TestCase):
|
|||
dry_run=True)
|
||||
mock_pr.return_value.reserve_node.assert_called_once_with(
|
||||
resource_class='compute',
|
||||
capabilities={}
|
||||
capabilities={},
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
mock_pr.return_value.reserve_node.return_value,
|
||||
|
@ -194,7 +197,8 @@ class TestDeploy(testtools.TestCase):
|
|||
dry_run=False)
|
||||
mock_pr.return_value.reserve_node.assert_called_once_with(
|
||||
resource_class='compute',
|
||||
capabilities={}
|
||||
capabilities={},
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
mock_pr.return_value.reserve_node.return_value,
|
||||
|
@ -224,7 +228,8 @@ class TestDeploy(testtools.TestCase):
|
|||
dry_run=False)
|
||||
mock_pr.return_value.reserve_node.assert_called_once_with(
|
||||
resource_class='compute',
|
||||
capabilities={}
|
||||
capabilities={},
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
mock_pr.return_value.reserve_node.return_value,
|
||||
|
@ -256,7 +261,8 @@ class TestDeploy(testtools.TestCase):
|
|||
dry_run=False)
|
||||
mock_pr.return_value.reserve_node.assert_called_once_with(
|
||||
resource_class='compute',
|
||||
capabilities={}
|
||||
capabilities={},
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
mock_pr.return_value.reserve_node.return_value,
|
||||
|
@ -286,7 +292,8 @@ class TestDeploy(testtools.TestCase):
|
|||
dry_run=False)
|
||||
mock_pr.return_value.reserve_node.assert_called_once_with(
|
||||
resource_class='compute',
|
||||
capabilities={}
|
||||
capabilities={},
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
mock_pr.return_value.reserve_node.return_value,
|
||||
|
@ -316,7 +323,8 @@ class TestDeploy(testtools.TestCase):
|
|||
dry_run=False)
|
||||
mock_pr.return_value.reserve_node.assert_called_once_with(
|
||||
resource_class='compute',
|
||||
capabilities={}
|
||||
capabilities={},
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
mock_pr.return_value.reserve_node.return_value,
|
||||
|
@ -371,7 +379,8 @@ class TestDeploy(testtools.TestCase):
|
|||
dry_run=False)
|
||||
mock_pr.return_value.reserve_node.assert_called_once_with(
|
||||
resource_class='compute',
|
||||
capabilities={'foo': 'bar', 'answer': '42'}
|
||||
capabilities={'foo': 'bar', 'answer': '42'},
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
mock_pr.return_value.reserve_node.return_value,
|
||||
|
@ -396,7 +405,8 @@ class TestDeploy(testtools.TestCase):
|
|||
dry_run=False)
|
||||
mock_pr.return_value.reserve_node.assert_called_once_with(
|
||||
resource_class='compute',
|
||||
capabilities={}
|
||||
capabilities={},
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
mock_pr.return_value.reserve_node.return_value,
|
||||
|
@ -420,7 +430,8 @@ class TestDeploy(testtools.TestCase):
|
|||
dry_run=False)
|
||||
mock_pr.return_value.reserve_node.assert_called_once_with(
|
||||
resource_class='compute',
|
||||
capabilities={}
|
||||
capabilities={},
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
mock_pr.return_value.reserve_node.return_value,
|
||||
|
@ -447,7 +458,8 @@ class TestDeploy(testtools.TestCase):
|
|||
dry_run=False)
|
||||
mock_pr.return_value.reserve_node.assert_called_once_with(
|
||||
resource_class='compute',
|
||||
capabilities={}
|
||||
capabilities={},
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
mock_pr.return_value.reserve_node.return_value,
|
||||
|
@ -471,7 +483,8 @@ class TestDeploy(testtools.TestCase):
|
|||
dry_run=False)
|
||||
mock_pr.return_value.reserve_node.assert_called_once_with(
|
||||
resource_class='compute',
|
||||
capabilities={}
|
||||
capabilities={},
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
mock_pr.return_value.reserve_node.return_value,
|
||||
|
@ -491,7 +504,8 @@ class TestDeploy(testtools.TestCase):
|
|||
dry_run=False)
|
||||
mock_pr.return_value.reserve_node.assert_called_once_with(
|
||||
resource_class='compute',
|
||||
capabilities={}
|
||||
capabilities={},
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
mock_pr.return_value.reserve_node.return_value,
|
||||
|
@ -513,7 +527,8 @@ class TestDeploy(testtools.TestCase):
|
|||
dry_run=False)
|
||||
mock_pr.return_value.reserve_node.assert_called_once_with(
|
||||
resource_class='compute',
|
||||
capabilities={}
|
||||
capabilities={},
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
mock_pr.return_value.reserve_node.return_value,
|
||||
|
@ -535,7 +550,30 @@ class TestDeploy(testtools.TestCase):
|
|||
dry_run=False)
|
||||
mock_pr.return_value.reserve_node.assert_called_once_with(
|
||||
resource_class='compute',
|
||||
capabilities={}
|
||||
capabilities={},
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
mock_pr.return_value.reserve_node.return_value,
|
||||
image='myimg',
|
||||
nics=None,
|
||||
root_disk_size=None,
|
||||
config=mock.ANY,
|
||||
hostname='host',
|
||||
netboot=False,
|
||||
wait=1800)
|
||||
|
||||
def test_args_with_candidates(self, mock_os_conf, mock_pr):
|
||||
args = ['deploy', '--hostname', 'host', '--image', 'myimg',
|
||||
'--candidate', 'node1', '--candidate', 'node2']
|
||||
_cmd.main(args)
|
||||
mock_pr.assert_called_once_with(
|
||||
cloud_region=mock_os_conf.return_value.get_one.return_value,
|
||||
dry_run=False)
|
||||
mock_pr.return_value.reserve_node.assert_called_once_with(
|
||||
resource_class=None,
|
||||
capabilities={},
|
||||
candidates=['node1', 'node2']
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
mock_pr.return_value.reserve_node.return_value,
|
||||
|
@ -556,7 +594,8 @@ class TestDeploy(testtools.TestCase):
|
|||
dry_run=False)
|
||||
mock_pr.return_value.reserve_node.assert_called_once_with(
|
||||
resource_class='compute',
|
||||
capabilities={}
|
||||
capabilities={},
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
mock_pr.return_value.reserve_node.return_value,
|
||||
|
@ -577,7 +616,8 @@ class TestDeploy(testtools.TestCase):
|
|||
dry_run=False)
|
||||
mock_pr.return_value.reserve_node.assert_called_once_with(
|
||||
resource_class='compute',
|
||||
capabilities={}
|
||||
capabilities={},
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
mock_pr.return_value.reserve_node.return_value,
|
||||
|
|
|
@ -75,6 +75,19 @@ class TestReserveNode(Base):
|
|||
self.assertIn(node, nodes)
|
||||
self.assertFalse(self.api.update_node.called)
|
||||
|
||||
def test_any_resource_class(self):
|
||||
nodes = [
|
||||
mock.Mock(spec=['uuid', 'name', 'properties'],
|
||||
properties={'local_gb': 100})
|
||||
]
|
||||
self.api.list_nodes.return_value = nodes
|
||||
self.api.reserve_node.side_effect = lambda n, instance_uuid: n
|
||||
|
||||
node = self.pr.reserve_node()
|
||||
|
||||
self.assertIn(node, nodes)
|
||||
self.assertFalse(self.api.update_node.called)
|
||||
|
||||
def test_with_capabilities(self):
|
||||
nodes = [
|
||||
mock.Mock(spec=['uuid', 'name', 'properties'],
|
||||
|
@ -91,6 +104,54 @@ class TestReserveNode(Base):
|
|||
self.api.update_node.assert_called_once_with(
|
||||
node, {'/instance_info/capabilities': {'answer': '42'}})
|
||||
|
||||
def test_provided_node(self):
|
||||
nodes = [
|
||||
mock.Mock(spec=['uuid', 'name', 'properties'],
|
||||
properties={'local_gb': 100})
|
||||
]
|
||||
self.api.reserve_node.side_effect = lambda n, instance_uuid: n
|
||||
|
||||
node = self.pr.reserve_node(candidates=nodes)
|
||||
|
||||
self.assertEqual(node, nodes[0])
|
||||
self.assertFalse(self.api.list_nodes.called)
|
||||
self.assertFalse(self.api.update_node.called)
|
||||
|
||||
def test_provided_nodes(self):
|
||||
nodes = [
|
||||
mock.Mock(spec=['uuid', 'name', 'properties'],
|
||||
properties={'local_gb': 100}),
|
||||
mock.Mock(spec=['uuid', 'name', 'properties'],
|
||||
properties={'local_gb': 100})
|
||||
]
|
||||
self.api.reserve_node.side_effect = lambda n, instance_uuid: n
|
||||
|
||||
node = self.pr.reserve_node(candidates=nodes)
|
||||
|
||||
self.assertEqual(node, nodes[0])
|
||||
self.assertFalse(self.api.list_nodes.called)
|
||||
self.assertFalse(self.api.update_node.called)
|
||||
|
||||
def test_nodes_filtered(self):
|
||||
nodes = [
|
||||
mock.Mock(spec=['uuid', 'name', 'properties', 'resource_class'],
|
||||
properties={'local_gb': 100}, resource_class='banana'),
|
||||
mock.Mock(spec=['uuid', 'name', 'properties', 'resource_class'],
|
||||
properties={'local_gb': 100}, resource_class='compute'),
|
||||
mock.Mock(spec=['uuid', 'name', 'properties', 'resource_class'],
|
||||
properties={'local_gb': 100, 'capabilities': 'cat:meow'},
|
||||
resource_class='compute'),
|
||||
]
|
||||
self.api.reserve_node.side_effect = lambda n, instance_uuid: n
|
||||
|
||||
node = self.pr.reserve_node('compute', candidates=nodes,
|
||||
capabilities={'cat': 'meow'})
|
||||
|
||||
self.assertEqual(node, nodes[2])
|
||||
self.assertFalse(self.api.list_nodes.called)
|
||||
self.api.update_node.assert_called_once_with(
|
||||
node, {'/instance_info/capabilities': {'cat': 'meow'}})
|
||||
|
||||
|
||||
CLEAN_UP = {
|
||||
'/extra/metalsmith_created_ports': _os_api.REMOVE,
|
||||
|
|
|
@ -13,12 +13,14 @@ The only required variable is:
|
|||
|
||||
The following optional variables provide the defaults for Instance_ attributes:
|
||||
|
||||
``metalsmith_candidates``
|
||||
the default for ``candidates``.
|
||||
``metalsmith_capabilities``
|
||||
the default for ``capabilities``.
|
||||
``metalsmith_extra_args``
|
||||
the default for ``extra_args``.
|
||||
``metalsmith_image``
|
||||
the default for ``image``.
|
||||
``metalsmith_capabilities``
|
||||
the default for ``capabilities``.
|
||||
``metalsmith_netboot``
|
||||
the default for ``netboot``
|
||||
``metalsmith_nics``
|
||||
|
@ -35,12 +37,14 @@ Instance
|
|||
|
||||
Each instances has the following attributes:
|
||||
|
||||
``candidates`` (defaults to ``metalsmith_candidates``)
|
||||
list of nodes (UUIDs or names) to be considered for deployment.
|
||||
``capabilities`` (defaults to ``metalsmith_capabilities``)
|
||||
node capabilities to request when scheduling.
|
||||
``extra_args`` (defaults to ``metalsmith_extra_args``)
|
||||
additional arguments to pass to the ``metalsmith`` CLI on all calls.
|
||||
``image`` (defaults to ``metalsmith_image``)
|
||||
UUID or name of the image to use for deployment. Mandatory.
|
||||
``capabilities`` (defaults to ``metalsmith_capabilities``)
|
||||
node capabilities to request when scheduling.
|
||||
``netboot``
|
||||
whether to boot the deployed instance from network (PXE, iPXE, etc).
|
||||
The default is to use local boot (requires a bootloader on the image).
|
||||
|
@ -66,7 +70,7 @@ Each instances has the following attributes:
|
|||
- port: b2254316-7867-4615-9fb7-911b3f38ca2a
|
||||
|
||||
``resource_class`` (defaults to ``metalsmith_resource_class``)
|
||||
requested node's resource class. Mandatory.
|
||||
requested node's resource class.
|
||||
``root_size`` (defaults to ``metalsmith_root_size``)
|
||||
size of the root partition, if partition images are used.
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
# Optional parameters
|
||||
metalsmith_candidates: []
|
||||
metalsmith_capabilities: {}
|
||||
metalsmith_extra_args:
|
||||
metalsmith_netboot: false
|
||||
metalsmith_nics: []
|
||||
metalsmith_resource_class:
|
||||
metalsmith_root_size:
|
||||
metalsmith_ssh_public_keys: []
|
||||
metalsmith_user_name: metalsmith
|
||||
|
|
|
@ -25,12 +25,18 @@
|
|||
{% if user_name %}
|
||||
--user-name {{ user_name }}
|
||||
{% endif %}
|
||||
{% if resource_class %}
|
||||
--resource-class {{ resource_class }}
|
||||
{% endif %}
|
||||
{% for node in candidates %}
|
||||
--candidate {{ node }}
|
||||
{% endfor %}
|
||||
when: state == 'present'
|
||||
vars:
|
||||
candidates: "{{ instance.candidates | default(metalsmith_candidates) }}"
|
||||
capabilities: "{{ instance.capabilities | default(metalsmith_capabilities) }}"
|
||||
extra_args: "{{ instance.extra_args | default(metalsmith_extra_args) }}"
|
||||
image: "{{ instance.image | default(metalsmith_image) }}"
|
||||
capabilities: "{{ instance.capabilities | default(metalsmith_capabilities) }}"
|
||||
netboot: "{{ instance.netboot | default(metalsmith_netboot) }}"
|
||||
nics: "{{ instance.nics | default(metalsmith_nics) }}"
|
||||
resource_class: "{{ instance.resource_class | default(metalsmith_resource_class) }}"
|
||||
|
|
Loading…
Reference in New Issue