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:
Dmitry Tantsur 2018-07-05 18:22:43 +02:00
parent 2d6ccf26d8
commit 5aacd7bbdb
7 changed files with 161 additions and 31 deletions

View File

@ -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')

View File

@ -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),

View File

@ -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,

View File

@ -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,

View File

@ -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.

View File

@ -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

View File

@ -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) }}"