diff --git a/releasenotes/notes/5.8.0-d1ca2298ba598431.yaml b/releasenotes/notes/5.8.0-d1ca2298ba598431.yaml index 6591fb35c..65e4d17d2 100644 --- a/releasenotes/notes/5.8.0-d1ca2298ba598431.yaml +++ b/releasenotes/notes/5.8.0-d1ca2298ba598431.yaml @@ -8,6 +8,8 @@ features: - Add a new Workflow which can be used to wait for Heat stacks finish with COMPLETE or FAILED. - CephMdsKey is now a generated Heat parameter. + - Add an new Action which generates environment parameters for configuring + fencing. fixes: - Fixes `bug 1644756 `__ so that flavour matching works as expected with the object-storage role. diff --git a/setup.cfg b/setup.cfg index 494374ec6..8952e1afd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -78,6 +78,7 @@ mistral.actions = tripleo.parameters.update_role = tripleo_common.actions.parameters:UpdateRoleParametersAction tripleo.parameters.generate_passwords = tripleo_common.actions.parameters:GeneratePasswordsAction tripleo.parameters.get_passwords = tripleo_common.actions.parameters:GetPasswordsAction + tripleo.parameters.generate_fencing = tripleo_common.actions.parameters:GenerateFencingParametersAction tripleo.plan.create = tripleo_common.actions.plan:CreatePlanAction tripleo.plan.update = tripleo_common.actions.plan:UpdatePlanAction tripleo.plan.create_container = tripleo_common.actions.plan:CreateContainerAction diff --git a/tripleo_common/actions/parameters.py b/tripleo_common/actions/parameters.py index bee8418f6..bed457231 100644 --- a/tripleo_common/actions/parameters.py +++ b/tripleo_common/actions/parameters.py @@ -34,6 +34,7 @@ from mistral.workflow import utils as mistral_workflow_utils from tripleo_common.actions import base from tripleo_common.actions import templates from tripleo_common import constants +from tripleo_common.utils import nodes from tripleo_common.utils import parameters from tripleo_common.utils import passwords as password_utils @@ -210,3 +211,87 @@ class GetPasswordsAction(base.TripleOAction): passwords[name] = parameter_defaults[name] return passwords + + +class GenerateFencingParametersAction(base.TripleOAction): + """Generates fencing configuration for a deployment. + + :param nodes_json: list of nodes & attributes in json format + :param os_auth: dictionary of OS client auth data (if using pxe_ssh) + :param fence_action: action to take when fencing nodes + :param delay: time to wait before taking fencing action + :param ipmi_level: IPMI user level to use + :param ipmi_cipher: IPMI cipher suite to use + :param ipmi_lanplus: whether to use IPMIv2.0 + """ + + def __init__(self, nodes_json, os_auth, fence_action, delay, + ipmi_level, ipmi_cipher, ipmi_lanplus): + super(GenerateFencingParametersAction, self).__init__() + self.nodes_json = nodes_json + self.os_auth = os_auth + self.fence_action = fence_action + self.delay = delay + self.ipmi_level = ipmi_level + self.ipmi_cipher = ipmi_cipher + self.ipmi_lanplus = ipmi_lanplus + + def run(self): + """Returns the parameters for fencing controller nodes""" + hostmap = nodes.generate_hostmap(self.get_baremetal_client(), + self.get_compute_client()) + fence_params = {"EnableFencing": True, "FencingConfig": {}} + devices = [] + + for node in self.nodes_json: + node_data = {} + params = {} + if "mac" in node: + # Not all Ironic drivers present a MAC address, so we only + # capture it if it's present + mac_addr = node["mac"][0] + node_data["host_mac"] = mac_addr + + # Build up fencing parameters based on which Ironic driver this + # node is using + if node["pm_type"] == "pxe_ssh": + # Ironic fencing driver + node_data["agent"] = "fence_ironic" + params["action"] = self.fence_action + params["auth_url"] = self.os_auth["auth_url"] + params["login"] = self.os_auth["login"] + params["passwd"] = self.os_auth["passwd"] + params["tenant_name"] = self.os_auth["tenant_name"] + params["pcmk_host_map"] = "%(compute_name)s:%(bm_name)s" % ( + {"compute_name": hostmap[mac_addr]["compute_name"], + "bm_name": hostmap[mac_addr]["baremetal_name"]}) + if self.delay: + params["delay"] = self.delay + elif node["pm_type"].split('_')[1] in ("ipmitool", "ilo", "drac"): + # IPMI fencing driver + node_data["agent"] = "fence_ipmilan" + params["action"] = self.fence_action + params["ipaddr"] = node["pm_addr"] + params["passwd"] = node["pm_password"] + params["login"] = node["pm_user"] + params["pcmk_host_list"] = hostmap[mac_addr]["compute_name"] + if "pm_port" in node: + params["ipport"] = node["pm_port"] + if self.ipmi_lanplus: + params["lanplus"] = self.ipmi_lanplus + if self.delay: + params["delay"] = self.delay + if self.ipmi_cipher: + params["cipher"] = self.ipmi_cipher + if self.ipmi_level: + params["privlvl"] = self.ipmi_level + else: + error = ("Unable to generate fencing parameters for %s" % + node["pm_type"]) + raise ValueError(error) + + node_data["params"] = params + devices.append(node_data) + + fence_params["FencingConfig"]["devices"] = devices + return {"parameter_defaults": fence_params} diff --git a/tripleo_common/tests/actions/test_parameters.py b/tripleo_common/tests/actions/test_parameters.py index 2f9ed33ae..ab4d67d29 100644 --- a/tripleo_common/tests/actions/test_parameters.py +++ b/tripleo_common/tests/actions/test_parameters.py @@ -497,3 +497,99 @@ class GetPasswordsActionTest(base.TestCase): # ensure old passwords used and no new generation self.assertEqual(_EXISTING_PASSWORDS, result) + + +class GenerateFencingParametersActionTestCase(base.TestCase): + + @mock.patch('tripleo_common.utils.nodes.' + 'generate_hostmap') + @mock.patch('tripleo_common.actions.base.TripleOAction.' + 'get_compute_client') + @mock.patch('tripleo_common.actions.base.TripleOAction.' + 'get_baremetal_client') + @mock.patch('tripleo_common.actions.base.TripleOAction.' + 'get_workflow_client') + @mock.patch('tripleo_common.actions.base.TripleOAction.' + 'get_orchestration_client') + @mock.patch('mistral.context.ctx') + def test_no_success(self, mock_ctx, mock_get_orchestration, + mock_get_workflow, mock_get_baremetal, + mock_get_compute, mock_generate_hostmap): + test_hostmap = { + "00:11:22:33:44:55": { + "compute_name": "compute_name_0", + "baremetal_name": "baremetal_name_0" + }, + "11:22:33:44:55:66": { + "compute_name": "compute_name_1", + "baremetal_name": "baremetal_name_1" + } + } + mock_generate_hostmap.return_value = test_hostmap + + test_envjson = [{ + "name": "control-0", + "pm_password": "control-0-password", + "pm_type": "pxe_ipmitool", + "pm_user": "control-0-admin", + "pm_addr": "0.1.2.3", + "pm_port": "0123", + "mac": [ + "00:11:22:33:44:55" + ] + }, { + "name": "control-1", + "pm_password": "control-1-password", + "pm_type": "pxe_ssh", + "pm_user": "control-1-admin", + "pm_addr": "1.2.3.4", + "mac": [ + "11:22:33:44:55:66" + ] + }] + test_osauth = { + "auth_url": "test://auth.url", + "login": "test_os_username", + "passwd": "test_os_password", + "tenant_name": "test_os_tenant_name", + } + + action = parameters.GenerateFencingParametersAction(test_envjson, + test_osauth, + "test_action", + 28, + 5, + 0, + True) + + result = action.run()["parameter_defaults"] + + self.assertTrue(result["EnableFencing"]) + self.assertEqual(result["FencingConfig"]["devices"][0], { + "agent": "fence_ipmilan", + "host_mac": "00:11:22:33:44:55", + "params": { + "action": "test_action", + "delay": 28, + "ipaddr": "0.1.2.3", + "ipport": "0123", + "lanplus": True, + "privlvl": 5, + "login": "control-0-admin", + "passwd": "control-0-password", + "pcmk_host_list": "compute_name_0" + } + }) + self.assertEqual(result["FencingConfig"]["devices"][1], { + "agent": "fence_ironic", + "host_mac": "11:22:33:44:55:66", + "params": { + "auth_url": "test://auth.url", + "delay": 28, + "action": "test_action", + "login": "test_os_username", + "passwd": "test_os_password", + "tenant_name": "test_os_tenant_name", + "pcmk_host_map": "compute_name_1:baremetal_name_1" + } + }) diff --git a/tripleo_common/utils/nodes.py b/tripleo_common/utils/nodes.py index 9f5459144..50fd28423 100644 --- a/tripleo_common/utils/nodes.py +++ b/tripleo_common/utils/nodes.py @@ -432,3 +432,14 @@ def update_node_capability(node_uuid, capability, value, client): node = client.node.get(node_uuid) patch = _get_capability_patch(node, capability, value) return client.node.update(node_uuid, patch) + + +def generate_hostmap(baremetal_client, compute_client): + """Create a map between Compute nodes and Baremetal nodes""" + hostmap = {} + for node in compute_client.servers.list(): + bm_node = baremetal_client.node.get_by_instance_uuid(node.id) + for port in baremetal_client.port.list(node=bm_node.uuid): + hostmap[port.address] = {"compute_name": node.name, + "baremetal_name": bm_node.name} + return hostmap