import pytest from unittest.mock import patch, MagicMock import reactive.etcd from etcdctl import ( EtcdCtl, etcdctl_command, get_connection_string, ) # noqa from etcd_databag import EtcdDatabag from reactive.etcd import ( clear_flag, endpoint_from_flag, force_rejoin_requested, force_rejoin, GRAFANA_DASHBOARD_NAME, host, pre_series_upgrade, post_series_upgrade, register_grafana_dashboard, register_prometheus_jobs, status, ) class TestEtcdCtl: @pytest.fixture def etcdctl(self): return EtcdCtl() def test_register(self, etcdctl): with patch('etcdctl.EtcdCtl.run') as spcm: etcdctl.register({'cluster_address': '127.0.0.1', 'unit_name': 'etcd0', 'management_port': '1313', 'leader_address': 'http://127.1.1.1:1212'}) spcm.assert_called_with('member add etcd0 https://127.0.0.1:1313', api=2, endpoints='http://127.1.1.1:1212') def test_unregister(self, etcdctl): with patch('etcdctl.EtcdCtl.run') as spcm: etcdctl.unregister('br1212121212') spcm.assert_called_with(['member', 'remove', 'br1212121212'], api=2, endpoints=None) def test_member_list(self, etcdctl): with patch('etcdctl.EtcdCtl.run') as comock: comock.return_value = '7dc8404daa2b8ca0: name=etcd22 peerURLs=https://10.113.96.220:2380 clientURLs=https://10.113.96.220:2379\n' # noqa members = etcdctl.member_list() assert(members['etcd22']['unit_id'] == '7dc8404daa2b8ca0') assert(members['etcd22']['peer_urls'] == 'https://10.113.96.220:2380') assert(members['etcd22']['client_urls'] == 'https://10.113.96.220:2379') def test_member_list_with_unstarted_member(self, etcdctl): ''' Validate we receive information only about members we can parse from the current status string ''' # 57fa5c39949c138e[unstarted]: peerURLs=http://10.113.96.80:2380 # bb0f83ebb26386f7: name=etcd9 peerURLs=https://10.113.96.178:2380 clientURLs=https://10.113.96.178:2379 with patch('etcdctl.EtcdCtl.run') as comock: comock.return_value = '57fa5c39949c138e[unstarted]: peerURLs=http://10.113.96.80:2380]\nbb0f83ebb26386f7: name=etcd9 peerURLs=https://10.113.96.178:2380 clientURLs=https://10.113.96.178:2379\n' # noqa members = etcdctl.member_list() assert(members['etcd9']['unit_id'] == 'bb0f83ebb26386f7') assert(members['etcd9']['peer_urls'] == 'https://10.113.96.178:2380') assert(members['etcd9']['client_urls'] == 'https://10.113.96.178:2379') assert('unstarted' in members.keys()) assert(members['unstarted']['unit_id'] == '57fa5c39949c138e') assert("10.113.96.80:2380" in members['unstarted']['peer_urls']) def test_etcd_v2_version(self, etcdctl): ''' Validate that etcdctl can parse versions for both etcd v2 and etcd v3 ''' # Define fixtures of what we expect for the version output etcdctl_2_version = b"etcdctl version 2.3.8\n" with patch('etcdctl.check_output') as comock: comock.return_value = etcdctl_2_version ver = etcdctl.version() assert(ver == '2.3.8') def test_etcd_v3_version(self, etcdctl): ''' Validate that etcdctl can parse version for etcdctl v3 ''' etcdctl_3_version = b"etcdctl version: 3.0.17\nAPI version: 2\n" with patch('etcdctl.check_output') as comock: comock.return_value = etcdctl_3_version ver = etcdctl.version() assert(ver == '3.0.17') def test_etcdctl_command(self): ''' Validate sane results from etcdctl_command ''' assert(isinstance(etcdctl_command(), str)) def test_etcdctl_environment_with_version_2(self, etcdctl): ''' Validate that environment gets set correctly spoiler alert; it shouldn't be set when passing --version ''' with patch('etcdctl.check_output') as comock: etcdctl.run('member list', api=2) api_version = comock.call_args[1].get('env').get('ETCDCTL_API') assert(api_version == '2') def test_etcdctl_environment_with_version_3(self, etcdctl): ''' Validate that environment gets set correctly spoiler alert; it shouldn't be set when passing --version ''' with patch('etcdctl.check_output') as comock: etcdctl.run('member list', api=3) api_version = comock.call_args[1].get('env').get('ETCDCTL_API') assert(api_version == '3') def test_get_connection_string(self): ''' Validate the get_connection_string function gives a sane return. ''' assert( get_connection_string(['1.1.1.1'], '1111') == 'https://1.1.1.1:1111' ) @patch('reactive.etcd.render_grafana_dashboard') def test_register_grafana_dashboard(self, mock_dashboard_render): """Register grafana dashboard.""" dashboard_json = {'foo': 'bar'} mock_dashboard_render.return_value = dashboard_json grafana = MagicMock() endpoint_from_flag.return_value = grafana register_grafana_dashboard() mock_dashboard_render.assert_called_once() grafana.register_dashboard.assert_called_with( name=GRAFANA_DASHBOARD_NAME, dashboard=dashboard_json) reactive.etcd.set_flag.assert_called_with('grafana.configured') def test_register_prometheus_job(self, mocker): """Test successful registration of prometheus job.""" ingress_address = '10.0.0.1' port = '2379' targets = ['{}:{}'.format(ingress_address, port)] prometheus_mock = MagicMock() etcd_cluster_mock = MagicMock() job_data = {'scheme': 'https', 'static_configs': [{'targets': targets}] } etcd_cluster_mock.get_db_ingress_addresses.return_value = [] endpoint_from_flag.side_effect = [prometheus_mock, etcd_cluster_mock] mocker.patch.object(reactive.etcd, 'get_ingress_address', return_value=ingress_address) reactive.etcd.config.return_value = port register_prometheus_jobs() prometheus_mock.register_job.assert_called_with(job_name='etcd', job_data=job_data) reactive.etcd.set_flag.assert_called_with('prometheus.configured') def test_series_upgrade(self): assert host.service_pause.call_count == 0 assert host.service_resume.call_count == 0 assert status.blocked.call_count == 0 pre_series_upgrade() assert host.service_pause.call_count == 1 assert host.service_resume.call_count == 0 assert status.blocked.call_count == 1 post_series_upgrade() assert host.service_pause.call_count == 1 assert host.service_resume.call_count == 1 assert status.blocked.call_count == 1 @patch('reactive.etcd.force_rejoin') @patch('reactive.etcd.check_cluster_health') def test_rejoin_trigger(self, cluster_health_mock, rejoin_mock): """Test that unit will trigger force_rejoin on new request""" force_rejoin_requested() rejoin_mock.assert_called_once() cluster_health_mock.assert_called_once() @patch('reactive.etcd.register_node_with_leader') @patch('os.path.exists') @patch('shutil.rmtree') @patch('os.path.join') @patch('time.sleep') def test_force_rejoin(self, sleep, path_join, rmtree, path_exists, register_node): """Test that force_rejoin performs required steps.""" data_dir = '/foo/bar' path_exists.return_value = True path_join.return_value = data_dir force_rejoin() host.service_stop.assert_called_with(EtcdDatabag().etcd_daemon) clear_flag.assert_called_with('etcd.registered') rmtree.assert_called_with(data_dir) register_node.assert_called()