Hello community, here is the log from the commit of package crmsh for openSUSE:Factory checked in at 2020-02-18 13:29:25 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/crmsh (Old) and /work/SRC/openSUSE:Factory/.crmsh.new.26092 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "crmsh" Tue Feb 18 13:29:25 2020 rev:174 rq:775026 version:4.2.0+git.1581993539.5a1deb03 Changes: -------- --- /work/SRC/openSUSE:Factory/crmsh/crmsh.changes 2020-02-03 11:16:24.629938705 +0100 +++ /work/SRC/openSUSE:Factory/.crmsh.new.26092/crmsh.changes 2020-02-18 13:29:26.480706709 +0100 @@ -1,0 +2,10 @@ +Tue Feb 18 02:53:29 UTC 2020 - xli...@suse.com + +- Update to version 4.2.0+git.1581993539.5a1deb03: + * Dev: behave: functional test for setup cluster with crossed network + * Dev: unittest: test for get_peer_hostname/is_online/find_configured_ip functions + * Dev: corosync: check whether local ip has already configured + * Dev: bootstrap: check whether init node is online while joining + * Dev: bootstrap: for udpu, don't check join node's ip was in the same network + +------------------------------------------------------------------- Old: ---- crmsh-4.2.0+git.1580544897.c42c9530.tar.bz2 New: ---- crmsh-4.2.0+git.1581993539.5a1deb03.tar.bz2 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ crmsh.spec ++++++ --- /var/tmp/diff_new_pack.2egUa9/_old 2020-02-18 13:29:27.540708771 +0100 +++ /var/tmp/diff_new_pack.2egUa9/_new 2020-02-18 13:29:27.548708786 +0100 @@ -36,7 +36,7 @@ Summary: High Availability cluster command-line interface License: GPL-2.0-or-later Group: %{pkg_group} -Version: 4.2.0+git.1580544897.c42c9530 +Version: 4.2.0+git.1581993539.5a1deb03 Release: 0 Url: http://crmsh.github.io Source0: %{name}-%{version}.tar.bz2 ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.2egUa9/_old 2020-02-18 13:29:27.608708902 +0100 +++ /var/tmp/diff_new_pack.2egUa9/_new 2020-02-18 13:29:27.608708902 +0100 @@ -5,4 +5,4 @@ <param name="url">https://github.com/liangxin1300/crmsh.git</param> <param name="changesrevision">d8dc51b4cb34964aa72e918999ebc7f03b48f3c9</param></service><service name="tar_scm"> <param name="url">https://github.com/ClusterLabs/crmsh.git</param> - <param name="changesrevision">a8fc76646a14dfcf7b34e62303d2195ef96c3daa</param></service></servicedata> \ No newline at end of file + <param name="changesrevision">e2a36e8724bbbb63a85806255adc08396883b53a</param></service></servicedata> \ No newline at end of file ++++++ crmsh-4.2.0+git.1580544897.c42c9530.tar.bz2 -> crmsh-4.2.0+git.1581993539.5a1deb03.tar.bz2 ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.2.0+git.1580544897.c42c9530/crmsh/bootstrap.py new/crmsh-4.2.0+git.1581993539.5a1deb03/crmsh/bootstrap.py --- old/crmsh-4.2.0+git.1580544897.c42c9530/crmsh/bootstrap.py 2020-02-01 09:14:57.000000000 +0100 +++ new/crmsh-4.2.0+git.1581993539.5a1deb03/crmsh/bootstrap.py 2020-02-18 03:38:59.000000000 +0100 @@ -18,6 +18,7 @@ import re import time import readline +import shutil from string import Template from lxml import etree from . import config @@ -38,6 +39,7 @@ SYSCONFIG_FW = "/etc/sysconfig/SuSEfirewall2" SYSCONFIG_FW_CLUSTER = "/etc/sysconfig/SuSEfirewall2.d/services/cluster" PCMK_REMOTE_AUTH = "/etc/pacemaker/authkey" +COROSYNC_CONF_ORIG = tmpfiles.create()[1] INIT_STAGES = ("ssh", "ssh_remote", "csync2", "csync2_remote", "corosync", "storage", "sbd", "cluster", "vgfs", "admin", "qdevice") @@ -228,13 +230,53 @@ status_long("Waiting for cluster") while True: _rc, out, _err = utils.get_stdout_stderr("crm_mon -1") - if "online" in out.lower(): + if is_online(out): break status_progress() - sleep(5) + sleep(2) status_done() +def get_cluster_node_hostname(): + """ + Get the hostname of the cluster node used during the join process if an IP address is used. + """ + peer_node = None + if _context.cluster_node: + if utils.valid_ip_addr(_context.cluster_node): + rc, out, err = utils.get_stdout_stderr("ssh {} crm_node --name".format(_context.cluster_node)) + if rc != 0: + error(err) + peer_node = out + else: + peer_node = _context.cluster_node + return peer_node + + +def is_online(crm_mon_txt): + """ + Check whether local node is online + Besides that, in join process, check whether init node is online + """ + if not re.search("Online: .* {} ".format(utils.this_node()), crm_mon_txt): + return False + + # if peer_node is None, this is in the init process + peer_node = get_cluster_node_hostname() + if peer_node is None: + return True + # In join process + # If the joining node is already online but can't find the init node + # The communication IP maybe mis-configured + if not re.search("Online: .* {} ".format(peer_node), crm_mon_txt): + shutil.copy(COROSYNC_CONF_ORIG, corosync.conf()) + csync2_update(corosync.conf()) + stop_service("corosync") + print() + error("Cannot see peer node \"{}\", please check the communication IP".format(peer_node)) + return True + + def pick_default_value(default_list, prev_list): """ Provide default value for function 'prompt_for_string'. @@ -1846,6 +1888,8 @@ else: corosync.set_value("totem.nodeid", nodeid) + shutil.copy(corosync.conf(), COROSYNC_CONF_ORIG) + # check if use IPv6 ipv6_flag = False ipv6 = corosync.get_value("totem.ip_version") @@ -1909,21 +1953,6 @@ ringXaddr_res) if not ringXaddr: error("No value for ring{}".format(i)) - - # this check does not work on GCP (bsc#1106946) - if utils.detect_cloud() != "google-cloud-platform": - _, outp = utils.get_stdout("ip addr show") - tmp = re.findall(r' {}/[0-9]+ '.format(ringXaddr), outp, re.M)[0].strip() - peer_ip = corosync.get_value("nodelist.node.ring{}_addr".format(i)) - # peer ring0_addr and local ring0_addr must be configured in the same network - if not utils.ip_in_network(peer_ip, tmp): - errmsg = " Peer IP {} is not in the same network: {}".format(peer_ip, tmp) - if _context.yes_to_all: - error(errmsg) - else: - print(term.render(clidisplay.error(errmsg))) - continue - ringXaddr_res.append(ringXaddr) break if not rrp_flag: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.2.0+git.1580544897.c42c9530/crmsh/corosync.py new/crmsh-4.2.0+git.1581993539.5a1deb03/crmsh/corosync.py --- old/crmsh-4.2.0+git.1580544897.c42c9530/crmsh/corosync.py 2020-02-01 09:14:57.000000000 +0100 +++ new/crmsh-4.2.0+git.1581993539.5a1deb03/crmsh/corosync.py 2020-02-18 03:38:59.000000000 +0100 @@ -811,24 +811,47 @@ pass -def add_node_ucast(IParray, node_id=None): +def find_configured_ip(ip_list): + """ + find if the same IP already configured + If so, raise IPAlreadyConfiguredError + """ + with open(conf()) as f: + p = Parser(f.read()) + + # get exist ip list from corosync.conf + corosync_iplist = [] + for path in set(p.all_paths()): + if re.search('nodelist.node.ring[0-9]*_addr', path): + corosync_iplist.extend(p.get_all(path)) + + # all_possible_ip is a ip set to check whether one of them already configured + all_possible_ip = set(ip_list) + # get local ip list + local_ip_list = utils.ip_in_local(utils.is_ipv6(ip_list[0])) + # extend all_possible_ip if ip_list contain local ip + # to avoid this scenarios in join node: + # eth0's ip already configured in corosync.conf + # eth1's ip also want to add in nodelist + # if this scenarios happened, raise IPAlreadyConfiguredError + if bool(set(ip_list) & set(local_ip_list)): + all_possible_ip |= set(local_ip_list) + configured_ip = list(all_possible_ip & set(corosync_iplist)) + if configured_ip: + raise IPAlreadyConfiguredError("IP {} was already configured".format(','.join(configured_ip))) + + +def add_node_ucast(ip_list, node_id=None): + + find_configured_ip(ip_list) f = open(conf()).read() p = Parser(f) - # to check if the same IP already configured - exist_iplist = [] - for path in p.all_paths(): - if re.search('nodelist.node.ring[0-9]*_addr', path): - exist_iplist.extend(p.get_all(path)) - for ip in IParray: - if ip in exist_iplist: - raise IPAlreadyConfiguredError("IP {} was already configured".format(ip)) - if node_id is None: node_id = get_free_nodeid(p) node_value = [] - for i, addr in enumerate(IParray): + for i, addr in enumerate(ip_list): node_value += make_value('nodelist.node.ring{}_addr'.format(i), addr) node_value += make_value('nodelist.node.nodeid', str(node_id)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.2.0+git.1580544897.c42c9530/crmsh/utils.py new/crmsh-4.2.0+git.1581993539.5a1deb03/crmsh/utils.py --- old/crmsh-4.2.0+git.1580544897.c42c9530/crmsh/utils.py 2020-02-01 09:14:57.000000000 +0100 +++ new/crmsh-4.2.0+git.1581993539.5a1deb03/crmsh/utils.py 2020-02-18 03:38:59.000000000 +0100 @@ -2083,6 +2083,11 @@ def version(self): return self.addr.version + +def is_ipv6(addr): + return IP(addr).version() == 6 + + # Set by detect_cloud() or iplist_for_cloud() # to avoid multiple requests _ip_for_cloud = None diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.2.0+git.1580544897.c42c9530/test/features/bootstrap_bugs.feature new/crmsh-4.2.0+git.1581993539.5a1deb03/test/features/bootstrap_bugs.feature --- old/crmsh-4.2.0+git.1580544897.c42c9530/test/features/bootstrap_bugs.feature 2020-02-01 09:14:57.000000000 +0100 +++ new/crmsh-4.2.0+git.1581993539.5a1deb03/test/features/bootstrap_bugs.feature 2020-02-18 03:38:59.000000000 +0100 @@ -33,3 +33,15 @@ Then Except "ERROR: cluster.geo_join: Space value not allowed for dest "node"" When Try "crm cluster geo-init-arbitrator -c ' '" Then Except "ERROR: cluster.geo_init_arbitrator: Space value not allowed for dest "other"" + + @clean + Scenario: Setup cluster with crossed network(udpu only) + Given Cluster service is "stopped" on "hanode1" + Given Cluster service is "stopped" on "hanode2" + When Run "crm cluster init -u -i eth0 -y --no-overwrite-sshkey" on "hanode1" + Then Cluster service is "started" on "hanode1" + When Try "crm cluster join -c hanode1 -i eth1 -y" on "hanode2" + Then Cluster service is "stopped" on "hanode2" + And Except "Cannot see peer node "hanode1", please check the communication IP" in stderr + When Run "crm cluster join -c hanode1 -i eth0 -y" on "hanode2" + Then Cluster service is "started" on "hanode2" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.2.0+git.1580544897.c42c9530/test/features/steps/step_implenment.py new/crmsh-4.2.0+git.1581993539.5a1deb03/test/features/steps/step_implenment.py --- old/crmsh-4.2.0+git.1580544897.c42c9530/test/features/steps/step_implenment.py 2020-02-01 09:14:57.000000000 +0100 +++ new/crmsh-4.2.0+git.1581993539.5a1deb03/test/features/steps/step_implenment.py 2020-02-18 03:38:59.000000000 +0100 @@ -35,6 +35,11 @@ context.stdout = out +@when('Try "{cmd}" on "{addr}"') +def step_impl(context, cmd, addr): + run_command_local_or_remote(context, cmd, addr, err_record=True) + + @when('Try "{cmd}"') def step_impl(context, cmd): run_command(context, cmd, err_record=True) @@ -69,6 +74,12 @@ context.command_error_output = None +@then('Except "{msg}" in stderr') +def step_impl(context, msg): + assert msg in context.command_error_output + context.command_error_output = None + + @then('Cluster service is "{state}" on "{addr}"') def step_impl(context, state, addr): assert check_cluster_state(context, state, addr) is True diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.2.0+git.1580544897.c42c9530/test/features/steps/utils.py new/crmsh-4.2.0+git.1581993539.5a1deb03/test/features/steps/utils.py --- old/crmsh-4.2.0+git.1580544897.c42c9530/test/features/steps/utils.py 2020-02-01 09:14:57.000000000 +0100 +++ new/crmsh-4.2.0+git.1581993539.5a1deb03/test/features/steps/utils.py 2020-02-18 03:38:59.000000000 +0100 @@ -19,14 +19,17 @@ return out -def run_command_local_or_remote(context, cmd, addr): +def run_command_local_or_remote(context, cmd, addr, err_record=False): if addr == me(): - out = run_command(context, cmd) + out = run_command(context, cmd, err_record) return out else: try: results = parallax.parallax_call([addr], cmd) except ValueError as err: + if err_record: + context.command_error_output = str(err) + return context.logger.error("\n{}\n".format(err)) context.failed = True else: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.2.0+git.1580544897.c42c9530/test/unittests/test_bootstrap.py new/crmsh-4.2.0+git.1581993539.5a1deb03/test/unittests/test_bootstrap.py --- old/crmsh-4.2.0+git.1580544897.c42c9530/test/unittests/test_bootstrap.py 2020-02-01 09:14:57.000000000 +0100 +++ new/crmsh-4.2.0+git.1581993539.5a1deb03/test/unittests/test_bootstrap.py 2020-02-18 03:38:59.000000000 +0100 @@ -32,7 +32,6 @@ Global setUp. """ - #@mock.patch('crmsh.bootstrap.Context') def setUp(self): """ Test setUp. @@ -158,3 +157,128 @@ mock.call(['10.10.10.1', '20.20.20.1'], '1'), mock.call(['10.10.10.2', '20.20.20.2'], '2') ]) + + @mock.patch('crmsh.utils.valid_ip_addr') + @mock.patch('crmsh.utils.get_stdout_stderr') + def test_get_cluster_node_hostname_None(self, mock_stdout_stderr, mock_valid_ip): + bootstrap._context = mock.Mock(cluster_node=None) + + peer_node = bootstrap.get_cluster_node_hostname() + assert peer_node is None + + mock_valid_ip.assert_not_called() + mock_stdout_stderr.assert_not_called() + + @mock.patch('crmsh.utils.valid_ip_addr') + @mock.patch('crmsh.utils.get_stdout_stderr') + def test_get_cluster_node_hostname_IP(self, mock_stdout_stderr, mock_valid_ip): + bootstrap._context = mock.Mock(cluster_node="1.1.1.1") + mock_valid_ip.return_value = True + mock_stdout_stderr.return_value = (0, "node1", None) + + peer_node = bootstrap.get_cluster_node_hostname() + assert peer_node == "node1" + + mock_valid_ip.assert_called_once_with("1.1.1.1") + mock_stdout_stderr.assert_called_once_with("ssh 1.1.1.1 crm_node --name") + + @mock.patch('crmsh.utils.valid_ip_addr') + @mock.patch('crmsh.utils.get_stdout_stderr') + def test_get_cluster_node_hostname_HOST(self, mock_stdout_stderr, mock_valid_ip): + bootstrap._context = mock.Mock(cluster_node="node2") + mock_valid_ip.return_value = False + + peer_node = bootstrap.get_cluster_node_hostname() + assert peer_node == "node2" + + mock_valid_ip.assert_called_once_with("node2") + mock_stdout_stderr.assert_not_called() + + @mock.patch('crmsh.utils.this_node') + @mock.patch('re.search') + @mock.patch('crmsh.bootstrap.get_cluster_node_hostname') + def test_is_online_local_offline(self, mock_get_peer, mock_search, mock_this_node): + mock_this_node.return_value = "node1" + mock_search.return_value = None + + assert bootstrap.is_online("text") is False + + mock_this_node.assert_called_once_with() + mock_get_peer.assert_not_called() + mock_search.assert_called_once_with("Online: .* node1 ", "text") + + @mock.patch('crmsh.utils.this_node') + @mock.patch('re.search') + @mock.patch('crmsh.bootstrap.get_cluster_node_hostname') + def test_is_online_on_init_node(self, mock_get_peer, mock_search, mock_this_node): + mock_search.return_value = mock.Mock() + mock_this_node.return_value = "node1" + mock_get_peer.return_value = None + + assert bootstrap.is_online("text") is True + + mock_this_node.assert_called_once_with() + mock_get_peer.assert_called_once_with() + mock_search.assert_called_once_with("Online: .* node1 ", "text") + + @mock.patch('crmsh.bootstrap.error') + @mock.patch('crmsh.bootstrap.stop_service') + @mock.patch('crmsh.bootstrap.csync2_update') + @mock.patch('crmsh.corosync.conf') + @mock.patch('shutil.copy') + @mock.patch('crmsh.utils.this_node') + @mock.patch('re.search') + @mock.patch('crmsh.bootstrap.get_cluster_node_hostname') + def test_is_online_peer_offline(self, mock_get_peer, mock_search, mock_this_node, + mock_copy, mock_corosync_conf, mock_csync2, mock_stop_service, mock_error): + bootstrap.COROSYNC_CONF_ORIG = "/tmp/crmsh_tmpfile" + mock_search.side_effect = [ mock.Mock(), None ] + mock_this_node.return_value = "node2" + mock_get_peer.return_value = "node1" + mock_corosync_conf.side_effect = [ "/etc/corosync/corosync.conf", + "/etc/corosync/corosync.conf"] + + bootstrap.is_online("text") + + mock_this_node.assert_called_once_with() + mock_get_peer.assert_called_once_with() + mock_search.assert_has_calls([ + mock.call("Online: .* node2 ", "text"), + mock.call("Online: .* node1 ", "text") + ]) + mock_corosync_conf.assert_has_calls([ + mock.call(), + mock.call() + ]) + mock_copy.assert_called_once_with(bootstrap.COROSYNC_CONF_ORIG, "/etc/corosync/corosync.conf") + mock_csync2.assert_called_once_with("/etc/corosync/corosync.conf") + mock_stop_service.assert_called_once_with("corosync") + mock_error.assert_called_once_with("Cannot see peer node \"node1\", please check the communication IP") + + @mock.patch('crmsh.bootstrap.error') + @mock.patch('crmsh.bootstrap.stop_service') + @mock.patch('crmsh.bootstrap.csync2_update') + @mock.patch('crmsh.corosync.conf') + @mock.patch('shutil.copy') + @mock.patch('crmsh.utils.this_node') + @mock.patch('re.search') + @mock.patch('crmsh.bootstrap.get_cluster_node_hostname') + def test_is_online_both_online(self, mock_get_peer, mock_search, mock_this_node, + mock_copy, mock_corosync_conf, mock_csync2, mock_stop_service, mock_error): + mock_search.side_effect = [ mock.Mock(), mock.Mock() ] + mock_this_node.return_value = "node2" + mock_get_peer.return_value = "node1" + + assert bootstrap.is_online("text") is True + + mock_this_node.assert_called_once_with() + mock_get_peer.assert_called_once_with() + mock_search.assert_has_calls([ + mock.call("Online: .* node2 ", "text"), + mock.call("Online: .* node1 ", "text") + ]) + mock_corosync_conf.assert_not_called() + mock_copy.assert_not_called() + mock_csync2.assert_not_called() + mock_stop_service.assert_not_called() + mock_error.assert_not_called() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.2.0+git.1580544897.c42c9530/test/unittests/test_corosync.py new/crmsh-4.2.0+git.1581993539.5a1deb03/test/unittests/test_corosync.py --- old/crmsh-4.2.0+git.1580544897.c42c9530/test/unittests/test_corosync.py 2020-02-01 09:14:57.000000000 +0100 +++ new/crmsh-4.2.0+git.1581993539.5a1deb03/test/unittests/test_corosync.py 2020-02-18 03:38:59.000000000 +0100 @@ -93,6 +93,60 @@ make_value('nodelist.node.nodeid', str(nid)))) _valid(p) self.assertEqual(p.count('nodelist.node'), nid - 1) + + @mock.patch("crmsh.utils.is_ipv6") + @mock.patch("crmsh.utils.ip_in_local") + @mock.patch("re.search") + @mock.patch("crmsh.corosync.Parser") + @mock.patch("crmsh.corosync.conf") + @mock.patch("builtins.open", new_callable=mock.mock_open, read_data="corosync conf data") + def test_find_configured_ip_no_exception(self, mock_open_file, mock_conf, mock_parser, mock_search, mock_ip_local, mock_isv6): + mock_conf.return_value = "/etc/corosync/corosync.conf" + mock_parser_inst = mock.Mock() + mock_parser.return_value = mock_parser_inst + mock_parser_inst.all_paths.return_value = ["nodelist.node.ring0_addr"] + mock_search.return_value = mock.Mock() + mock_parser_inst.get_all.return_value = ["10.10.10.1"] + mock_isv6.return_value = False + mock_ip_local.return_value = ["192.168.1.1", "10.10.10.2", "20.20.20.2"] + + corosync.find_configured_ip(["10.10.10.2"]) + + mock_conf.assert_called_once_with() + mock_parser_inst.all_paths.assert_called_once_with() + mock_parser_inst.get_all.assert_called_once_with("nodelist.node.ring0_addr") + mock_open_file.assert_called_once_with(mock_conf.return_value) + mock_isv6.assert_called_once_with("10.10.10.2") + mock_ip_local.assert_called_once_with(False) + mock_search.assert_called_once_with("nodelist.node.ring[0-9]*_addr", "nodelist.node.ring0_addr") + + @mock.patch("crmsh.utils.is_ipv6") + @mock.patch("crmsh.utils.ip_in_local") + @mock.patch("re.search") + @mock.patch("crmsh.corosync.Parser") + @mock.patch("crmsh.corosync.conf") + @mock.patch("builtins.open", new_callable=mock.mock_open, read_data="corosync conf data") + def test_find_configured_ip_exception(self, mock_open_file, mock_conf, mock_parser, mock_search, mock_ip_local, mock_isv6): + mock_conf.return_value = "/etc/corosync/corosync.conf" + mock_parser_inst = mock.Mock() + mock_parser.return_value = mock_parser_inst + mock_parser_inst.all_paths.return_value = ["nodelist.node.ring0_addr"] + mock_search.return_value = mock.Mock() + mock_parser_inst.get_all.return_value = ["10.10.10.1", "10.10.10.2"] + mock_isv6.return_value = False + mock_ip_local.return_value = ["192.168.1.1", "10.10.10.2", "20.20.20.2"] + + with self.assertRaises(corosync.IPAlreadyConfiguredError) as err: + corosync.find_configured_ip(["10.10.10.2"]) + self.assertEqual("IP 10.10.10.2 was already configured", str(err.exception)) + + mock_conf.assert_called_once_with() + mock_parser_inst.all_paths.assert_called_once_with() + mock_parser_inst.get_all.assert_called_once_with("nodelist.node.ring0_addr") + mock_open_file.assert_called_once_with(mock_conf.return_value) + mock_isv6.assert_called_once_with("10.10.10.2") + mock_ip_local.assert_called_once_with(False) + mock_search.assert_called_once_with("nodelist.node.ring[0-9]*_addr", "nodelist.node.ring0_addr") def test_add_node_ucast(self): from crmsh.corosync import add_node_ucast, get_values