diff --git a/hooks/percona_hooks.py b/hooks/percona_hooks.py index e9904bd..99e0b4d 100755 --- a/hooks/percona_hooks.py +++ b/hooks/percona_hooks.py @@ -56,6 +56,7 @@ from charmhelpers.fetch import ( apt_update, apt_install, add_source, + SourceConfigError, ) from charmhelpers.contrib.peerstorage import ( peer_echo, @@ -94,6 +95,7 @@ from charmhelpers.contrib.openstack.ha.utils import ( update_hacluster_vip, update_hacluster_dns_ha, ) +from charmhelpers.core.unitdata import kv from percona_utils import ( determine_packages, @@ -149,10 +151,10 @@ from percona_utils import ( delete_replication_user, list_replication_users, check_mysql_connection, + update_source, + ADD_APT_REPOSITORY_FAILED, ) -from charmhelpers.core.unitdata import kv - hooks = Hooks() RES_MONITOR_PARAMS = ('params user="sstuser" password="%(sstpass)s" ' @@ -532,12 +534,29 @@ def config_changed(): # leader or if the leader is bootstrapped and therefore ready for install. install_percona_xtradb_cluster() + # run a package update if the source or key has changed + cfg = config() + kvstore = kv() + if cfg.changed("source") or cfg.changed("key"): + status_set("maintenance", "Upgrading Percona packages") + try: + update_source(source=cfg["source"], key=cfg["key"]) + kvstore.set(ADD_APT_REPOSITORY_FAILED, False) + except (subprocess.CalledProcessError, SourceConfigError): + # NOTE (rgildein): Need to store the local state to prevent + # `assess_status` from running, which changes the unit state + # to "Unit is ready" + kvstore.set(ADD_APT_REPOSITORY_FAILED, True) + kvstore.flush() + + if kvstore.get(ADD_APT_REPOSITORY_FAILED, False): + return + if config('prefer-ipv6'): assert_charm_supports_ipv6() hosts = get_cluster_hosts() leader_bootstrapped = is_leader_bootstrapped() - leader_ip = leader_get('leader-ip') # Cluster upgrade adds some complication cluster_series_upgrading = leader_get("cluster_series_upgrading") diff --git a/hooks/percona_utils.py b/hooks/percona_utils.py index c2c2a78..e5927e6 100644 --- a/hooks/percona_utils.py +++ b/hooks/percona_utils.py @@ -49,9 +49,12 @@ from charmhelpers.core.hookenv import ( ) from charmhelpers.core.unitdata import kv from charmhelpers.fetch import ( + add_source, apt_install, + apt_update, filter_installed_packages, get_upstream_version, + SourceConfigError, ) from charmhelpers.contrib.network.ip import ( get_address_in_network, @@ -88,6 +91,7 @@ HOSTS_FILE = '/etc/hosts' DEFAULT_MYSQL_PORT = 3306 INITIAL_CLUSTERED_KEY = 'initial-cluster-complete' INITIAL_CLIENT_UPDATE_KEY = 'initial_client_update_done' +ADD_APT_REPOSITORY_FAILED = "add-apt-repository-failed" # NOTE(ajkavanagh) - this is 'required' for the pause/resume code for # maintenance mode, but is currently not populated as the @@ -806,6 +810,16 @@ def assess_status(configs): @param configs: a templating.OSConfigRenderer() object @returns None - this function is executed for its side-effect """ + kvstore = kv() + if kvstore.get(ADD_APT_REPOSITORY_FAILED, False): + # NOTE (rgildein): prevent unit status from changing from blocked + # to "Unit is ready", if adding a new source failed + log("skip assess_status, because adding new source failed", level=INFO) + status_set( + "blocked", "problem adding new source: {}".format(config("source")) + ) + return + assess_status_func(configs)() if pxc_installed(): # NOTE(fnordahl) ensure we do not call application_version_set with @@ -1717,3 +1731,16 @@ def mysqldump(backup_dir, databases=None): gzcmd = ["gzip", _filename] subprocess.check_call(gzcmd) return "{}.gz".format(_filename) + + +def update_source(source, key=None): + """Add source and run apt update.""" + try: + add_source(source=source, key=key, fail_invalid=True) + log("add new package source: {}".format(source)) + except (subprocess.CalledProcessError, SourceConfigError) as error: + log(error, level=ERROR) + raise + else: + apt_update() # run without retries + log("apt update after adding a new package source") diff --git a/unit_tests/test_percona_hooks.py b/unit_tests/test_percona_hooks.py index 67c4e23..ca26419 100644 --- a/unit_tests/test_percona_hooks.py +++ b/unit_tests/test_percona_hooks.py @@ -402,10 +402,14 @@ class TestConfigChanged(CharmTestCase): 'is_unit_paused_set', 'is_unit_upgrading_set', 'get_cluster_host_ip', + 'status_set', + 'kv', ] def setUp(self): CharmTestCase.setUp(self, hooks, self.TO_PATCH) + self.test_config.set_previous("source", self.test_config["source"]) + self.test_config.set_previous("key", self.test_config["key"]) self.config.side_effect = self.test_config.get self.is_unit_paused_set.return_value = False self.is_unit_upgrading_set.return_value = False @@ -416,6 +420,9 @@ class TestConfigChanged(CharmTestCase): self.relation_ids.return_value = [] self.is_relation_made.return_value = False self.get_cluster_hosts.return_value = [] + self.kvstore = mock.MagicMock() + self.kvstore.get.return_value = False + self.kv.return_value = self.kvstore def _leader_get(key): settings = {'leader-ip': '10.10.10.10', @@ -423,9 +430,36 @@ class TestConfigChanged(CharmTestCase): return settings.get(key) self.leader_get.side_effect = _leader_get + @mock.patch("percona_utils.add_source") + @mock.patch("percona_utils.apt_update") + def test_config_change_update_source( + self, mock_apt_update, mock_add_source): + """Ensure add_source and apt_update is called after changing source""" + self.test_config.set("source", "test-source") + + hooks.config_changed() + self.status_set.assert_any_call( + "maintenance", "Upgrading Percona packages") + mock_add_source.assert_called_once_with( + source="test-source", key=None, fail_invalid=True) + mock_apt_update.assert_called_once_with() + + def test_config_change_update_source_failed(self): + """Ensure failure after configuring an invalid source""" + self.test_config.set("source", "invalid-source") + self.kvstore.get.return_value = True + + hooks.config_changed() + self.kvstore.set.assert_called_once_with( + hooks.ADD_APT_REPOSITORY_FAILED, True) + self.status_set.assert_has_calls([ + mock.call("maintenance", "Upgrading Percona packages"), + ]) + def test_config_changed_open_port(self): '''Ensure open_port is called with MySQL default port''' self.is_leader_bootstrapped.return_value = True + hooks.config_changed() self.open_port.assert_called_with(3306) @@ -779,7 +813,7 @@ class TestConfigs(CharmTestCase): ] def setUp(self): - CharmTestCase.setUp(self, hooks, TO_PATCH) + CharmTestCase.setUp(self, hooks, self.TO_PATCH) self.config.side_effect = self.test_config.get self.default_config = self._get_default_config() for key, value in self.default_config.items(): @@ -987,11 +1021,6 @@ class TestConfigs(CharmTestCase): class TestClusterRelation(CharmTestCase): - TO_PATCH = [ - 'config', - 'is_leader', - ] - def setUp(self): CharmTestCase.setUp(self, hooks, TO_PATCH) self.config.side_effect = self.test_config.get diff --git a/unit_tests/test_percona_utils.py b/unit_tests/test_percona_utils.py index 657662f..24781c8 100644 --- a/unit_tests/test_percona_utils.py +++ b/unit_tests/test_percona_utils.py @@ -4,6 +4,8 @@ import tempfile import mock +from charmhelpers.fetch import SourceConfigError + import percona_utils from test_utils import CharmTestCase, patch_open @@ -518,6 +520,24 @@ class UtilsTests(CharmTestCase): percona_utils.maybe_notify_bootstrapped() _notify_bootstrapped.assert_called_once_with(cluster_uuid=_uuid) + @mock.patch("percona_utils.add_source") + @mock.patch("percona_utils.apt_update") + def test_update_source(self, mock_apt_update, mock_add_source): + """Ensure that add_source and apt_update has been called""" + percona_utils.update_source("test-source", key=None) + + mock_add_source.assert_called_once_with( + source="test-source", key=None, fail_invalid=True) + mock_apt_update.assert_called_once_with() + + @mock.patch("percona_utils.apt_update") + def test_update_invalid_source(self, mock_apt_update): + """Ensure raise error and set blocked status after invalid source""" + with self.assertRaises(SourceConfigError): + percona_utils.update_source("invalid-source", key=None) + + mock_apt_update.assert_not_called() + class UtilsTestsStatus(CharmTestCase): @@ -606,11 +626,14 @@ class UtilsTestsCTC(CharmTestCase): 'is_clustered', 'distributed_wait', 'clustered_once', - 'kv' + 'kv', ] def setUp(self): super(UtilsTestsCTC, self).setUp(percona_utils, self.TO_PATCH) + kvstore = mock.MagicMock() + kvstore.get.return_value = False + self.kv.return_value = kvstore @mock.patch.object(percona_utils, 'pxc_installed') @mock.patch.object(percona_utils, 'determine_packages')