Hello community, here is the log from the commit of package crmsh for openSUSE:Factory checked in at 2019-11-06 13:56:46 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/crmsh (Old) and /work/SRC/openSUSE:Factory/.crmsh.new.2990 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "crmsh" Wed Nov 6 13:56:46 2019 rev:165 rq:745170 version:4.1.0+git.1572504697.472361c5 Changes: -------- --- /work/SRC/openSUSE:Factory/crmsh/crmsh.changes 2019-10-30 14:46:46.358115561 +0100 +++ /work/SRC/openSUSE:Factory/.crmsh.new.2990/crmsh.changes 2019-11-06 13:56:49.444203017 +0100 @@ -1,0 +2,17 @@ +Thu Oct 31 06:57:08 UTC 2019 - xli...@suse.com + +- Update to version 4.1.0+git.1572504697.472361c5: + * Doc: ui_configure: do_property: ask to remove maintenance from resources and nodes + * Test: ui_configure: do_property: ask to remove maintenance from resources and nodes + * Dev: ui_configure: do_property: ask to remove maintenance from resources and nodes + +------------------------------------------------------------------- +Tue Oct 29 21:57:47 UTC 2019 - xli...@suse.com + +- Update to version 4.1.0+git.1572385946.69f4f51b: + * Low: unittest: test init_ssh and init_ssh_remote in bootstrap.py + * Low: bootstrap: create authorized_keys file if not exists + * Low: bootstrap: add "--no-overwrite-sshkey" option to avoid SSH key be overwritten + * Low: bootstrap: don't overwrite ssh key if already exists + +------------------------------------------------------------------- Old: ---- crmsh-4.1.0+git.1572337494.6f2c8ea9.tar.bz2 New: ---- crmsh-4.1.0+git.1572504697.472361c5.tar.bz2 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ crmsh.spec ++++++ --- /var/tmp/diff_new_pack.WURMsp/_old 2019-11-06 13:56:50.712204346 +0100 +++ /var/tmp/diff_new_pack.WURMsp/_new 2019-11-06 13:56:50.720204354 +0100 @@ -36,7 +36,7 @@ Summary: High Availability cluster command-line interface License: GPL-2.0-or-later Group: %{pkg_group} -Version: 4.1.0+git.1572337494.6f2c8ea9 +Version: 4.1.0+git.1572504697.472361c5 Release: 0 Url: http://crmsh.github.io Source0: %{name}-%{version}.tar.bz2 ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.WURMsp/_old 2019-11-06 13:56:50.824204464 +0100 +++ /var/tmp/diff_new_pack.WURMsp/_new 2019-11-06 13:56:50.824204464 +0100 @@ -1,4 +1,4 @@ <servicedata> <service name="tar_scm"> <param name="url">git://github.com/ClusterLabs/crmsh.git</param> - <param name="changesrevision">6f2c8ea926f61f2ce99fcfbefa401d51feaddbf1</param></service></servicedata> \ No newline at end of file + <param name="changesrevision">c8d41bd637dd03b4d60a9f35ca099e41ac32eec4</param></service></servicedata> \ No newline at end of file ++++++ crmsh-4.1.0+git.1572337494.6f2c8ea9.tar.bz2 -> crmsh-4.1.0+git.1572504697.472361c5.tar.bz2 ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.1.0+git.1572337494.6f2c8ea9/crmsh/bootstrap.py new/crmsh-4.1.0+git.1572504697.472361c5/crmsh/bootstrap.py --- old/crmsh-4.1.0+git.1572337494.6f2c8ea9/crmsh/bootstrap.py 2019-10-29 09:24:54.000000000 +0100 +++ new/crmsh-4.1.0+git.1572504697.472361c5/crmsh/bootstrap.py 2019-10-31 07:51:37.000000000 +0100 @@ -670,7 +670,8 @@ start_service("sshd.service") invoke("mkdir -m 700 -p /root/.ssh") if os.path.exists("/root/.ssh/id_rsa"): - if not confirm("/root/.ssh/id_rsa already exists - overwrite?"): + if _context.yes_to_all and _context.no_overwrite_sshkey or \ + not confirm("/root/.ssh/id_rsa already exists - overwrite?"): return rmfile("/root/.ssh/id_rsa") status("Generating SSH key") @@ -682,7 +683,10 @@ """ Called by ha-cluster-join """ - authkeys = open("/root/.ssh/authorized_keys", "r+") + authorized_keys_file = "/root/.ssh/authorized_keys" + if not os.path.exists(authorized_keys_file): + open(authorized_keys_file, 'w').close() + authkeys = open(authorized_keys_file, "r+") authkeys_data = authkeys.read() for key in ("id_rsa", "id_dsa", "id_ecdsa", "id_ed25519"): fn = os.path.join("/root/.ssh", key) @@ -690,7 +694,7 @@ continue keydata = open(fn + ".pub").read() if keydata not in authkeys_data: - append(fn + ".pub", "/root/.ssh/authorized_keys") + append(fn + ".pub", authorized_keys_file) def init_csync2(): @@ -2056,7 +2060,7 @@ def bootstrap_init(cluster_name="hacluster", ui_context=None, nic=None, ocfs2_device=None, shared_device=None, sbd_device=None, diskless_sbd=False, quiet=False, - template=None, admin_ip=None, yes_to_all=False, + template=None, admin_ip=None, yes_to_all=False, no_overwrite_sshkey=False, unicast=False, second_hb=False, ipv6=False, watchdog=None, qdevice=None, stage=None, args=None): """ -i <nic> @@ -2097,6 +2101,7 @@ _context.watchdog = watchdog _context.ui_context = ui_context _context.qdevice = qdevice + _context.no_overwrite_sshkey = no_overwrite_sshkey def check_option(): if _context.admin_ip and not valid_adminIP(_context.admin_ip): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.1.0+git.1572337494.6f2c8ea9/crmsh/ui_cluster.py new/crmsh-4.1.0+git.1572504697.472361c5/crmsh/ui_cluster.py --- old/crmsh-4.1.0+git.1572337494.6f2c8ea9/crmsh/ui_cluster.py 2019-10-29 09:24:54.000000000 +0100 +++ new/crmsh-4.1.0+git.1572504697.472361c5/crmsh/ui_cluster.py 2019-10-31 07:51:37.000000000 +0100 @@ -187,7 +187,7 @@ parser.add_option("-q", "--quiet", action="store_true", dest="quiet", help="Be quiet (don't describe what's happening, just do it)") parser.add_option("-y", "--yes", action="store_true", dest="yes_to_all", - help='Answer "yes" to all prompts (use with caution, this is destructive, especially during the "storage" stage)') + help='Answer "yes" to all prompts (use with caution, this is destructive, especially during the "storage" stage. The /root/.ssh/id_rsa key will be overwritten unless the option "--no-overwrite-sshkey" is used)') parser.add_option("-t", "--template", dest="template", help='Optionally configure cluster with template "name" (currently only "ocfs2" is valid here)') parser.add_option("-n", "--name", metavar="NAME", dest="name", default="hacluster", @@ -200,6 +200,8 @@ help="Enable SBD even if no SBD device is configured (diskless mode)") parser.add_option("-w", "--watchdog", dest="watchdog", metavar="WATCHDOG", help="Use the given watchdog device") + parser.add_option("--no-overwrite-sshkey", action="store_true", dest="no_overwrite_sshkey", + help='Avoid "/root/.ssh/id_rsa" overwrite if "-y" option is used (False by default)') network_group = optparse.OptionGroup(parser, "Network configuration", "Options for configuring the network and messaging layer.") network_group.add_option("-i", "--interface", dest="nic", metavar="IF", @@ -278,6 +280,7 @@ template=options.template, admin_ip=options.admin_ip, yes_to_all=options.yes_to_all, + no_overwrite_sshkey=options.no_overwrite_sshkey, unicast=options.unicast, second_hb=options.second_hb, ipv6=options.ipv6, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.1.0+git.1572337494.6f2c8ea9/crmsh/ui_configure.py new/crmsh-4.1.0+git.1572504697.472361c5/crmsh/ui_configure.py --- old/crmsh-4.1.0+git.1572337494.6f2c8ea9/crmsh/ui_configure.py 2019-10-29 09:24:54.000000000 +0100 +++ new/crmsh-4.1.0+git.1572504697.472361c5/crmsh/ui_configure.py 2019-10-31 07:51:37.000000000 +0100 @@ -27,6 +27,7 @@ from . import ui_utils from . import ui_assist from .crm_gv import gv_types +from .ui_node import get_resources_on_nodes, remove_redundant_attrs def _type_completions(): @@ -884,6 +885,34 @@ return True return cib_factory.change_schema(schema_st) + def __override_lower_level_attrs(self, *args): + """ + When setting up an attribute of a cluster, the same + attribute may already exist in one of the nodes an/or + any resource. + The user should be informed about it and, if he wants, + he will have an option to delete the already existing + attribute. + """ + if not args: + return + + nvpair = args[0].split('=', 1) + if 2 != len(nvpair): + return + + attr_name, attr_value = nvpair + + if "maintenance-mode" == attr_name: + attr = "maintenance" + conflicting_lower_level_attr = 'is-managed' + # FIXME! the first argument is hardcoded + objs = get_resources_on_nodes(cib_factory.node_id_list(), [ "primitive", "group", "clone"]) + remove_redundant_attrs(objs, "meta_attributes", attr, conflicting_lower_level_attr) + + objs = get_resources_on_nodes(cib_factory.node_id_list(), [ "node" ]) + remove_redundant_attrs(objs, "instance_attributes", attr, conflicting_lower_level_attr) + def __conf_object(self, cmd, *args): "The configure object command." if cmd in list(constants.cib_cli_map.values()) and \ @@ -1024,6 +1053,7 @@ @command.completers_repeating(_property_completer) def do_property(self, context, *args): "usage: property [$id=<set_id>] <option>=<value>" + self.__override_lower_level_attrs(*args) return self.__conf_object(context.get_command_name(), *args) @command.skill_level('administrator') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.1.0+git.1572337494.6f2c8ea9/crmsh/ui_node.py new/crmsh-4.1.0+git.1572504697.472361c5/crmsh/ui_node.py --- old/crmsh-4.1.0+git.1572337494.6f2c8ea9/crmsh/ui_node.py 2019-10-29 09:24:54.000000000 +0100 +++ new/crmsh-4.1.0+git.1572504697.472361c5/crmsh/ui_node.py 2019-10-31 07:51:37.000000000 +0100 @@ -16,21 +16,26 @@ from .cibconfig import cib_factory from .ui_resource import rm_meta_attribute -def remove_redundant_attrs(objs, attr, conflicting_attr = None): +def remove_redundant_attrs(objs, attributes_tag, attr, conflicting_attr = None): """ Remove attr from all resources_tags in the cib.xml """ + field2show = "id" # if attributes_tag == "meta_attributes" + # By default the id of the object should be shown + # The id of nodes is simply an integer number => show its uname field + if "instance_attributes" == attributes_tag: + field2show = "uname" # Override the resources on the node for r in objs: - for meta_set in xmlutil.get_set_nodes(r, "meta_attributes", create=False): + for meta_set in xmlutil.get_set_nodes(r, attributes_tag, create=False): a = xmlutil.get_attr_in_set(meta_set, attr) if a is not None and \ (config.core.manage_children == "always" or \ (config.core.manage_children == "ask" and utils.ask("'%s' attribute already exists in %s. Remove it?" % - (attr, r.get("id"))))): + (attr, r.get(field2show))))): common_debug("force remove meta attr %s from %s" % - (attr, r.get("id"))) + (attr, r.get(field2show))) xmlutil.rmnode(a) xmlutil.xml_processnodes(r, xmlutil.is_emptynvpairs, xmlutil.rmnodes) if conflicting_attr is not None: @@ -39,9 +44,9 @@ (config.core.manage_children == "always" or \ (config.core.manage_children == "ask" and utils.ask("'%s' conflicts with '%s' in %s. Remove it?" % - (conflicting_attr, attr, r.get("id"))))): + (conflicting_attr, attr, r.get(field2show))))): common_debug("force remove meta attr %s from %s" % - (conflicting_attr, r.get("id"))) + (conflicting_attr, r.get(field2show))) xmlutil.rmnode(a) xmlutil.xml_processnodes(r, xmlutil.is_emptynvpairs, xmlutil.rmnodes) @@ -84,7 +89,7 @@ objs = get_resources_on_nodes([cluster_node_name], [ "primitive", "group", "clone"]) # Ask the user to remove the 'attr' attributes on those primitives, groups and clones - remove_redundant_attrs(objs, attr, conflicting_attr) + remove_redundant_attrs(objs, "meta_attributes", attr, conflicting_attr) # Remove the node conflicting attribute nvpairs = xml_node.xpath("./instance_attributes/nvpair[@name='%s']" % (conflicting_attr)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.1.0+git.1572337494.6f2c8ea9/doc/crm.8.adoc new/crmsh-4.1.0+git.1572504697.472361c5/doc/crm.8.adoc --- old/crmsh-4.1.0+git.1572337494.6f2c8ea9/doc/crm.8.adoc 2019-10-29 09:24:54.000000000 +0100 +++ new/crmsh-4.1.0+git.1572504697.472361c5/doc/crm.8.adoc 2019-10-31 07:51:37.000000000 +0100 @@ -3841,6 +3841,9 @@ available cluster configuration properties, use the <<cmdhelp_ra_info,`ra info`>> command with +pengine+, +crmd+, +cib+ and +stonithd+ as arguments. +When setting the +maintenance-mode+ property, it will +inform the user if there are nodes or resources that +have the +maintenance+ property. For more information on rule expressions, see <<topics_Syntax_RuleExpressions,Syntax: Rule expressions>>. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.1.0+git.1572337494.6f2c8ea9/doc/website-v1/man-2.0.adoc new/crmsh-4.1.0+git.1572504697.472361c5/doc/website-v1/man-2.0.adoc --- old/crmsh-4.1.0+git.1572337494.6f2c8ea9/doc/website-v1/man-2.0.adoc 2019-10-29 09:24:54.000000000 +0100 +++ new/crmsh-4.1.0+git.1572504697.472361c5/doc/website-v1/man-2.0.adoc 2019-10-31 07:51:37.000000000 +0100 @@ -3471,6 +3471,9 @@ available cluster configuration properties, use the <<cmdhelp_ra_info,`ra info`>> command with +pengine+, +crmd+, +cib+ and +stonithd+ as arguments. +When setting the +maintenance-mode+ property, it will +inform the user if there are nodes or resources that +have the +maintenance+ property. For more information on rule expressions, see <<topics_Syntax_RuleExpressions,Syntax: Rule expressions>>. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.1.0+git.1572337494.6f2c8ea9/doc/website-v1/man-3.adoc new/crmsh-4.1.0+git.1572504697.472361c5/doc/website-v1/man-3.adoc --- old/crmsh-4.1.0+git.1572337494.6f2c8ea9/doc/website-v1/man-3.adoc 2019-10-29 09:24:54.000000000 +0100 +++ new/crmsh-4.1.0+git.1572504697.472361c5/doc/website-v1/man-3.adoc 2019-10-31 07:51:37.000000000 +0100 @@ -3732,6 +3732,9 @@ available cluster configuration properties, use the <<cmdhelp_ra_info,`ra info`>> command with +pengine+, +crmd+, +cib+ and +stonithd+ as arguments. +When setting the +maintenance-mode+ property, it will +inform the user if there are nodes or resources that +have the +maintenance+ property. For more information on rule expressions, see <<topics_Syntax_RuleExpressions,Syntax: Rule expressions>>. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.1.0+git.1572337494.6f2c8ea9/test/testcases/confbasic new/crmsh-4.1.0+git.1572504697.472361c5/test/testcases/confbasic --- old/crmsh-4.1.0+git.1572337494.6f2c8ea9/test/testcases/confbasic 2019-10-29 09:24:54.000000000 +0100 +++ new/crmsh-4.1.0+git.1572504697.472361c5/test/testcases/confbasic 2019-10-31 07:51:37.000000000 +0100 @@ -85,3 +85,7 @@ _test verify . +-F node maintenance node1 +-F resource maintenance g1 off +-F resource maintenance d1 +-F configure property maintenance-mode=true diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.1.0+git.1572337494.6f2c8ea9/test/testcases/confbasic.exp new/crmsh-4.1.0+git.1572504697.472361c5/test/testcases/confbasic.exp --- old/crmsh-4.1.0+git.1572337494.6f2c8ea9/test/testcases/confbasic.exp 2019-10-29 09:24:54.000000000 +0100 +++ new/crmsh-4.1.0+git.1572504697.472361c5/test/testcases/confbasic.exp 2019-10-31 07:51:37.000000000 +0100 @@ -147,3 +147,13 @@ record-pending=true .INP: commit WARNING: 55: c2: resource d1 is grouped, constraints should apply to the group +.TRY -F node maintenance node1 +.TRY -F resource maintenance g1 off +.TRY -F resource maintenance d1 +.TRY -F configure property maintenance-mode=true +INFO: 'maintenance' attribute already exists in d1. Remove it? [YES] +INFO: 'maintenance' attribute already exists in g1. Remove it? [YES] +INFO: 'maintenance' attribute already exists in node1. Remove it? [YES] +.EXT crmd metadata +.EXT pengine metadata +.EXT cib metadata diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.1.0+git.1572337494.6f2c8ea9/test/unittests/test_bootstrap.py new/crmsh-4.1.0+git.1572504697.472361c5/test/unittests/test_bootstrap.py --- old/crmsh-4.1.0+git.1572337494.6f2c8ea9/test/unittests/test_bootstrap.py 1970-01-01 01:00:00.000000000 +0100 +++ new/crmsh-4.1.0+git.1572504697.472361c5/test/unittests/test_bootstrap.py 2019-10-31 07:51:37.000000000 +0100 @@ -0,0 +1,146 @@ +""" +Unitary tests for crmsh/bootstrap.py + +:author: xinliang +:organization: SUSE Linux GmbH +:contact: xli...@suse.de + +:since: 2019-10-21 +""" + +# pylint:disable=C0103,C0111,W0212,W0611 + +import os +import unittest + +try: + from unittest import mock +except ImportError: + import mock + +from crmsh import bootstrap + + +class TestBootstrap(unittest.TestCase): + """ + Unitary tests for crmsh/bootstrap.py + """ + + @classmethod + def setUpClass(cls): + """ + Global setUp. + """ + + #@mock.patch('crmsh.bootstrap.Context') + def setUp(self): + """ + Test setUp. + """ + + def tearDown(self): + """ + Test tearDown. + """ + + @classmethod + def tearDownClass(cls): + """ + Global tearDown. + """ + + @mock.patch('crmsh.bootstrap.append') + @mock.patch('crmsh.bootstrap.status') + @mock.patch('os.path.exists') + @mock.patch('crmsh.bootstrap.start_service') + @mock.patch('crmsh.bootstrap.invoke') + def test_init_ssh_no_exist_keys(self, mock_invoke, mock_start_service, + mock_exists, mock_status, mock_append): + mock_exists.return_value = False + + bootstrap.init_ssh() + + mock_start_service.assert_called_once_with("sshd.service") + mock_invoke.assert_has_calls([ + mock.call("mkdir -m 700 -p /root/.ssh"), + mock.call("ssh-keygen -q -f /root/.ssh/id_rsa -C 'Cluster Internal' -N ''") + ]) + mock_exists.assert_called_once_with("/root/.ssh/id_rsa") + mock_status.assert_called_once_with("Generating SSH key") + mock_append.assert_called_once_with("/root/.ssh/id_rsa.pub", "/root/.ssh/authorized_keys") + + @mock.patch('crmsh.bootstrap.append') + @mock.patch('crmsh.bootstrap.status') + @mock.patch('crmsh.bootstrap.rmfile') + @mock.patch('crmsh.bootstrap.confirm') + @mock.patch('os.path.exists') + @mock.patch('crmsh.bootstrap.start_service') + @mock.patch('crmsh.bootstrap.invoke') + def test_init_ssh_exits_keys_yes_to_all_confirm(self, mock_invoke, mock_start_service, + mock_exists, mock_confirm, mock_rmfile, mock_status, mock_append): + mock_exists.return_value = True + bootstrap._context = mock.Mock(yes_to_all=True, no_overwrite_sshkey=False) + mock_confirm.return_value = True + + bootstrap.init_ssh() + + mock_start_service.assert_called_once_with("sshd.service") + mock_invoke.assert_has_calls([ + mock.call("mkdir -m 700 -p /root/.ssh"), + mock.call("ssh-keygen -q -f /root/.ssh/id_rsa -C 'Cluster Internal' -N ''") + ]) + mock_exists.assert_called_once_with("/root/.ssh/id_rsa") + mock_confirm.assert_called_once_with("/root/.ssh/id_rsa already exists - overwrite?") + mock_rmfile.assert_called_once_with("/root/.ssh/id_rsa") + mock_status.assert_called_once_with("Generating SSH key") + mock_append.assert_called_once_with("/root/.ssh/id_rsa.pub", "/root/.ssh/authorized_keys") + + @mock.patch('crmsh.bootstrap.rmfile') + @mock.patch('crmsh.bootstrap.confirm') + @mock.patch('os.path.exists') + @mock.patch('crmsh.bootstrap.start_service') + @mock.patch('crmsh.bootstrap.invoke') + def test_init_ssh_exits_keys_no_overwrite(self, mock_invoke, mock_start_service, + mock_exists, mock_confirm, mock_rmfile): + mock_exists.return_value = True + bootstrap._context = mock.Mock(yes_to_all=True, no_overwrite_sshkey=True) + + bootstrap.init_ssh() + + mock_start_service.assert_called_once_with("sshd.service") + mock_invoke.assert_called_once_with("mkdir -m 700 -p /root/.ssh") + mock_exists.assert_called_once_with("/root/.ssh/id_rsa") + mock_confirm.assert_not_called() + mock_rmfile.assert_not_called() + + @mock.patch('builtins.open') + @mock.patch('crmsh.bootstrap.append') + @mock.patch('os.path.join') + @mock.patch('os.path.exists') + def test_init_ssh_remote_no_sshkey(self, mock_exists, mock_join, mock_append, mock_open_file): + mock_exists.side_effect = [False, True, False, False, False] + mock_join.side_effect = ["/root/.ssh/id_rsa", + "/root/.ssh/id_dsa", + "/root/.ssh/id_ecdsa", + "/root/.ssh/id_ed25519"] + mock_open_file.side_effect = [ + mock.mock_open().return_value, + mock.mock_open(read_data="data1 data2").return_value, + mock.mock_open(read_data="data1111").return_value + ] + + bootstrap.init_ssh_remote() + + mock_open_file.assert_has_calls([ + mock.call("/root/.ssh/authorized_keys", 'w'), + mock.call("/root/.ssh/authorized_keys", "r+"), + mock.call("/root/.ssh/id_rsa.pub") + ]) + mock_exists.assert_has_calls([ + mock.call("/root/.ssh/authorized_keys"), + mock.call("/root/.ssh/id_rsa"), + mock.call("/root/.ssh/id_dsa"), + mock.call("/root/.ssh/id_ecdsa"), + mock.call("/root/.ssh/id_ed25519"), + ]) + mock_append.assert_called_once_with("/root/.ssh/id_rsa.pub", "/root/.ssh/authorized_keys")