示例#1
0
    def load_content(
        self,
        cib,
        returncode=0,
        stderr=None,
        name="runner.cib.load_content",
        instead=None,
        before=None,
    ):
        """
        Create call for loading CIB specified by its full content

        string cib -- CIB data (stdout of the loading process)
        string stderr -- error returned from the loading process
        int returncode -- exit code of the loading process
        string name -- key of the call
        string instead -- key of call instead of which this new call is to be
            placed
        string before -- key of call before which this new call is to be placed
        """
        command = ["cibadmin", "--local", "--query"]
        if returncode != 0:
            call = RunnerCall(command, stderr=stderr, returncode=returncode)
        else:
            call = RunnerCall(command, stdout=cib)
        self.__calls.place(name, call, before=before, instead=instead)
示例#2
0
    def load_agent(
        self,
        name="runner.pcmk.load_agent",
        agent_name="ocf:heartbeat:Dummy",
        agent_filename=None,
        agent_is_missing=False,
        instead=None,
    ):
        """
        Create call for loading resource agent metadata.

        string name -- key of the call
        string agent_name
        string agent_filename -- points to file with the agent metadata in the
            content
        string instead -- key of call instead of which this new call is to be
            placed
        """

        if agent_filename:
            agent_metadata_filename = agent_filename
        elif agent_name in AGENT_FILENAME_MAP:
            agent_metadata_filename = AGENT_FILENAME_MAP[agent_name]
        elif not agent_is_missing:
            raise AssertionError(
                ("Filename with metadata of agent '{0}' not specified.\n"
                 "Please specify file with metadata for agent:\n"
                 "  a) explicitly for this test:"
                 " config.runner.pcmk.load_agent(agent_name='{0}',"
                 " filename='FILENAME_HERE.xml')\n"
                 "  b) implicitly for agent '{0}' in 'AGENT_FILENAME_MAP' in"
                 " '{1}'\n"
                 "Place agent metadata into '{2}FILENAME_HERE.xml'").format(
                     agent_name, os.path.realpath(__file__), rc("")))

        if agent_is_missing:
            self.__calls.place(
                name,
                RunnerCall(
                    "crm_resource --show-metadata {0}".format(agent_name),
                    stdout="",
                    stderr=(
                        f"Agent {agent_name} not found or does not support "
                        "meta-data: Invalid argument (22)\n"
                        f"Metadata query for {agent_name} failed: Input/output "
                        "error\n"),
                    returncode=74),
                instead=instead,
            )
            return

        with open(rc(agent_metadata_filename)) as a_file:
            self.__calls.place(
                name,
                RunnerCall(
                    "crm_resource --show-metadata {0}".format(agent_name),
                    stdout=a_file.read()),
                instead=instead,
            )
示例#3
0
    def load(
        self,
        modifiers=None,
        name="runner.cib.load",
        filename=None,
        before=None,
        returncode=0,
        stderr=None,
        instead=None,
        env=None,
        **modifier_shortcuts,
    ):
        """
        Create call for loading cib.

        string name -- key of the call
        list of callable modifiers -- every callable takes etree.Element and
            returns new etree.Element with desired modification.
        string filename -- points to file with cib in the content
        string before -- key of call before which this new call is to be placed
        int returncode
        string stderr
        string instead -- key of call instead of which this new call is to be
            placed
        dict env -- CommandRunner environment variables
        dict modifier_shortcuts -- a new modifier is generated from each
            modifier shortcut.
            As key there can be keys of MODIFIER_GENERATORS.
            Value is passed into appropriate generator from MODIFIER_GENERATORS.
            For details see pcs_test.tools.fixture_cib (mainly the variable
            MODIFIER_GENERATORS - please refer it when you are adding params
            here)
        """
        # pylint: disable=too-many-arguments
        if (returncode != 0 or stderr is not None) and (
            modifiers is not None or filename is not None or modifier_shortcuts
        ):
            raise AssertionError(
                "Do not combine parameters 'returncode' and 'stderr' with"
                " parameters 'modifiers', 'filename' and 'modifier_shortcuts'"
            )

        command = ["cibadmin", "--local", "--query"]
        if returncode != 0:
            call = RunnerCall(
                command, stderr=stderr, returncode=returncode, env=env
            )
        else:
            with open(
                rc(filename if filename else self.cib_filename)
            ) as cib_file:
                cib = modify_cib(
                    cib_file.read(), modifiers, **modifier_shortcuts
                )
                call = RunnerCall(command, stdout=cib, env=env)

        self.__calls.place(name, call, before=before, instead=instead)
示例#4
0
    def load_state(
        self,
        name="runner.pcmk.load_state",
        filename="crm_mon.minimal.xml",
        resources=None,
        nodes=None,
        stdout="",
        stderr="",
        returncode=0,
        env=None,
    ):
        """
        Create call for loading pacemaker state.

        string name -- key of the call
        string filename -- points to file with the status in the content
        string resources -- xml - resources section, will be put to state
        string nodes -- xml - nodes section, will be put to state
        string stdout -- crm_mon's stdout
        string stderr -- crm_mon's stderr
        int returncode -- crm_mon's returncode
        dict env -- CommandRunner environment variables
        """
        if (resources or nodes) and (stdout or stderr or returncode):
            raise AssertionError(
                "Cannot specify resources or nodes when stdout, stderr or "
                "returncode is specified"
            )

        command = ["crm_mon", "--one-shot", "--inactive", "--output-as", "xml"]

        if stdout or stderr or returncode:
            self.__calls.place(
                name,
                RunnerCall(
                    command,
                    stdout=stdout,
                    stderr=stderr,
                    returncode=returncode,
                    env=env,
                ),
            )
            return

        with open(rc(filename)) as a_file:
            state_xml = a_file.read()

        self.__calls.place(
            name,
            RunnerCall(
                command,
                stdout=etree_to_str(
                    complete_state(state_xml, resources, nodes)
                ),
                env=env,
            ),
        )
示例#5
0
    def load_ticket_state_plaintext(
        self,
        name="runner.pcmk.load_ticket_state_plaintext",
        stdout="",
        stderr="",
        returncode=0,
        env=None,
    ):
        """
        Create a call for loading plaintext tickets status

        str name -- key of the call
        str stdout -- crm_ticket's stdout
        str stderr -- crm_ticket's stderr
        int returncode -- crm_ticket's returncode
        dict env -- CommandRunner environment variables
        """
        self.__calls.place(
            name,
            RunnerCall(
                ["crm_ticket", "--details"],
                stdout=stdout,
                stderr=stderr,
                returncode=returncode,
                env=env,
            ),
        )
示例#6
0
    def place(
        self,
        command,
        name="",
        stdout="",
        stderr="",
        returncode=0,
        check_stdin=None,
        before=None,
        instead=None,
        env=None,
    ):
        # pylint: disable=too-many-arguments
        """
        Place new call to a config.

        string command -- cmdline call (e.g. "crm_mon --one-shot --as-xml")
        string name -- name of the call; it is possible to get it by the method
            "get"
        string stdout -- stdout of the call
        string stderr -- stderr of the call
        int returncode -- returncode of the call
        callable check_stdin -- callable that can check if stdin is as expected
        string before -- name of another call to insert this call before it
        string instead -- name of another call to replace it by this call
        dict env -- CommandRunner environment variables
        """
        call = RunnerCall(command,
                          stdout,
                          stderr,
                          returncode,
                          check_stdin,
                          env=env)
        self.__calls.place(name, call, before, instead)
        return self
示例#7
0
 def _resource_move_ban_clear(self,
                              name,
                              action,
                              instead=None,
                              before=None,
                              resource=None,
                              node=None,
                              master=None,
                              lifetime=None,
                              expired=None,
                              stdout="",
                              stderr="",
                              returncode=0):
     cmd = ["crm_resource", action]
     if resource:
         cmd.extend(["--resource", resource])
     if node:
         cmd.extend(["--node", node])
     if master:
         cmd.extend(["--master"])
     if lifetime:
         cmd.extend(["--lifetime", lifetime])
     if expired:
         cmd.extend(["--expired"])
     self.__calls.place(
         name,
         RunnerCall(" ".join(cmd),
                    stdout=stdout,
                    stderr=stderr,
                    returncode=returncode),
         before=before,
         instead=instead,
     )
示例#8
0
    def fence_history_cleanup(
        self,
        name="runner.pcmk.fence_history_cleanup",
        node=None,
        stdout="",
        stderr="",
        returncode=0,
    ):
        """
        Create call for cleaning fencing history up.

        string name -- key of the call
        string node -- a node to clean a history from
        string stdout -- pacemaker's stdout
        string stderr -- pacemaker's stderr
        int returncode -- pacemaker's returncode
        """
        self.__calls.place(
            name,
            RunnerCall(
                "stonith_admin --history {0} --cleanup".format(node),
                stdout=stdout,
                stderr=stderr,
                returncode=returncode,
            ),
        )
示例#9
0
    def ticket_revoke(self,
                      ticket_name,
                      site_ip,
                      stdout="",
                      stderr="",
                      returncode=0,
                      name="runner.booth.ticket_revoke",
                      instead=None,
                      before=None):
        # pylint: disable=too-many-arguments
        """
        Create a call for revoking a ticket

        string ticket_name -- the name of the ticket to be revoked
        string site_ip -- an IP address of a site the ticket is being revoked to
        string stdout -- stdout of the booth revoke command
        string stderr -- stderr of the booth revoke command
        int returncode -- returncode of the booth revoke command
        string name -- the key of the call
        string before -- the key of a call before which this call is to be
            placed
        string instead -- the key of a call instead of which this new call is to
            be placed
        """
        self.__calls.place(
            name,
            RunnerCall(
                f"{settings.booth_binary} revoke -s {site_ip} {ticket_name}",
                stdout=stdout,
                stderr=stderr,
                returncode=returncode,
            ),
            before=before,
            instead=instead)
示例#10
0
    def push_independent(
        self, cib, name="runner.cib.push_independent", instead=None,
    ):
        """
        Create call for pushing cib.
        Cib is specified as an argument.

        string name -- key of the call
        string cib -- whole cib to push
        string instead -- key of call instead of which this new call is to be
            placed
        """
        self.__calls.place(
            name,
            RunnerCall(
                [
                    "cibadmin",
                    "--replace",
                    "--verbose",
                    "--xml-pipe",
                    "--scope",
                    "configuration",
                ],
                check_stdin=CheckStdinEqualXml(cib),
            ),
            instead=instead,
        )
示例#11
0
    def get_status(
        self,
        node,
        device,
        fence_agent,
        stdout="",
        stderr="",
        return_code=0,
        name="runner.scsi.is_fenced",
    ):
        """
        Create a call for getting scsi status

        string node -- a node from which is unfencing performed
        str device -- a device to check
        string stdout -- stdout from fence_scsi agent script
        string stderr -- stderr from fence_scsi agent script
        int return_code -- return code of the fence_scsi agent script
        string name -- the key of this call
        """
        self.__calls.place(
            name,
            RunnerCall(
                [
                    os.path.join(settings.fence_agent_binaries, fence_agent),
                    "--action=status",
                    f"--devices={device}",
                    f"--plug={node}",
                ],
                stdout=stdout,
                stderr=stderr,
                returncode=return_code,
            ),
        )
示例#12
0
 def qdevice_get_pk12(
     self,
     cert_path="cert path",
     output_path="output_path",
     stdout=None,
     stderr="",
     returncode=0,
     name="runner.corosync.qdevice_get_pk12",
 ):
     if stdout is not None and output_path is not None:
         raise AssertionError(
             "Cannot specify both 'output_path' and 'stdout'"
         )
     self.__calls.place(
         name,
         RunnerCall(
             ["corosync-qdevice-net-certutil", "-M", "-c", cert_path],
             stdout=(
                 stdout
                 if stdout is not None
                 else f"Certificate stored in {output_path}\n"
             ),
             stderr=stderr,
             returncode=returncode,
         ),
     )
示例#13
0
 def is_enabled(
     self,
     service,
     is_enabled=True,
     name="runner_systemctl.is_enabled",
     before=None,
     instead=None,
 ):
     args = dict(
         stdout="disabled\n",
         returncode=1,
     )
     if is_enabled:
         args = dict(
             stdout="enabled\n",
             returncode=0,
         )
     self.__calls.place(
         name,
         RunnerCall(
             [
                 settings.systemctl_binary, "is-enabled",
                 f"{service}.service"
             ],
             **args,
         ),
         before=before,
         instead=instead,
     )
示例#14
0
 def qdevice_generate_cert(
     self,
     cluster_name,
     cert_req_path="cert_path",
     stdout=None,
     stderr="",
     returncode=0,
     name="runner.corosync.qdevice_generate_cert",
 ):
     if stdout is not None and cert_req_path is not None:
         raise AssertionError(
             "Cannot specify both 'cert_req_path' and 'stdout'"
         )
     self.__calls.place(
         name,
         RunnerCall(
             ["corosync-qdevice-net-certutil", "-r", "-n", cluster_name],
             stdout=(
                 stdout
                 if stdout is not None
                 else f"Certificate request stored in {cert_req_path}\n"
             ),
             stderr=stderr,
             returncode=returncode,
         ),
     )
示例#15
0
    def initialize_devices(
        self,
        devices,
        options,
        stdout="",
        stderr="",
        return_code=0,
        name="runner.sbd.initialize_devices",
    ):
        cmd = [settings.sbd_binary]
        for device in devices:
            cmd += ["-d", device]

        for opt, val in sorted(options.items()):
            cmd += [DEVICE_INITIALIZATION_OPTIONS_MAPPING[opt], str(val)]

        cmd.append("create")
        self.__calls.place(
            name,
            RunnerCall(
                cmd,
                stdout=stdout,
                stderr=stderr,
                returncode=return_code,
            ),
        )
示例#16
0
    def status_daemon(self,
                      instance_name=None,
                      stdout="",
                      stderr="",
                      returncode=0,
                      name="runner.booth.status_daemon",
                      instead=None,
                      before=None):
        """
        Create a call for getting the booth daemon status

        string instance_name -- booth instance name
        string stdout -- stdout of the booth command
        string stderr -- stderr of the booth command
        int returncode -- returncode of the booth command
        string name -- the key of the call
        string before -- the key of a call before which this call is to be
            placed
        string instead -- the key of a call instead of which this new call is to
            be placed
        """
        cmd = f"{settings.booth_binary} status"
        if instance_name:
            cmd += f" -c {instance_name}"
        self.__calls.place(name,
                           RunnerCall(
                               cmd,
                               stdout=stdout,
                               stderr=stderr,
                               returncode=returncode,
                           ),
                           before=before,
                           instead=instead)
示例#17
0
    def verify(
        self,
        name="runner.pcmk.verify",
        cib_tempfile=None,
        stderr=None,
        verbose=False,
        env=None,
    ):
        """
        Create call that checks that wait for idle is supported

        string name -- key of the call
        string before -- key of call before which this new call is to be placed
        dict env -- CommandRunner environment variables
        """
        cmd = ["crm_verify"]
        if verbose:
            cmd.extend(["-V", "-V"])
        if cib_tempfile:
            cmd.extend(["--xml-file", cib_tempfile])
        else:
            cmd.append("--live-check")
        self.__calls.place(
            name,
            RunnerCall(
                cmd,
                stderr=("" if stderr is None else stderr),
                returncode=(0 if stderr is None else 55),
                env=env,
            ),
        )
示例#18
0
 def diff(
     self,
     cib_old_file,
     cib_new_file,
     name="runner.cib.diff",
     stdout="resulting diff",
     stderr="",
     returncode=1,  # 0 -> old and new are the same, 1 -> old and new differ
 ):
     """
     Create a call for diffing two CIBs stored in two files
     string cib_old_file -- path to a file with an old CIB
     string cib_new_file -- path to a file with a new CIB
     string name -- key of the call
     string stdout -- resulting diff
     string stderr -- error returned from the diff process
     int returncode -- exit code of the diff process
     """
     self.__calls.place(
         name,
         RunnerCall(
             [
                 "crm_diff",
                 "--original",
                 cib_old_file,
                 "--new",
                 cib_new_file,
                 "--no-version",
             ],
             stdout=stdout,
             stderr=stderr,
             returncode=returncode,
         ),
     )
示例#19
0
    def get_rule_in_effect_status(
        self,
        rule_id,
        returncode=0,
        name="runner.pcmk.get_rule_in_effect_status",
        cib_load_name="runner.cib.load",
    ):
        """
        Create a call for running a tool to get rule expired status

        string rule_id -- id of the rule to be checked
        int returncode -- result of the check
        sting name -- key of the call
        string cib_load_name -- key of a call from whose stdout the cib is taken
        """
        cib_xml = self.__calls.get(cib_load_name).stdout
        self.__calls.place(
            name,
            RunnerCall(
                ["crm_rule", "--check", "--rule", rule_id, "--xml-text", "-"],
                check_stdin=CheckStdinEqualXml(cib_xml),
                stdout="",
                stderr="",
                returncode=returncode,
            ),
        )
示例#20
0
    def wait(self,
             name="runner.pcmk.wait",
             stderr="",
             returncode=None,
             timeout=None):
        """
        Create call for waiting to pacemaker idle

        string name -- key of the call
        string stderr -- stderr of wait command
        int returncode -- returncode of the wait command, defaults to 0 if
            stderr is empty and to 124 if stderr is not empty
        """
        if returncode is None:
            returncode = self.default_wait_error_returncode if stderr else 0

        self.__calls.place(
            name,
            RunnerCall(
                "crm_resource --wait --timeout={0}".format(
                    timeout if timeout else self.default_wait_timeout),
                stderr=stderr,
                returncode=returncode,
            ),
        )
示例#21
0
    def fence_history_get(
        self,
        name="runner.pcmk.fence_history_get",
        node=None,
        stdout="",
        stderr="",
        returncode=0,
    ):
        """
        Create call for getting plain text fencing history.

        string name -- key of the call
        string node -- a node to get a history from
        string stdout -- pacemaker's stdout
        string stderr -- pacemaker's stderr
        int returncode -- pacemaker's returncode
        """
        self.__calls.place(
            name,
            RunnerCall(
                "stonith_admin --history {0} --verbose".format(node),
                stdout=stdout,
                stderr=stderr,
                returncode=returncode,
            ),
        )
示例#22
0
    def load_fenced_metadata(
        self,
        name="runner.pcmk.load_fenced_metadata",
        stdout=None,
        stderr="",
        returncode=0,
        instead=None,
        before=None,
    ):
        """
        Create a call for loading fenced metadata - additional fence options

        string name -- the key of this call
        string stdout -- fenced stdout, default metadata if None
        string stderr -- fenced stderr
        int returncode -- fenced returncode
        string instead -- the key of a call instead of which this new call is to
            be placed
        string before -- the key of a call before which this new call is to be
            placed
        """
        self.__calls.place(
            name,
            RunnerCall("/usr/libexec/pacemaker/pacemaker-fenced metadata",
                       stdout=(stdout if stdout is not None else open(
                           rc("fenced_metadata.xml")).read()),
                       stderr=stderr,
                       returncode=returncode),
            before=before,
            instead=instead,
        )
示例#23
0
    def load_fenced_metadata(
        self,
        name="runner.pcmk.load_fenced_metadata",
        stdout=None,
        stderr="",
        returncode=0,
        instead=None,
        before=None,
    ):
        """
        Create a call for loading fenced metadata - additional fence options

        string name -- the key of this call
        string stdout -- fenced stdout, default metadata if None
        string stderr -- fenced stderr
        int returncode -- fenced returncode
        string instead -- the key of a call instead of which this new call is to
            be placed
        string before -- the key of a call before which this new call is to be
            placed
        """
        if stdout is None:
            with open(rc("fenced_metadata.xml")) as a_file:
                stdout = a_file.read()
        self.__calls.place(
            name,
            RunnerCall(
                f"{settings.pacemaker_fenced} metadata",
                stdout=stdout,
                stderr=stderr,
                returncode=returncode,
            ),
            before=before,
            instead=instead,
        )
示例#24
0
 def push_diff(
     self,
     name="runner.cib.push_diff",
     cib_diff="resulting diff",
     stdout="",
     stderr="",
     returncode=0,
     env=None,
 ):
     """
     Create a call for pushing a diff of CIBs
     string name -- key of the call
     string cib_diff -- the diff of CIBs
     dict env -- CommandRunner environment variables
     """
     self.__calls.place(
         name,
         RunnerCall(
             ["cibadmin", "--patch", "--verbose", "--xml-pipe"],
             check_stdin=CheckStdinEqualXml(cib_diff),
             stdout=stdout,
             stderr=stderr,
             returncode=returncode,
             env=env,
         ),
     )
示例#25
0
    def verify(
        self,
        name="runner.pcmk.verify",
        cib_tempfile=None,
        stderr=None,
        verbose=False,
    ):
        """
        Create call that checks that wait for idle is supported

        string name -- key of the call
        string before -- key of call before which this new call is to be placed
        """
        self.__calls.place(
            name,
            RunnerCall(
                "crm_verify{0} {1}".format(
                    " -V -V" if verbose else "",
                    "--xml-file {0}".format(cib_tempfile)
                    if cib_tempfile else "--live-check",
                ),
                stderr=("" if stderr is None else stderr),
                returncode=(0 if stderr is None else 55),
            ),
        )
 def is_enabled(
     self, service, is_enabled=True,
     name="runner_systemctl.is_enabled", before=None, instead=None
 ):
     args = dict(
         stdout="disabled\n",
         returncode=1,
     )
     if is_enabled:
         args = dict(
             stdout="enabled\n",
             returncode=0,
         )
     self.__calls.place(
         name,
         RunnerCall(
             "{bin_path} is-enabled {service}.service".format(
                 bin_path=settings.systemctl_binary,
                 service=service,
             ),
             **args
         ),
         before=before,
         instead=instead
     )
示例#27
0
    def local_node_name(self,
                        name="runner.pcmk.local_node_name",
                        instead=None,
                        before=None,
                        node_name="",
                        stdout="",
                        stderr="",
                        returncode=0):
        """
        Create a call for crm_node --name

        string name -- the key of this call
        string instead -- the key of a call instead of which this new call is to
            be placed
        string before -- the key of a call before which this new call is to be
            placed
        string node_name -- resulting node name
        string stdout -- crm_node's stdout
        string stderr -- crm_node's stderr
        int returncode -- crm_node's returncode
        """
        if node_name and (stdout or stderr or returncode):
            raise AssertionError(
                "Cannot specify node_name when stdout, stderr or returncode is "
                "specified")
        cmd = ["crm_node", "--name"]
        self.__calls.place(
            name,
            RunnerCall(" ".join(cmd),
                       stdout=(node_name if node_name else stdout),
                       stderr=stderr,
                       returncode=returncode),
            before=before,
            instead=instead,
        )
示例#28
0
 def qdevice_generate_cert(
     self,
     cluster_name,
     cert_req_path="cert_path",
     stdout=None,
     stderr="",
     returncode=0,
     name="runner.corosync.qdevice_generate_cert",
 ):
     if stdout is not None and cert_req_path is not None:
         raise AssertionError(
             "Cannot specify both 'cert_req_path' and 'stdout'")
     self.__calls.place(
         name,
         RunnerCall(
             "{binary} -r -n {cluster_name}".format(
                 binary=os.path.join(
                     settings.corosync_binaries,
                     "corosync-qdevice-net-certutil",
                 ),
                 cluster_name=cluster_name,
             ),
             stdout=(stdout if stdout is not None else
                     f"Certificate request stored in {cert_req_path}\n"),
             stderr=stderr,
             returncode=returncode,
         ),
     )
示例#29
0
    def unfence_node(
        self,
        node,
        devices,
        stdout="",
        stderr="",
        return_code=0,
        name="runner.scsi.unfence_node",
    ):
        """
        Create a calls for node scsi unfencing

        string node -- a node from which is unfencing performed
        list devices -- list of devices to unfence
        string stdout -- stdout from fence_scsi agent script
        string stderr -- stderr from fence_scsi agent script
        int return_code -- return code of the fence_scsi agent script
        string name -- the key of this call
        """
        self.__calls.place(
            name,
            RunnerCall(
                [
                    os.path.join(settings.fence_agent_binaries, "fence_scsi"),
                    "--action=on",
                    "--devices",
                    ",".join(devices),
                    f"--plug={node}",
                ],
                stdout=stdout,
                stderr=stderr,
                returncode=return_code,
            ),
        )
示例#30
0
 def qdevice_get_pk12(
     self,
     cert_path="cert path",
     output_path="output_path",
     stdout=None,
     stderr="",
     returncode=0,
     name="runner.corosync.qdevice_get_pk12",
 ):
     if stdout is not None and output_path is not None:
         raise AssertionError(
             "Cannot specify both 'output_path' and 'stdout'")
     self.__calls.place(
         name,
         RunnerCall(
             "{binary} -M -c {cert_path}".format(
                 binary=os.path.join(
                     settings.corosync_binaries,
                     "corosync-qdevice-net-certutil",
                 ),
                 cert_path=cert_path,
             ),
             stdout=(stdout if stdout is not None else
                     f"Certificate stored in {output_path}\n"),
             stderr=stderr,
             returncode=returncode,
         ),
     )