Support traits for scheduling
Story: #2003685 Task: #26209 Change-Id: I4895c414abc55ece6cf56133f13ffaa7cd4f5f92
This commit is contained in:
parent
51a006e307
commit
df831309ba
|
@ -124,6 +124,7 @@
|
|||
metalsmith_precreate_port: false
|
||||
metalsmith_partition_image: test-centos-partition
|
||||
metalsmith_whole_disk_image: test-centos-wholedisk
|
||||
metalsmith_traits: [CUSTOM_GOLD]
|
||||
|
||||
- job:
|
||||
name: metalsmith-integration-glance-netboot-cirros-iscsi-py3
|
||||
|
|
|
@ -76,6 +76,7 @@ def _do_deploy(api, args, formatter):
|
|||
node = api.reserve_node(resource_class=args.resource_class,
|
||||
conductor_group=args.conductor_group,
|
||||
capabilities=capabilities,
|
||||
traits=args.trait,
|
||||
candidates=args.candidate)
|
||||
instance = api.provision_node(node,
|
||||
image=source,
|
||||
|
@ -153,7 +154,9 @@ def _parse_args(args, config):
|
|||
help='root disk size (in GiB), defaults to (local_gb '
|
||||
'- 2)')
|
||||
deploy.add_argument('--capability', action='append', metavar='NAME=VALUE',
|
||||
default=[], help='capabilities the nodes should have')
|
||||
default=[], help='capabilities the node should have')
|
||||
deploy.add_argument('--trait', action='append',
|
||||
default=[], help='trait the node should have')
|
||||
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')
|
||||
|
|
|
@ -67,7 +67,8 @@ class Provisioner(object):
|
|||
self._dry_run = dry_run
|
||||
|
||||
def reserve_node(self, resource_class=None, conductor_group=None,
|
||||
capabilities=None, candidates=None, predicate=None):
|
||||
capabilities=None, traits=None, candidates=None,
|
||||
predicate=None):
|
||||
"""Find and reserve a suitable node.
|
||||
|
||||
Example::
|
||||
|
@ -81,6 +82,7 @@ class Provisioner(object):
|
|||
Value ``None`` means any group, use empty string "" for nodes
|
||||
from the default group.
|
||||
:param capabilities: Requested capabilities as a dict.
|
||||
:param traits: Requested traits as a list of strings.
|
||||
: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
|
||||
|
@ -111,15 +113,21 @@ class Provisioner(object):
|
|||
LOG.debug('Candidate nodes: %s', nodes)
|
||||
|
||||
filters.append(_scheduler.CapabilitiesFilter(capabilities))
|
||||
filters.append(_scheduler.TraitsFilter(traits))
|
||||
if predicate is not None:
|
||||
filters.append(_scheduler.CustomPredicateFilter(predicate))
|
||||
|
||||
reserver = _scheduler.IronicReserver(self._api)
|
||||
node = _scheduler.schedule_node(nodes, filters, reserver,
|
||||
dry_run=self._dry_run)
|
||||
|
||||
update = {}
|
||||
if capabilities:
|
||||
node = self._api.update_node(
|
||||
node, {'/instance_info/capabilities': capabilities})
|
||||
update['/instance_info/capabilities'] = capabilities
|
||||
if traits:
|
||||
update['/instance_info/traits'] = traits
|
||||
if update:
|
||||
node = self._api.update_node(node, update)
|
||||
|
||||
LOG.debug('Reserved node: %s', node)
|
||||
return node
|
||||
|
|
|
@ -142,6 +142,9 @@ class CapabilitiesFilter(Filter):
|
|||
self._counter = collections.Counter()
|
||||
|
||||
def __call__(self, node):
|
||||
if not self._capabilities:
|
||||
return True
|
||||
|
||||
try:
|
||||
caps = _utils.get_capabilities(node)
|
||||
except Exception:
|
||||
|
@ -181,6 +184,41 @@ class CapabilitiesFilter(Filter):
|
|||
raise exceptions.CapabilitiesNotFound(message, self._capabilities)
|
||||
|
||||
|
||||
class TraitsFilter(Filter):
|
||||
"""Filter that checks traits."""
|
||||
|
||||
def __init__(self, traits):
|
||||
self._traits = traits
|
||||
self._counter = collections.Counter()
|
||||
|
||||
def __call__(self, node):
|
||||
if not self._traits:
|
||||
return True
|
||||
|
||||
traits = node.traits or []
|
||||
LOG.debug('Traits for node %(node)s: %(traits)s',
|
||||
{'node': _utils.log_node(node), 'traits': traits})
|
||||
for trait in traits:
|
||||
self._counter[trait] += 1
|
||||
|
||||
missing = set(self._traits) - set(traits)
|
||||
if missing:
|
||||
LOG.debug('Node %(node)s does not have traits %(missing)s',
|
||||
{'node': _utils.log_node(node), 'missing': missing})
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def fail(self):
|
||||
existing = ", ".join("%s (%d node(s))" % item
|
||||
for item in self._counter.items())
|
||||
requested = ', '.join(self._traits)
|
||||
message = ("No available nodes found with traits %(req)s, "
|
||||
"existing traits: %(exist)s" %
|
||||
{'req': requested, 'exist': existing or 'none'})
|
||||
raise exceptions.TraitsNotFound(message, self._traits)
|
||||
|
||||
|
||||
class CustomPredicateFilter(Filter):
|
||||
|
||||
def __init__(self, predicate):
|
||||
|
|
|
@ -67,6 +67,17 @@ class CapabilitiesNotFound(ReservationFailed):
|
|||
super(CapabilitiesNotFound, self).__init__(message)
|
||||
|
||||
|
||||
class TraitsNotFound(ReservationFailed):
|
||||
"""Requested traits do not match any nodes.
|
||||
|
||||
:ivar requested_traits: Requested node's traits.
|
||||
"""
|
||||
|
||||
def __init__(self, message, traits):
|
||||
self.requested_traits = traits
|
||||
super(TraitsNotFound, self).__init__(message)
|
||||
|
||||
|
||||
class ValidationFailed(ReservationFailed):
|
||||
"""Validation failed for all requested nodes."""
|
||||
|
||||
|
|
|
@ -58,6 +58,7 @@ class TestDeploy(testtools.TestCase):
|
|||
resource_class='compute',
|
||||
conductor_group=None,
|
||||
capabilities={},
|
||||
traits=[],
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
|
@ -105,6 +106,7 @@ class TestDeploy(testtools.TestCase):
|
|||
resource_class='compute',
|
||||
conductor_group=None,
|
||||
capabilities={},
|
||||
traits=[],
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
|
@ -179,6 +181,7 @@ class TestDeploy(testtools.TestCase):
|
|||
resource_class='compute',
|
||||
conductor_group=None,
|
||||
capabilities={},
|
||||
traits=[],
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
|
@ -203,6 +206,7 @@ class TestDeploy(testtools.TestCase):
|
|||
resource_class='compute',
|
||||
conductor_group=None,
|
||||
capabilities={},
|
||||
traits=[],
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
|
@ -235,6 +239,7 @@ class TestDeploy(testtools.TestCase):
|
|||
resource_class='compute',
|
||||
conductor_group=None,
|
||||
capabilities={},
|
||||
traits=[],
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
|
@ -269,6 +274,7 @@ class TestDeploy(testtools.TestCase):
|
|||
resource_class='compute',
|
||||
conductor_group=None,
|
||||
capabilities={},
|
||||
traits=[],
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
|
@ -301,6 +307,7 @@ class TestDeploy(testtools.TestCase):
|
|||
resource_class='compute',
|
||||
conductor_group=None,
|
||||
capabilities={},
|
||||
traits=[],
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
|
@ -333,6 +340,7 @@ class TestDeploy(testtools.TestCase):
|
|||
resource_class='compute',
|
||||
conductor_group=None,
|
||||
capabilities={},
|
||||
traits=[],
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
|
@ -390,6 +398,32 @@ class TestDeploy(testtools.TestCase):
|
|||
resource_class='compute',
|
||||
conductor_group=None,
|
||||
capabilities={'foo': 'bar', 'answer': '42'},
|
||||
traits=[],
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
mock_pr.return_value.reserve_node.return_value,
|
||||
image='myimg',
|
||||
nics=[{'network': 'mynet'}],
|
||||
root_disk_size=None,
|
||||
config=mock.ANY,
|
||||
hostname=None,
|
||||
netboot=False,
|
||||
wait=1800)
|
||||
|
||||
def test_args_traits(self, mock_os_conf, mock_pr):
|
||||
args = ['deploy', '--network', 'mynet', '--image', 'myimg',
|
||||
'--trait', 'foo:bar', '--trait', 'answer:42',
|
||||
'--resource-class', 'compute']
|
||||
_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='compute',
|
||||
conductor_group=None,
|
||||
capabilities={},
|
||||
traits=['foo:bar', 'answer:42'],
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
|
@ -417,6 +451,7 @@ class TestDeploy(testtools.TestCase):
|
|||
resource_class='compute',
|
||||
conductor_group=None,
|
||||
capabilities={},
|
||||
traits=[],
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
|
@ -443,6 +478,7 @@ class TestDeploy(testtools.TestCase):
|
|||
resource_class='compute',
|
||||
conductor_group=None,
|
||||
capabilities={},
|
||||
traits=[],
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
|
@ -472,6 +508,7 @@ class TestDeploy(testtools.TestCase):
|
|||
resource_class='compute',
|
||||
conductor_group=None,
|
||||
capabilities={},
|
||||
traits=[],
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
|
@ -498,6 +535,7 @@ class TestDeploy(testtools.TestCase):
|
|||
resource_class='compute',
|
||||
conductor_group=None,
|
||||
capabilities={},
|
||||
traits=[],
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
|
@ -520,6 +558,7 @@ class TestDeploy(testtools.TestCase):
|
|||
resource_class='compute',
|
||||
conductor_group=None,
|
||||
capabilities={},
|
||||
traits=[],
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
|
@ -544,6 +583,7 @@ class TestDeploy(testtools.TestCase):
|
|||
resource_class='compute',
|
||||
conductor_group=None,
|
||||
capabilities={},
|
||||
traits=[],
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
|
@ -568,6 +608,7 @@ class TestDeploy(testtools.TestCase):
|
|||
resource_class='compute',
|
||||
conductor_group=None,
|
||||
capabilities={},
|
||||
traits=[],
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
|
@ -591,6 +632,7 @@ class TestDeploy(testtools.TestCase):
|
|||
resource_class=None,
|
||||
conductor_group=None,
|
||||
capabilities={},
|
||||
traits=[],
|
||||
candidates=['node1', 'node2']
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
|
@ -614,6 +656,7 @@ class TestDeploy(testtools.TestCase):
|
|||
resource_class='compute',
|
||||
conductor_group='loc1',
|
||||
capabilities={},
|
||||
traits=[],
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
|
@ -638,6 +681,7 @@ class TestDeploy(testtools.TestCase):
|
|||
resource_class='compute',
|
||||
conductor_group=None,
|
||||
capabilities={},
|
||||
traits=[],
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
|
@ -666,6 +710,7 @@ class TestDeploy(testtools.TestCase):
|
|||
resource_class='compute',
|
||||
conductor_group=None,
|
||||
capabilities={},
|
||||
traits=[],
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
|
@ -703,6 +748,7 @@ class TestDeploy(testtools.TestCase):
|
|||
resource_class='compute',
|
||||
conductor_group=None,
|
||||
capabilities={},
|
||||
traits=[],
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
|
@ -726,6 +772,7 @@ class TestDeploy(testtools.TestCase):
|
|||
resource_class='compute',
|
||||
conductor_group=None,
|
||||
capabilities={},
|
||||
traits=[],
|
||||
candidates=None
|
||||
)
|
||||
mock_pr.return_value.provision_node.assert_called_once_with(
|
||||
|
|
|
@ -29,7 +29,7 @@ from metalsmith import sources
|
|||
|
||||
NODE_FIELDS = ['name', 'uuid', 'instance_info', 'instance_uuid', 'maintenance',
|
||||
'maintenance_reason', 'properties', 'provision_state', 'extra',
|
||||
'last_error']
|
||||
'last_error', 'traits']
|
||||
|
||||
|
||||
class TestInit(testtools.TestCase):
|
||||
|
@ -138,6 +138,23 @@ class TestReserveNode(Base):
|
|||
self.api.update_node.assert_called_once_with(
|
||||
node, {'/instance_info/capabilities': {'answer': '42'}})
|
||||
|
||||
def test_with_traits(self):
|
||||
nodes = [
|
||||
mock.Mock(spec=['uuid', 'name', 'properties'],
|
||||
properties={'local_gb': 100}, traits=traits)
|
||||
for traits in [['foo', 'answer:1'], ['answer:42', 'foo'],
|
||||
['answer'], None]
|
||||
]
|
||||
expected = nodes[1]
|
||||
self.api.list_nodes.return_value = nodes
|
||||
self.api.reserve_node.side_effect = lambda n, instance_uuid: n
|
||||
|
||||
node = self.pr.reserve_node(traits=['foo', 'answer:42'])
|
||||
|
||||
self.assertIs(node, expected)
|
||||
self.api.update_node.assert_called_once_with(
|
||||
node, {'/instance_info/traits': ['foo', 'answer:42']})
|
||||
|
||||
def test_custom_predicate(self):
|
||||
nodes = [
|
||||
mock.Mock(spec=['uuid', 'name', 'properties'],
|
||||
|
|
|
@ -164,6 +164,38 @@ class TestCapabilitiesFilter(testtools.TestCase):
|
|||
fltr.fail)
|
||||
|
||||
|
||||
class TestTraitsFilter(testtools.TestCase):
|
||||
|
||||
def test_fail_no_traits(self):
|
||||
fltr = _scheduler.TraitsFilter(['tr1', 'tr2'])
|
||||
self.assertRaisesRegex(exceptions.TraitsNotFound,
|
||||
'No available nodes found with traits '
|
||||
'tr1, tr2, existing traits: none',
|
||||
fltr.fail)
|
||||
|
||||
def test_no_traits(self):
|
||||
fltr = _scheduler.TraitsFilter([])
|
||||
node = mock.Mock(spec=['name', 'uuid'])
|
||||
self.assertTrue(fltr(node))
|
||||
|
||||
def test_ok(self):
|
||||
fltr = _scheduler.TraitsFilter(['tr1', 'tr2'])
|
||||
node = mock.Mock(spec=['name', 'uuid', 'traits'],
|
||||
traits=['tr3', 'tr2', 'tr1'])
|
||||
self.assertTrue(fltr(node))
|
||||
|
||||
def test_missing_one(self):
|
||||
fltr = _scheduler.TraitsFilter(['tr1', 'tr2'])
|
||||
node = mock.Mock(spec=['name', 'uuid', 'traits'],
|
||||
traits=['tr3', 'tr1'])
|
||||
self.assertFalse(fltr(node))
|
||||
|
||||
def test_missing_all(self):
|
||||
fltr = _scheduler.TraitsFilter(['tr1', 'tr2'])
|
||||
node = mock.Mock(spec=['name', 'uuid', 'traits'], traits=None)
|
||||
self.assertFalse(fltr(node))
|
||||
|
||||
|
||||
class TestIronicReserver(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
|
|
@ -35,6 +35,8 @@ The following optional variables provide the defaults for Instance_ attributes:
|
|||
the default for ``root_size``.
|
||||
``metalsmith_ssh_public_keys``
|
||||
the default for ``ssh_public_keys``.
|
||||
``metalsmith_traits``
|
||||
the default for ``traits``.
|
||||
``metalsmith_user_name``
|
||||
the default for ``user_name``, the default value is ``metalsmith``.
|
||||
|
||||
|
@ -93,6 +95,8 @@ Each instances has the following attributes:
|
|||
|
||||
``ssh_public_keys`` (defaults to ``metalsmith_ssh_public_keys``)
|
||||
list of file names with SSH public keys to put to the node.
|
||||
``traits``
|
||||
list of traits the node should have.
|
||||
``user_name`` (defaults to ``metalsmith_user_name``)
|
||||
name of the user to create on the instance via configdrive. Requires
|
||||
cloud-init_ on the image.
|
||||
|
@ -121,6 +125,8 @@ Example
|
|||
root_size: 100
|
||||
capabilities:
|
||||
boot_mode: uefi
|
||||
traits:
|
||||
- CUSTOM_GPU
|
||||
- hostname: compute-1
|
||||
resource_class: compute
|
||||
root_size: 100
|
||||
|
|
|
@ -8,6 +8,7 @@ metalsmith_netboot: false
|
|||
metalsmith_nics: []
|
||||
metalsmith_resource_class:
|
||||
metalsmith_root_size:
|
||||
metalsmith_traits: []
|
||||
metalsmith_ssh_public_keys: []
|
||||
metalsmith_user_name: metalsmith
|
||||
|
||||
|
|
|
@ -6,6 +6,9 @@
|
|||
{% for cap_name, cap_value in capabilities.items() %}
|
||||
--capability {{ cap_name }}={{ cap_value }}
|
||||
{% endfor %}
|
||||
{% for trait in traits %}
|
||||
--trait {{ trait }}
|
||||
{% endfor %}
|
||||
{% for nic in nics %}
|
||||
{% for nic_type, nic_value in nic.items() %}
|
||||
--{{ nic_type }} {{ nic_value }}
|
||||
|
@ -51,6 +54,7 @@
|
|||
root_size: "{{ instance.root_size | default(metalsmith_root_size) }}"
|
||||
ssh_public_keys: "{{ instance.ssh_public_keys | default(metalsmith_ssh_public_keys) }}"
|
||||
state: "{{ instance.state | default('present') }}"
|
||||
traits: "{{ instance.traits | default(metalsmith_traits) }}"
|
||||
user_name: "{{ instance.user_name | default(metalsmith_user_name) }}"
|
||||
with_items: "{{ metalsmith_instances }}"
|
||||
loop_control:
|
||||
|
|
Loading…
Reference in New Issue