# Copyright 2020 Canonical Ltd # # 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 copy import deepcopy from unittest import mock import charms_openstack.test_mocks import charms_openstack.test_utils as test_utils import charms.leadership as leadership import charmhelpers.core.hookenv as hookenv import charms.reactive as reactive from charmhelpers.contrib.openstack.utils import ( CompareOpenStackReleases, os_release, ) from charm.openstack.ironic import ironic from charm.openstack.ironic import controller_utils as ctrl_util class TestIronicCharmConfigProperties(test_utils.PatchHelper): def setUp(self): super().setUp() self.patch_release(ironic.IronicConductorCharm.release) def test_deployment_interface_ip(self): cls = mock.MagicMock() self.patch_object(ironic, 'ch_ip') ironic.deployment_interface_ip(cls) self.ch_ip.get_relation_ip.assert_called_with('deployment') def test_internal_interface_ip(self): cls = mock.MagicMock() self.patch_object(ironic, 'ch_ip') ironic.internal_interface_ip(cls) self.ch_ip.get_relation_ip.assert_called_with('internal') def test_temp_url_secret(self): cls = mock.MagicMock() leadership.leader_get.return_value = "fake" self.assertEqual(ironic.temp_url_secret(cls), "fake") leadership.leader_get.assert_called_with("temp_url_secret") class TestIronicCharm(test_utils.PatchHelper): def setUp(self): super().setUp() hookenv.config.return_value = {} self.patch_release(ironic.IronicConductorCharm.release) self.patch_object(ironic.controller_utils, 'get_pxe_config_class') self.mocked_pxe_cfg = mock.MagicMock() self.mocked_pxe_cfg.TFTP_ROOT = ctrl_util.PXEBootBase.TFTP_ROOT self.mocked_pxe_cfg.HTTP_ROOT = ctrl_util.PXEBootBase.HTTP_ROOT self.mocked_pxe_cfg.IRONIC_USER = ctrl_util.PXEBootBase.IRONIC_USER self.mocked_pxe_cfg.IRONIC_GROUP = ctrl_util.PXEBootBase.IRONIC_GROUP self.mocked_pxe_cfg.determine_packages.return_value = [ "fakepkg1", "fakepkg2"] self.mocked_pxe_cfg.get_restart_map.return_value = { "fake_config": [ "fake_svc", ]} self.mocked_pxe_cfg.HTTPD_SERVICE_NAME = "fakehttpd" self.get_pxe_config_class.return_value = self.mocked_pxe_cfg def test_setup_power_adapter_config_train(self): os_release.return_value = "train" cfg_data = { "enabled-hw-types": "ipmi, redfish, idrac", } hookenv.config.return_value = cfg_data target = ironic.IronicConductorCharm() target._setup_power_adapter_config() expected = { "enabled_hardware_types": "idrac, intel-ipmi, ipmi, redfish", "enabled_management_interfaces": ("idrac-redfish, intel-ipmitool, " "ipmitool, redfish, noop"), "enabled_inspect_interfaces": "idrac-redfish, redfish, no-inspect", "enabled_power_interfaces": "idrac-redfish, ipmitool, redfish", "enabled_console_interfaces": ("ipmitool-shellinabox, " "ipmitool-socat, no-console"), "enabled_raid_interfaces": "idrac-wsman, no-raid", "enabled_vendor_interfaces": "idrac-wsman, ipmitool, no-vendor", "enabled_boot_interfaces": "pxe", "enabled_bios_interfaces": "no-bios" } self.assertEqual( target.config["hardware_type_cfg"], expected) def test_setup_power_adapter_config_train_ipxe(self): os_release.return_value = "train" cfg_data = { "enabled-hw-types": "ipmi, redfish, idrac", "use-ipxe": True, } hookenv.config.return_value = cfg_data target = ironic.IronicConductorCharm() target._setup_power_adapter_config() expected = { "enabled_hardware_types": "idrac, intel-ipmi, ipmi, redfish", "enabled_management_interfaces": ("idrac-redfish, intel-ipmitool, " "ipmitool, redfish, noop"), "enabled_inspect_interfaces": "idrac-redfish, redfish, no-inspect", "enabled_power_interfaces": "idrac-redfish, ipmitool, redfish", "enabled_console_interfaces": ("ipmitool-shellinabox, " "ipmitool-socat, no-console"), "enabled_raid_interfaces": "idrac-wsman, no-raid", "enabled_vendor_interfaces": "idrac-wsman, ipmitool, no-vendor", "enabled_boot_interfaces": "pxe, ipxe", "enabled_bios_interfaces": "no-bios" } self.assertEqual( target.config["hardware_type_cfg"], expected) def test_setup_power_adapter_config_unknown(self): # test that it defaults to latest, in this case ussuri os_release.return_value = "unknown" cfg_data = { "enabled-hw-types": "ipmi, redfish, idrac", } hookenv.config.return_value = cfg_data target = ironic.IronicConductorCharm() target._setup_power_adapter_config() expected = { "enabled_hardware_types": "idrac, intel-ipmi, ipmi, redfish", "enabled_management_interfaces": ("idrac-redfish, intel-ipmitool, " "ipmitool, redfish, noop"), "enabled_inspect_interfaces": "idrac-redfish, redfish, no-inspect", "enabled_power_interfaces": "idrac-redfish, ipmitool, redfish", "enabled_console_interfaces": ("ipmitool-shellinabox, " "ipmitool-socat, no-console"), "enabled_raid_interfaces": "idrac-wsman, no-raid", "enabled_vendor_interfaces": "idrac-wsman, ipmitool, no-vendor", "enabled_boot_interfaces": "pxe, redfish-virtual-media", "enabled_bios_interfaces": "idrac-wsman, no-bios" } self.assertEqual( target.config["hardware_type_cfg"], expected) def test_setup_power_adapter_config_ussuri(self): os_release.return_value = "ussuri" cfg_data = { "enabled-hw-types": "ipmi, redfish, idrac", } hookenv.config.return_value = cfg_data target = ironic.IronicConductorCharm() target._setup_power_adapter_config() self.maxDiff = None expected = { "enabled_hardware_types": "idrac, intel-ipmi, ipmi, redfish", "enabled_management_interfaces": ("idrac-redfish, intel-ipmitool, " "ipmitool, redfish, noop"), "enabled_inspect_interfaces": "idrac-redfish, redfish, no-inspect", "enabled_power_interfaces": "idrac-redfish, ipmitool, redfish", "enabled_console_interfaces": ("ipmitool-shellinabox, " "ipmitool-socat, no-console"), "enabled_raid_interfaces": "idrac-wsman, no-raid", "enabled_vendor_interfaces": "idrac-wsman, ipmitool, no-vendor", "enabled_boot_interfaces": "pxe, redfish-virtual-media", "enabled_bios_interfaces": "idrac-wsman, no-bios" } self.assertEqual( target.config["hardware_type_cfg"], expected) def test_get_amqp_credentials(self): os_release.return_value = "yoga" cfg_data = { "rabbit-user": "ironic", "rabbit-vhost": "openstack", } hookenv.config.return_value = cfg_data target = ironic.IronicConductorCharm() self.get_pxe_config_class.assert_called_with(cfg_data, 'yoga') result = target.get_amqp_credentials() self.assertEqual(result, ('ironic', 'openstack')) def test_get_database_setup(self): os_release.return_value = "yoga" cfg_data = { "database-user": "ironic", "database": "ironicdb", } hookenv.config.return_value = cfg_data target = ironic.IronicConductorCharm() self.get_pxe_config_class.assert_called_with(cfg_data, 'yoga') result = target.get_database_setup() self.assertEqual( result, [{ "database": cfg_data["database"], "username": cfg_data["database-user"]}]) def test_enabled_network_interfaces(self): os_release.return_value = "yoga" cfg_data = { "enabled-network-interfaces": "fake, fake2"} hookenv.config.return_value = cfg_data target = ironic.IronicConductorCharm() self.get_pxe_config_class.assert_called_with(cfg_data, 'yoga') self.assertEqual( target.enabled_network_interfaces, ["fake", "fake2"]) def test_enabled_deploy_interfaces(self): os_release.return_value = "yoga" cfg_data = { "enabled-deploy-interfaces": "fake, fake2"} hookenv.config.return_value = cfg_data target = ironic.IronicConductorCharm() self.get_pxe_config_class.assert_called_with(cfg_data, 'yoga') self.assertEqual( target.enabled_deploy_interfaces, ["fake", "fake2"]) def test_configure_defaults_no_cfg(self): cfg_data = { "default-network-interface": "", "default-deploy-interface": ""} hookenv.config.return_value = cfg_data target = ironic.IronicConductorCharm() self.assertEqual( target.config.get("default-network-interface"), ironic.DEFAULT_NET_IFACE) self.assertEqual( target.config.get("default-deploy-interface"), ironic.DEFAULT_DEPLOY_IFACE) def test_configure_defaults_with_user_defined_val(self): cfg_data = { "default-network-interface": "fake_net", "default-deploy-interface": "fake_deploy"} hookenv.config.return_value = cfg_data target = ironic.IronicConductorCharm() target._configure_defaults() self.assertEqual( target.config.get("default-network-interface"), "fake_net") self.assertEqual( target.config.get("default-deploy-interface"), "fake_deploy") def test_configure_logging_with_local_path(self): cfg_data = { "deploy-logs-collect": "always", "deploy-logs-storage-backend": "local", "deploy-logs-local-path": "/var/log/ironic/nodeploy"} hookenv.config.return_value = cfg_data target = ironic.IronicConductorCharm() target._configure_defaults() self.assertEqual( target.config.get("deploy-logs-collect"), "always") self.assertEqual( target.config.get("deploy-logs-storage-backend"), "local") self.assertEqual( target.config.get("deploy-logs-local-path"), "/var/log/ironic/nodeploy") def test_configure_logging_with_swift_container(self): cfg_data = { "deploy-logs-collect": "always", "deploy-logs-storage-backend": "swift", "deploy-logs-swift-container": "no_container", "deploy-logs-swift-days-to-expire": 15} hookenv.config.return_value = cfg_data target = ironic.IronicConductorCharm() target._configure_defaults() self.assertEqual( target.config.get("deploy-logs-collect"), "always") self.assertEqual( target.config.get("deploy-logs-storage-backend"), "swift") self.assertEqual( target.config.get("deploy-logs-swift-container"), "no_container"), self.assertEqual( target.config.get("deploy-logs-swift-days-to-expire"), 15), def test_setup_pxe_config(self): hookenv.config.return_value = { "default-network-interface": "fake_net", "default-deploy-interface": "fake_deploy"} target = ironic.IronicConductorCharm() target._setup_pxe_config(self.mocked_pxe_cfg) expected_pkgs = deepcopy(ironic.PACKAGES) + ["fakepkg1", "fakepkg2"] expected_cfg = { 'tftpboot': ctrl_util.PXEBootBase.TFTP_ROOT, 'httpboot': ctrl_util.PXEBootBase.HTTP_ROOT, 'ironic_user': ctrl_util.PXEBootBase.IRONIC_USER, 'ironic_group': ctrl_util.PXEBootBase.IRONIC_GROUP, 'hardware_type_cfg': { 'enabled_hardware_types': 'intel-ipmi, ipmi', 'enabled_management_interfaces': ('intel-ipmitool, ipmitool,' ' noop'), 'enabled_inspect_interfaces': 'no-inspect', 'enabled_power_interfaces': 'ipmitool', 'enabled_console_interfaces': ('ipmitool-shellinabox, ' 'ipmitool-socat, no-console'), 'enabled_raid_interfaces': 'no-raid', 'enabled_vendor_interfaces': 'ipmitool, no-vendor', 'enabled_boot_interfaces': 'pxe', 'enabled_bios_interfaces': 'no-bios'}, 'default-network-interface': 'fake_net', 'default-deploy-interface': 'fake_deploy'} self.assertEqual( sorted(target.packages), sorted(expected_pkgs)) self.assertEqual(target.config, expected_cfg) self.assertEqual( target.restart_map.get("fake_config", []), ["fake_svc"]) self.assertTrue("fakehttpd" in target.services) def test_packages_xena(self): reactive.is_flag_set.side_effect = [False, False, False] target = ironic.IronicConductorXenaCharm() expected_pkgs = deepcopy(ironic.PACKAGES) + ["fakepkg1", "fakepkg2"] expected_pkgs.remove("open-iscsi") self.assertEqual( sorted(target.packages), sorted(expected_pkgs)) def test_validate_network_interfaces(self): target = ironic.IronicConductorCharm() with self.assertRaises(ValueError): target._validate_network_interfaces(["bogus"]) self.assertIsNone( target._validate_network_interfaces(["neutron"])) def test_validate_deploy_interfaces(self): target = ironic.IronicConductorCharm() CompareOpenStackReleases.return_value = 'wallaby' with self.assertRaises(ValueError) as err: target._validate_deploy_interfaces(["bogus"]) expected_msg = ( 'Deploy interface bogus is not valid.' ' Valid interfaces are: direct, iscsi') self.assertIsNone( target._validate_deploy_interfaces(["direct"])) self.assertEqual(str(err.exception), expected_msg) def test_validate_deploy_interfaces_xena(self): reactive.is_flag_set.side_effect = [False, False, False] target = ironic.IronicConductorXenaCharm() CompareOpenStackReleases.return_value = 'xena' with self.assertRaises(ValueError) as err: target._validate_deploy_interfaces(["bogus"]) expected_msg = ( 'Deploy interface bogus is not valid.' ' Valid interfaces are: direct') self.assertIsNone( target._validate_deploy_interfaces(["direct"])) self.assertEqual(str(err.exception), expected_msg) def test_validate_deploy_interfaces_tmp_secret(self): # leadership.set.temp_url_secret is not set, and "direct" # boot method is enabled. Validate will fail, until # set-temp-url-secret action is run reactive.is_flag_set.side_effect = [False, True] CompareOpenStackReleases.return_value = 'wallaby' target = ironic.IronicConductorCharm() with self.assertRaises(ValueError) as err: target._validate_deploy_interfaces(["direct"]) expected_msg = ( 'run set-temp-url-secret action on ' 'leader to enable direct deploy method') self.assertEqual(str(err.exception), expected_msg) def test_validate_default_net_interface(self): hookenv.config.return_value = { "default-network-interface": "flat", "enabled-network-interfaces": "neutron, flat, noop"} target = ironic.IronicConductorCharm() self.assertIsNone(target._validate_default_net_interface()) def test_validate_default_net_interface_invalid_default(self): hookenv.config.return_value = { "default-network-interface": "bogus", "enabled-network-interfaces": "neutron, flat, noop"} target = ironic.IronicConductorCharm() with self.assertRaises(ValueError) as err: target._validate_default_net_interface() expected_msg = ( "default-network-interface (bogus) is not enabled " "in enabled-network-interfaces: neutron, flat, noop") self.assertEqual(str(err.exception), expected_msg) def test_validate_default_deploy_interface(self): hookenv.config.return_value = { "default-deploy-interface": "direct", "enabled-deploy-interfaces": "direct, iscsi"} target = ironic.IronicConductorCharm() self.assertIsNone(target._validate_default_deploy_interface()) def test_validate_default_deploy_interface_invalid_default(self): hookenv.config.return_value = { "default-deploy-interface": "bogus", "enabled-deploy-interfaces": "direct, iscsi"} target = ironic.IronicConductorCharm() with self.assertRaises(ValueError) as err: target._validate_default_deploy_interface() expected_msg = ( "default-deploy-interface (bogus) is not enabled " "in enabled-deploy-interfaces: direct, iscsi") self.assertEqual(str(err.exception), expected_msg) def test_custom_assess_status_check_all_good(self): hookenv.config.return_value = { "default-deploy-interface": "direct", "enabled-deploy-interfaces": "direct, iscsi", "default-network-interface": "flat", "enabled-network-interfaces": "neutron, flat, noop"} CompareOpenStackReleases.return_value = 'wallaby' target = ironic.IronicConductorCharm() self.assertEqual(target.custom_assess_status_check(), (None, None)) def test_custom_assess_status_check_invalid_enabled_net_ifaces(self): hookenv.config.return_value = { "default-deploy-interface": "direct", "enabled-deploy-interfaces": "direct, iscsi", "default-network-interface": "flat", "enabled-network-interfaces": "bogus, noop"} target = ironic.IronicConductorCharm() expected_status = ( 'blocked', 'invalid enabled-network-interfaces config, Network interface ' 'bogus is not valid. Valid interfaces are: neutron, flat, noop' ) self.assertEqual(target.custom_assess_status_check(), expected_status) def test_custom_assess_status_check_invalid_enabled_deploy_ifaces(self): hookenv.config.return_value = { "default-deploy-interface": "direct", "enabled-deploy-interfaces": "bogus, iscsi", "default-network-interface": "flat", "enabled-network-interfaces": "neutron, flat, noop"} CompareOpenStackReleases.return_value = 'wallaby' target = ironic.IronicConductorCharm() expected_status = ( 'blocked', 'invalid enabled-deploy-interfaces config, Deploy interface ' 'bogus is not valid. Valid interfaces are: direct, iscsi' ) self.assertEqual(target.custom_assess_status_check(), expected_status) def test_custom_assess_status_check_invalid_default_net_iface(self): hookenv.config.return_value = { "default-deploy-interface": "direct", "enabled-deploy-interfaces": "direct, iscsi", "default-network-interface": "bogus", "enabled-network-interfaces": "neutron, flat, noop"} target = ironic.IronicConductorCharm() expected_status = ( 'blocked', 'invalid default-network-interface config, ' 'default-network-interface (bogus) is not enabled ' 'in enabled-network-interfaces: neutron, flat, noop' ) self.assertEqual(target.custom_assess_status_check(), expected_status) def test_custom_assess_status_check_invalid_default_deploy_iface(self): hookenv.config.return_value = { "default-deploy-interface": "bogus", "enabled-deploy-interfaces": "direct, iscsi", "default-network-interface": "flat", "enabled-network-interfaces": "neutron, flat, noop"} CompareOpenStackReleases.return_value = 'wallaby' target = ironic.IronicConductorCharm() expected_status = ( 'blocked', 'invalid default-deploy-interface config, default-deploy-interface' ' (bogus) is not enabled in enabled-deploy-interfaces: direct, ' 'iscsi') self.assertEqual(target.custom_assess_status_check(), expected_status) @mock.patch('charms_openstack.charm.OpenStackCharm.upgrade_charm') def test_upgrade_charm(self, upgrade_charm): os_release.return_value = "ussuri" cfg_data = { "openstack-origin": "distro", } hookenv.config.return_value = cfg_data target = ironic.IronicConductorCharm() target.upgrade_charm() # check the parent's upgrade_charm was called upgrade_charm.assert_called() os_module = charms_openstack.test_mocks.charmhelpers.contrib.openstack templating = os_module.templating templating.OSConfigRenderer.assert_called_with( templates_dir='templates/', openstack_release='ussuri') configs = templating.OSConfigRenderer() configs.register.assert_called_with(config_file=ironic.IRONIC_DEFAULT, contexts=[]) configs.write_all.assert_called_with() @mock.patch('charms_openstack.charm.OpenStackCharm.install') def test_install(self, install): os_release.return_value = "ussuri" cfg_data = { "openstack-origin": "distro", } charmhelpers = charms_openstack.test_mocks.charmhelpers os_utils = charmhelpers.contrib.openstack.utils os_utils.get_source_and_pgp_key.return_value = (None, None) hookenv.config.return_value = cfg_data target = ironic.IronicConductorCharm() target.install() install.assert_called_with() os_module = charms_openstack.test_mocks.charmhelpers.contrib.openstack templating = os_module.templating templating.OSConfigRenderer.assert_called_with( templates_dir='templates/', openstack_release='ussuri') configs = templating.OSConfigRenderer(templates_dir='templates/', openstack_release='ussuri') configs.register.assert_called_with(config_file=ironic.IRONIC_DEFAULT, contexts=[]) configs.write_all.assert_called_with()