Use Mistral for baremetal registration

Updates the baremetal registration workflows to use Mistral
instead of python.

Co-Authored-By: Dougal Matthews <dougal@redhat.com>
Co-Authored-By: Ryan Brady <rbrady@redhat.com>

Change-Id: Ide8b7753829170f503ef962b4ad4fde388cbb0ba
Depends-On: Ifc6bdd273a8e129ea7c4269d00add64e72cd371b
Depends-On: I910f50a377bcbc2c23b527953e9df7eee9c938a4
This commit is contained in:
Dan Prince 2016-05-29 22:09:17 -04:00 committed by Dougal Matthews
parent 13c2afda86
commit 9244349742
10 changed files with 329 additions and 109 deletions

View File

@ -10,6 +10,7 @@ passlib>=1.6 # BSD
python-ironic-inspector-client>=1.5.0 # Apache-2.0
python-heatclient>=1.1.0 # Apache-2.0
python-ironicclient>=1.1.0 # Apache-2.0
python-mistralclient>=2.0.0 # Apache-2.0
python-openstackclient>=2.1.0 # Apache-2.0
six>=1.9.0 # MIT
os-cloud-config # Apache-2.0

View File

@ -31,6 +31,11 @@ class UnknownService(Exception):
pass
class WorkflowServiceError(Exception):
"""The service type is unknown"""
pass
class NotFound(Exception):
"""Resource not found"""
pass
@ -54,6 +59,14 @@ class IntrospectionError(RuntimeError):
"""Introspection failed"""
class RegisterOrUpdateError(WorkflowServiceError):
"""Introspection failed"""
class NodeProvideError(WorkflowServiceError):
"""Node Provide failed."""
class StateTransitionFailed(Exception):
"""Ironic node state transition failed"""

View File

@ -34,6 +34,8 @@ class FakeApp(object):
class FakeClientManager(object):
def __init__(self):
self.identity = None
self.workflow_engine = None
self.tripleoclient = None
self.auth_ref = None
self.tripleoclient = FakeClientWrapper()

View File

@ -13,8 +13,9 @@
# under the License.
#
import ironic_inspector_client
import mock
import ironic_inspector_client
from openstackclient.tests import utils
@ -90,6 +91,19 @@ class FakeInspectorClient(object):
return {uuid: self.states[uuid] for uuid in uuids}
class ClientWrapper(object):
def __init__(self):
self._instance = None
self._mock_websocket = mock.Mock()
self._mock_websocket.__enter__ = mock.Mock(
return_value=self._mock_websocket)
self._mock_websocket.__exit__ = mock.Mock()
def messaging_websocket(self, queue_name='tripleo'):
return self._mock_websocket
class TestBaremetal(utils.TestCommand):
def setUp(self):
@ -101,3 +115,10 @@ class TestBaremetal(utils.TestCommand):
self.app.client_manager.baremetal_introspection = FakeInspectorClient()
self.app.client_manager._region_name = "Arcadia"
self.app.client_manager.session = mock.Mock()
self.app.client_manager.workflow_engine = mock.Mock()
self.app.client_manager.tripleoclient = ClientWrapper()
def tearDown(self):
super(TestBaremetal, self).tearDown()
mock.patch.stopall()

View File

@ -339,10 +339,24 @@ pxe_ssh,192.168.122.2,stack,"KEY2",00:0b:d0:69:7e:58""")
}
)
self.mock_initial_nodes = {
mock.Mock(uuid="ABCDEFGH", provision_state="enroll"),
mock.Mock(uuid="IJKLMNOP", provision_state="enroll")
}
self.mock_websocket_success = [{
"status": "SUCCESS",
"registered_nodes": [{
"uuid": "MOCK_NODE_UUID"
}],
}, {
"status": "SUCCESS"
}]
self.workflow = self.app.client_manager.workflow_engine
tripleoclient = self.app.client_manager.tripleoclient
websocket = tripleoclient.messaging_websocket()
websocket.wait_for_message.side_effect = self.mock_websocket_success
self.websocket = websocket
uuid4_patcher = mock.patch('uuid.uuid4', return_value="UUID4")
self.mock_uuid4 = uuid4_patcher.start()
self.addCleanup(self.mock_uuid4.stop)
def tearDown(self):
@ -353,25 +367,38 @@ pxe_ssh,192.168.122.2,stack,"KEY2",00:0b:d0:69:7e:58""")
os.unlink(self.yaml_file.name)
os.unlink(self.instack_yaml.name)
def _check_register_call(self, mock_register_nodes, local=True, **kwargs):
def _check_workflow_call(self, local=True, provide=True,
kernel_name='bm-deploy-kernel',
ramdisk_name='bm-deploy-ramdisk'):
nodes_list = copy.deepcopy(self.nodes_list)
for node in nodes_list:
if local:
node['capabilities'] = 'boot_option:local'
else:
node['capabilities'] = 'boot_option:netboot'
kwargs.setdefault('kernel_name', 'bm-deploy-kernel')
kwargs.setdefault('ramdisk_name', 'bm-deploy-ramdisk')
mock_register_nodes.assert_called_with(
'http://localhost', nodes_list,
client=self.app.client_manager.baremetal,
keystone_client=None,
glance_client=self.app.client_manager.image,
**kwargs)
call_list = [mock.call(
'tripleo.baremetal.v1.register_or_update', workflow_input={
'kernel_name': kernel_name,
'nodes_json': nodes_list,
'queue_name': 'UUID4',
'ramdisk_name': ramdisk_name}
)]
@mock.patch('tripleo_common.utils.nodes.register_all_nodes', autospec=True)
def test_json_import(self, mock_register_nodes):
if provide:
call_list.append(mock.call(
'tripleo.baremetal.v1.provide', workflow_input={
'node_uuids': ['MOCK_NODE_UUID', ],
'queue_name': 'UUID4'
}
))
self.workflow.executions.create.assert_has_calls(call_list)
self.assertEqual(self.workflow.executions.create.call_count,
2 if provide else 1)
def test_json_import(self):
arglist = [self.json_file.name, '--json', '-s', 'http://localhost']
@ -381,18 +408,12 @@ pxe_ssh,192.168.122.2,stack,"KEY2",00:0b:d0:69:7e:58""")
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
mock_register_nodes.return_value = self.mock_initial_nodes
self.cmd.take_action(parsed_args)
self._check_register_call(mock_register_nodes)
self.assertEqual(sorted([
('ABCDEFGH', 'manage'), ('IJKLMNOP', 'manage'),
('ABCDEFGH', 'provide'), ('IJKLMNOP', 'provide')
]), sorted(self.baremetal.node.updates))
self._check_workflow_call()
@mock.patch('tripleo_common.utils.nodes.register_all_nodes', autospec=True)
def test_json_import_initial_state_enroll(self, mock_register_nodes):
def test_json_import_initial_state_enroll(self):
arglist = [
self.json_file.name,
@ -407,14 +428,12 @@ pxe_ssh,192.168.122.2,stack,"KEY2",00:0b:d0:69:7e:58""")
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
mock_register_nodes.return_value = self.mock_initial_nodes
self.cmd.take_action(parsed_args)
self._check_register_call(mock_register_nodes)
self._check_workflow_call(provide=False)
self.assertEqual([], self.baremetal.node.updates)
@mock.patch('tripleo_common.utils.nodes.register_all_nodes', autospec=True)
def test_available_does_not_require_api_1_11(self, mock_register_nodes):
def test_available_does_not_require_api_1_11(self):
arglist = [self.json_file.name, '--json', '-s', 'http://localhost']
verifylist = [
@ -423,15 +442,9 @@ pxe_ssh,192.168.122.2,stack,"KEY2",00:0b:d0:69:7e:58""")
]
self.baremetal.http_client.os_ironic_api_version = '1.6'
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
mock_register_nodes.return_value = self.mock_initial_nodes
self.cmd.take_action(parsed_args)
self._check_register_call(mock_register_nodes)
self.assertEqual(sorted([
('ABCDEFGH', 'manage'), ('IJKLMNOP', 'manage'),
('ABCDEFGH', 'provide'), ('IJKLMNOP', 'provide')
]), sorted(self.baremetal.node.updates))
self._check_workflow_call()
def test_enroll_requires_api_1_11(self):
arglist = [
@ -450,9 +463,9 @@ pxe_ssh,192.168.122.2,stack,"KEY2",00:0b:d0:69:7e:58""")
self.assertRaisesRegexp(exceptions.InvalidConfiguration,
'OS_BAREMETAL_API_VERSION',
self.cmd.take_action, parsed_args)
self.workflow.executions.create.assert_not_called()
@mock.patch('tripleo_common.utils.nodes.register_all_nodes', autospec=True)
def test_json_import_detect_suffix(self, mock_register_nodes):
def test_json_import_detect_suffix(self):
arglist = [self.json_file.name, '-s', 'http://localhost']
@ -465,10 +478,9 @@ pxe_ssh,192.168.122.2,stack,"KEY2",00:0b:d0:69:7e:58""")
self.cmd.take_action(parsed_args)
self._check_register_call(mock_register_nodes)
self._check_workflow_call()
@mock.patch('tripleo_common.utils.nodes.register_all_nodes', autospec=True)
def test_instack_json_import(self, mock_register_nodes):
def test_instack_json_import(self):
arglist = [self.instack_json.name, '--json', '-s', 'http://localhost']
@ -478,18 +490,12 @@ pxe_ssh,192.168.122.2,stack,"KEY2",00:0b:d0:69:7e:58""")
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
mock_register_nodes.return_value = self.mock_initial_nodes
self.cmd.take_action(parsed_args)
self._check_register_call(mock_register_nodes)
self.assertEqual(sorted([
('ABCDEFGH', 'manage'), ('IJKLMNOP', 'manage'),
('ABCDEFGH', 'provide'), ('IJKLMNOP', 'provide')
]), sorted(self.baremetal.node.updates))
self._check_workflow_call()
@mock.patch('tripleo_common.utils.nodes.register_all_nodes', autospec=True)
def test_csv_import(self, mock_register_nodes):
def test_csv_import(self):
arglist = [self.csv_file.name, '--csv', '-s', 'http://localhost']
@ -499,14 +505,12 @@ pxe_ssh,192.168.122.2,stack,"KEY2",00:0b:d0:69:7e:58""")
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
mock_register_nodes.return_value = self.mock_initial_nodes
self.cmd.take_action(parsed_args)
self._check_register_call(mock_register_nodes)
self._check_workflow_call()
@mock.patch('tripleo_common.utils.nodes.register_all_nodes', autospec=True)
def test_csv_import_detect_suffix(self, mock_register_nodes):
def test_csv_import_detect_suffix(self):
arglist = [self.csv_file.name, '-s', 'http://localhost']
@ -519,10 +523,9 @@ pxe_ssh,192.168.122.2,stack,"KEY2",00:0b:d0:69:7e:58""")
self.cmd.take_action(parsed_args)
self._check_register_call(mock_register_nodes)
self._check_workflow_call()
@mock.patch('tripleo_common.utils.nodes.register_all_nodes', autospec=True)
def test_yaml_import(self, mock_register_nodes):
def test_yaml_import(self):
arglist = [self.yaml_file.name, '-s', 'http://localhost']
@ -535,7 +538,7 @@ pxe_ssh,192.168.122.2,stack,"KEY2",00:0b:d0:69:7e:58""")
self.cmd.take_action(parsed_args)
self._check_register_call(mock_register_nodes)
self._check_workflow_call()
def test_invalid_import_filetype(self):
@ -552,8 +555,7 @@ pxe_ssh,192.168.122.2,stack,"KEY2",00:0b:d0:69:7e:58""")
'Invalid file extension',
self.cmd.take_action, parsed_args)
@mock.patch('tripleo_common.utils.nodes.register_all_nodes', autospec=True)
def test_instack_yaml_import(self, mock_register_nodes):
def test_instack_yaml_import(self):
arglist = [self.instack_yaml.name, '-s', 'http://localhost']
@ -563,18 +565,12 @@ pxe_ssh,192.168.122.2,stack,"KEY2",00:0b:d0:69:7e:58""")
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
mock_register_nodes.return_value = self.mock_initial_nodes
self.cmd.take_action(parsed_args)
self._check_register_call(mock_register_nodes)
self.assertEqual(sorted([
('ABCDEFGH', 'manage'), ('IJKLMNOP', 'manage'),
('ABCDEFGH', 'provide'), ('IJKLMNOP', 'provide')
]), sorted(self.baremetal.node.updates))
self._check_workflow_call()
@mock.patch('tripleo_common.utils.nodes.register_all_nodes', autospec=True)
def test_netboot(self, mock_register_nodes):
def test_netboot(self):
arglist = [self.json_file.name, '-s', 'http://localhost',
'--instance-boot-option', 'netboot']
@ -587,10 +583,9 @@ pxe_ssh,192.168.122.2,stack,"KEY2",00:0b:d0:69:7e:58""")
self.cmd.take_action(parsed_args)
self._check_register_call(mock_register_nodes, local=False)
self._check_workflow_call(local=False)
@mock.patch('tripleo_common.utils.nodes.register_all_nodes', autospec=True)
def test_custom_image(self, mock_register_nodes):
def test_custom_image(self):
arglist = [self.json_file.name, '-s', 'http://localhost',
'--deploy-kernel', 'k', '--deploy-ramdisk', 'r']
@ -604,11 +599,9 @@ pxe_ssh,192.168.122.2,stack,"KEY2",00:0b:d0:69:7e:58""")
self.cmd.take_action(parsed_args)
self._check_register_call(mock_register_nodes, kernel_name='k',
ramdisk_name='r')
self._check_workflow_call(kernel_name='k', ramdisk_name='r')
@mock.patch('tripleo_common.utils.nodes.register_all_nodes', autospec=True)
def test_no_image(self, mock_register_nodes):
def test_no_image(self):
arglist = [self.json_file.name, '-s', 'http://localhost',
'--no-deploy-image']
@ -621,8 +614,7 @@ pxe_ssh,192.168.122.2,stack,"KEY2",00:0b:d0:69:7e:58""")
self.cmd.take_action(parsed_args)
self._check_register_call(mock_register_nodes, kernel_name=None,
ramdisk_name=None)
self._check_workflow_call(kernel_name=None, ramdisk_name=None)
class TestStartBaremetalIntrospectionBulk(fakes.TestBaremetal):

View File

@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mock
from openstackclient.tests import utils
from tripleoclient import exceptions
from tripleoclient.workflows import baremetal
class TestBaremetalWorkflows(utils.TestCommand):
def setUp(self):
super(TestBaremetalWorkflows, self).setUp()
self.app.client_manager.workflow_engine = self.workflow = mock.Mock()
self.tripleoclient = mock.Mock()
self.websocket = mock.Mock()
self.websocket.__enter__ = lambda s: self.websocket
self.websocket.__exit__ = lambda s, *exc: None
self.tripleoclient.messaging_websocket.return_value = self.websocket
self.app.client_manager.tripleoclient = self.tripleoclient
def test_register_or_update_success(self):
self.websocket.wait_for_message.return_value = {
"status": "SUCCESS",
"registered_nodes": [],
}
self.assertEqual(baremetal.register_or_update(
self.app.client_manager,
nodes_json=[],
queue_name="QUEUE_NAME",
kernel_name="kernel",
ramdisk_name="ramdisk"
), [])
self.workflow.executions.create.assert_called_once_with(
'tripleo.baremetal.v1.register_or_update',
workflow_input={
'kernel_name': 'kernel',
'queue_name': 'QUEUE_NAME',
'nodes_json': [],
'ramdisk_name': 'ramdisk'
})
def test_register_or_update_error(self):
self.websocket.wait_for_message.return_value = {
"status": "FAIL",
"message": "FAILED",
}
self.assertRaises(
exceptions.RegisterOrUpdateError,
baremetal.register_or_update,
self.app.client_manager,
nodes_json=[],
queue_name="QUEUE_NAME",
kernel_name="kernel",
ramdisk_name="ramdisk"
)
self.workflow.executions.create.assert_called_once_with(
'tripleo.baremetal.v1.register_or_update',
workflow_input={
'kernel_name': 'kernel',
'queue_name': 'QUEUE_NAME',
'nodes_json': [],
'ramdisk_name': 'ramdisk'
})
def test_provide_success(self):
self.websocket.wait_for_message.return_value = {
"status": "SUCCESS",
}
baremetal.provide(self.app.client_manager, node_uuids=[],
queue_name="QUEUE_NAME")
self.workflow.executions.create.assert_called_once_with(
'tripleo.baremetal.v1.provide',
workflow_input={
'node_uuids': [],
'queue_name': "QUEUE_NAME"
})
def test_provide_error(self):
self.websocket.wait_for_message.return_value = {
"status": "FAIL",
"message": "Failed"
}
self.assertRaises(
exceptions.NodeProvideError,
baremetal.provide,
self.app.client_manager,
node_uuids=[],
queue_name="QUEUE_NAME")
self.workflow.executions.create.assert_called_once_with(
'tripleo.baremetal.v1.provide',
workflow_input={
'node_uuids': [],
'queue_name': "QUEUE_NAME"
})

View File

@ -20,6 +20,7 @@ import csv
import json
import logging
import time
import uuid
import yaml
from cliff import command
@ -28,10 +29,10 @@ import ironic_inspector_client
from openstackclient.common import utils as osc_utils
from openstackclient.i18n import _
from oslo_utils import units
from tripleo_common.utils import nodes
from tripleoclient import exceptions
from tripleoclient import utils
from tripleoclient.workflows import baremetal
def _csv_to_nodes_dict(nodes_csv):
@ -152,8 +153,7 @@ class ImportBaremetal(command.Command):
def get_parser(self, prog_name):
parser = super(ImportBaremetal, self).get_parser(prog_name)
parser.add_argument('-s', '--service-host', dest='service_host',
help=_('Nova compute service host to register '
'nodes with'))
help=_('Deprecated, this argument has no impact.'))
parser.add_argument(
'--json', dest='json', action='store_true',
help=_('Deprecated, now detected via file extension.'))
@ -210,34 +210,34 @@ class ImportBaremetal(command.Command):
_("OS_BAREMETAL_API_VERSION must be >=1.11 for use of "
"'enroll' provision state; currently %s") % api_version)
# NOTE (dprince) move this to tripleo-common?
for node in nodes_config:
caps = utils.capabilities_to_dict(node.get('capabilities', {}))
caps.setdefault('boot_option', parsed_args.instance_boot_option)
node['capabilities'] = utils.dict_to_capabilities(caps)
new_nodes = nodes.register_all_nodes(
parsed_args.service_host,
nodes_config,
client=client,
keystone_client=self.app.client_manager.identity,
glance_client=self.app.client_manager.image,
kernel_name=(parsed_args.deploy_kernel if not
parsed_args.no_deploy_image else None),
ramdisk_name=(parsed_args.deploy_ramdisk if not
parsed_args.no_deploy_image else None))
queue_name = str(uuid.uuid4())
if parsed_args.no_deploy_image:
deploy_kernel = None
deploy_ramdisk = None
else:
deploy_kernel = parsed_args.deploy_kernel
deploy_ramdisk = parsed_args.deploy_ramdisk
nodes = baremetal.register_or_update(
self.app.client_manager,
nodes_json=nodes_config,
queue_name=queue_name,
kernel_name=deploy_kernel,
ramdisk_name=deploy_ramdisk
)
node_uuids = [node['uuid'] for node in nodes]
if parsed_args.initial_state == "available":
manageable_node_uuids = list(utils.set_nodes_state(
client, new_nodes, "manage", "manageable",
skipped_states={'manageable', 'available'}
))
manageable_nodes = [
n for n in new_nodes if n.uuid in manageable_node_uuids
]
list(utils.set_nodes_state(
client, manageable_nodes, "provide", "available",
skipped_states={'available'}
))
baremetal.provide(self.app.client_manager, node_uuids=node_uuids,
queue_name=queue_name)
class StartBaremetalIntrospectionBulk(command.Command):
@ -257,9 +257,9 @@ class StartBaremetalIntrospectionBulk(command.Command):
self.log.debug("Moving available/enroll nodes to manageable state.")
available_nodes = utils.nodes_in_states(client,
("available", "enroll"))
for uuid in utils.set_nodes_state(client, available_nodes, 'manage',
'manageable'):
self.log.debug("Node {0} has been set to manageable.".format(uuid))
for uu in utils.set_nodes_state(client, available_nodes, 'manage',
'manageable'):
self.log.debug("Node {0} has been set to manageable.".format(uu))
manageable_nodes = utils.nodes_in_states(client, ("manageable",))
for node in manageable_nodes:
@ -272,25 +272,25 @@ class StartBaremetalIntrospectionBulk(command.Command):
errors = []
successful_node_uuids = set()
results = inspector_client.wait_for_finish(node_uuids)
for uuid, status in results.items():
for uu, status in results.items():
if status['error'] is None:
print("Introspection for UUID {0} finished successfully."
.format(uuid))
successful_node_uuids.add(uuid)
.format(uu))
successful_node_uuids.add(uu)
else:
print("Introspection for UUID {0} finished with error: {1}"
.format(uuid, status['error']))
errors.append("%s: %s" % (uuid, status['error']))
.format(uu, status['error']))
errors.append("%s: %s" % (uu, status['error']))
print("Setting manageable nodes to available...")
self.log.debug("Moving manageable nodes to available state.")
successful_nodes = [n for n in manageable_nodes
if n.uuid in successful_node_uuids]
for uuid in utils.set_nodes_state(
for uu in utils.set_nodes_state(
client, successful_nodes, 'provide',
'available', skipped_states=("available", "active")):
print("Node {0} has been set to available.".format(uuid))
print("Node {0} has been set to available.".format(uu))
if errors:
raise exceptions.IntrospectionError(

View File

View File

@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from tripleoclient import exceptions
def register_or_update(clients, **workflow_input):
"""Node Registration or Update
Run the tripleo.baremetal.v1.register_or_update Mistral workflow.
"""
workflow_client = clients.workflow_engine
tripleoclients = clients.tripleoclient
queue_name = workflow_input['queue_name']
execution = workflow_client.executions.create(
'tripleo.baremetal.v1.register_or_update',
workflow_input=workflow_input
)
with tripleoclients.messaging_websocket(queue_name) as ws:
payload = ws.wait_for_message(execution.id)
if payload['status'] == 'SUCCESS':
registered_nodes = payload['registered_nodes']
for nd in registered_nodes:
print('Successfully registered node UUID %s' % nd['uuid'])
return registered_nodes
else:
raise exceptions.RegisterOrUpdateError(
'Exception registering nodes: {}'.format(payload['message']))
def provide(clients, **workflow_input):
"""Provide Baremetal Nodes
Run the tripleo.baremetal.v1.provide Mistral workflow.
"""
workflow_client = clients.workflow_engine
tripleoclients = clients.tripleoclient
queue_name = workflow_input['queue_name']
execution = workflow_client.executions.create(
'tripleo.baremetal.v1.provide',
workflow_input={'node_uuids': workflow_input['node_uuids'],
'queue_name': queue_name}
)
with tripleoclients.messaging_websocket(queue_name) as ws:
payload = ws.wait_for_message(execution.id)
if payload['status'] == 'SUCCESS':
print('Successfully set all nodes to available.')
else:
raise exceptions.NodeProvideError(
'Failed to set nodes to available state: {}'.format(
payload['message']))