Repository: ansible-collections/community.general

Issue:
java_keystore: created file is in PKCS#12 format rather than expected JKS
### Summary

Depending on the version of Java, the `keytool` command that creates the java keystore from a PKCS#12 bundle (certificate + private key) may or may not need explicit `-deststoretype jks`. Without these arguments, the command may create a `PKCS#12` file rather than the expected `JKS`. Also note that most Java application servers accept PKCS#12 keystores, that makes easy to never notice that the file is not a JKS.

### Issue Type

Bug Report

### Component Name

java_keystore

### Ansible Version

```console (paste below)
$ ansible --version
ansible 2.10.9
  config file = /home/quidame/github.com/ansible.cfg
  configured module search path = ['/home/quidame/github.com/library']
  ansible python module location = /home/quidame/.venvs/py3/lib/python3.7/site-packages/ansible
  executable location = /home/quidame/.venvs/py3/bin/ansible
  python version = 3.7.3 (default, Apr  3 2019, 05:39:12) [GCC 8.3.0]
```


### Configuration

```console (paste below)
$ ansible-config dump --only-changed
ANSIBLE_FORCE_COLOR(/home/quidame/github.com/ansible.cfg) = True
ANSIBLE_PIPELINING(/home/quidame/github.com/ansible.cfg) = True
ANSIBLE_SSH_ARGS(/home/quidame/github.com/ansible.cfg) = -o ControlMaster=auto -o ControlPersist=60s -o ProxyCommand=none -o UserKnownHostsFile=ssh_known_hosts -q
DEFAULT_ACTION_PLUGIN_PATH(/home/quidame/github.com/ansible.cfg) = ['/home/quidame/github.com/action_plugins']
DEFAULT_CALLBACK_WHITELIST(/home/quidame/github.com/ansible.cfg) = ['profile_roles']
DEFAULT_FILTER_PLUGIN_PATH(/home/quidame/github.com/ansible.cfg) = ['/home/quidame/github.com/filter_plugins']
DEFAULT_FORKS(/home/quidame/github.com/ansible.cfg) = 10
DEFAULT_HOST_LIST(/home/quidame/github.com/ansible.cfg) = ['/home/quidame/github.com/hosts']
DEFAULT_MODULE_PATH(/home/quidame/github.com/ansible.cfg) = ['/home/quidame/github.com/library']
DEFAULT_ROLES_PATH(/home/quidame/github.com/ansible.cfg) = ['/home/quidame/github.com/roles']
DEFAULT_VAULT_PASSWORD_FILE(/home/quidame/github.com/ansible.cfg) = /home/quidame/github.com/vault_password
HOST_KEY_CHECKING(/home/quidame/github.com/ansible.cfg) = True
INTERPRETER_PYTHON(/home/quidame/github.com/ansible.cfg) = auto_legacy_silent
```


### OS / Environment

Debian 10/11, CentOS 7/8 (or any OS running OpenJDK 11)

### Steps to Reproduce

- Install OpenJDK 11 on the targets (if possible, otherwise the latest available version)
- Create a private key and x509 certificate and copy them on the targets
- Run this playbook (in my case test1 is Debian 11, test4 is CentOS 7, test5 is CentOS 8, test6 is FreeBSD 12) :

```yaml
---
- name: TEST MODULE java_keystore
  hosts: test1:test4:test5:test6
  gather_facts: no
  vars:
    certificate: /tmp/ssl-cert-snakeoil.pem
    private_key: /tmp/ssl-cert-snakeoil.key
    keystore: /tmp/ssl-cert-snakeoil.jks
    password: /f00b4R-Baz!
    alias: snakeoil
  tasks:
    - name: Remove JKS
      file:
        path: "{{ keystore }}"
        state: absent

    - name: Create JKS
      java_keystore:
        dest: "{{ keystore }}"
        name: "{{ alias }}"
        certificate_path: "{{ certificate }}"
        private_key_path: "{{ private_key }}"
        password: "{{ password }}"

    - name: Get results
      command:
        cmd: "keytool -list -keystore {{ keystore }}"
        stdin: "{{ password }}"
      register: keystore_info
      environment:
        LC_MESSAGES: C

    - name: Show keystore format
      debug:
        var: keystore_info.stdout_lines[0]
```

### Expected Results

Result of last task should be `Keystore type: jks` or `Keystore type: JKS` everywhere, like:

```
TASK [Show keystore format] ************************************
ok: [test1] => {
    "keystore_info.stdout_lines[0]": "Keystore type: JKS"
}
ok: [test4] => {
    "keystore_info.stdout_lines[0]": "Keystore type: jks"
}
ok: [test5] => {
    "keystore_info.stdout_lines[0]": "Keystore type: jks"
}
ok: [test6] => {
    "keystore_info.stdout_lines[0]": "Keystore type: JKS"
}

```

### Actual Results

```console (paste below)
TASK [Show keystore format] ******************************************
ok: [test1] => {
    "keystore_info.stdout_lines[0]": "Keystore type: PKCS12"
}
ok: [test4] => {
    "keystore_info.stdout_lines[0]": "Keystore type: PKCS12"
}
ok: [test5] => {
    "keystore_info.stdout_lines[0]": "Keystore type: PKCS12"
}
ok: [test6] => {
    "keystore_info.stdout_lines[0]": "Keystore type: JKS"
}
```

The add of the explicit option `-deststoretype JKS` to create the keystore solves the issue.


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
* [`plugins/modules/system/java_keystore.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/system/java_keystore.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @Mogztter
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
@quidame do you want to create a PR to fix this?

Resolving patch:
diff --git a/plugins/modules/system/java_keystore.py b/plugins/modules/system/java_keystore.py
--- a/plugins/modules/system/java_keystore.py
+++ b/plugins/modules/system/java_keystore.py
@@ -1,8 +1,8 @@
 #!/usr/bin/python
 # -*- coding: utf-8 -*-
 
-# Copyright: (c) 2016, Guillaume Grossetie <ggrossetie@yuzutech.fr>
 # Copyright: (c) 2021, quidame <quidame@poivron.org>
+# Copyright: (c) 2016, Guillaume Grossetie <ggrossetie@yuzutech.fr>
 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
 
 from __future__ import (absolute_import, division, print_function)
@@ -97,6 +97,24 @@
       - openssl
       - cryptography
     version_added: 3.1.0
+  keystore_type:
+    description:
+      - Type of the Java keystore.
+      - When this option is omitted and the keystore doesn't already exist, the
+        behavior follows C(keytool)'s default store type which depends on
+        Java version; C(pkcs12) since Java 9 and C(jks) prior (may also
+        be C(pkcs12) if new default has been backported to this version).
+      - When this option is omitted and the keystore already exists, the current
+        type is left untouched, unless another option leads to overwrite the
+        keystore (in that case, this option behaves like for keystore creation).
+      - When I(keystore_type) is set, the keystore is created with this type if
+        it doesn't already exist, or is overwritten to match the given type in
+        case of mismatch.
+    type: str
+    choices:
+      - jks
+      - pkcs12
+    version_added: 3.3.0
 requirements:
   - openssl in PATH (when I(ssl_backend=openssl))
   - keytool in PATH
@@ -107,12 +125,16 @@
 extends_documentation_fragment:
   - files
 seealso:
+  - module: community.crypto.openssl_pkcs12
   - module: community.general.java_cert
 notes:
   - I(certificate) and I(private_key) require that their contents are available
     on the controller (either inline in a playbook, or with the C(file) lookup),
     while I(certificate_path) and I(private_key_path) require that the files are
     available on the target host.
+  - By design, any change of a value of options I(keystore_type), I(name) or
+    I(password), as well as changes of key or certificate materials will cause
+    the existing I(dest) to be overwritten.
 '''
 
 EXAMPLES = '''
@@ -156,6 +178,12 @@
   type: str
   sample: "Unable to find the current certificate fingerprint in ..."
 
+err:
+  description: Output from stderr of keytool/openssl command after error of given command.
+  returned: failure
+  type: str
+  sample: "Keystore password is too short - must be at least 6 characters\n"
+
 rc:
   description: keytool/openssl command execution return value
   returned: changed and failure
@@ -176,7 +204,7 @@
 
 from ansible.module_utils.six import PY2
 from ansible.module_utils.basic import AnsibleModule, missing_required_lib
-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.common.text.converters import to_bytes, to_native
 
 try:
     from cryptography.hazmat.primitives.serialization.pkcs12 import serialize_key_and_certificates
@@ -201,6 +229,7 @@
 class JavaKeystore:
     def __init__(self, module):
         self.module = module
+        self.result = dict()
 
         self.keytool_bin = module.get_bin_path('keytool', True)
 
@@ -211,6 +240,7 @@ def __init__(self, module):
         self.password = module.params['password']
         self.private_key = module.params['private_key']
         self.ssl_backend = module.params['ssl_backend']
+        self.keystore_type = module.params['keystore_type']
 
         if self.ssl_backend == 'openssl':
             self.openssl_bin = module.get_bin_path('openssl', True)
@@ -312,6 +342,9 @@ def read_stored_certificate_fingerprint(self):
                 rc=rc
             )
 
+        if self.keystore_type not in (None, self.current_type()):
+            return "keystore type mismatch"
+
         stored_certificate_match = re.search(r"SHA256: ([\w:]+)", stored_certificate_fingerprint_out)
         if not stored_certificate_match:
             return self.module.fail_json(
@@ -322,6 +355,14 @@ def read_stored_certificate_fingerprint(self):
 
         return stored_certificate_match.group(1)
 
+    def current_type(self):
+        magic_bytes = b'\xfe\xed\xfe\xed'
+        with open(self.keystore_path, 'rb') as fd:
+            header = fd.read(4)
+        if header == magic_bytes:
+            return 'jks'
+        return 'pkcs12'
+
     def cert_changed(self):
         current_certificate_fingerprint = self.read_certificate_fingerprint()
         stored_certificate_fingerprint = self.read_stored_certificate_fingerprint()
@@ -389,6 +430,8 @@ def cryptography_create_pkcs12_bundle(self, keystore_p12_path, key_format='PEM',
         with open(keystore_p12_path, 'wb') as p12_file:
             p12_file.write(pkcs12_bundle)
 
+        self.result.update(msg="PKCS#12 bundle created by cryptography backend")
+
     def openssl_create_pkcs12_bundle(self, keystore_p12_path):
         export_p12_cmd = [self.openssl_bin, "pkcs12", "-export", "-name", self.name, "-in", self.certificate_path,
                           "-inkey", self.private_key_path, "-out", keystore_p12_path, "-passout", "stdin"]
@@ -401,19 +444,22 @@ def openssl_create_pkcs12_bundle(self, keystore_p12_path):
             cmd_stdin = "%s\n" % self.keypass
         cmd_stdin += "%s\n%s" % (self.password, self.password)
 
-        (rc, export_p12_out, dummy) = self.module.run_command(
+        (rc, export_p12_out, export_p12_err) = self.module.run_command(
             export_p12_cmd, data=cmd_stdin, environ_update=None, check_rc=False
         )
 
+        self.result = dict(msg=export_p12_out, cmd=export_p12_cmd, rc=rc)
         if rc != 0:
-            self.module.fail_json(msg=export_p12_out, cmd=export_p12_cmd, rc=rc)
+            self.result['err'] = export_p12_err
+            self.module.fail_json(**self.result)
 
     def create(self):
+        """Create the keystore, or replace it with a rollback in case of
+           keytool failure.
+        """
         if self.module.check_mode:
-            return {'changed': True}
-
-        if os.path.exists(self.keystore_path):
-            os.remove(self.keystore_path)
+            self.result['changed'] = True
+            return self.result
 
         keystore_p12_path = create_path()
         self.module.add_cleanup_file(keystore_p12_path)
@@ -423,6 +469,13 @@ def create(self):
         else:
             self.openssl_create_pkcs12_bundle(keystore_p12_path)
 
+        if self.keystore_type == 'pkcs12':
+            # Preserve properties of the destination file, if any.
+            self.module.atomic_move(keystore_p12_path, self.keystore_path)
+            self.update_permissions()
+            self.result['changed'] = True
+            return self.result
+
         import_keystore_cmd = [self.keytool_bin, "-importkeystore",
                                "-destkeystore", self.keystore_path,
                                "-srckeystore", keystore_p12_path,
@@ -430,19 +483,38 @@ def create(self):
                                "-alias", self.name,
                                "-noprompt"]
 
-        (rc, import_keystore_out, dummy) = self.module.run_command(
+        if self.keystore_type == 'jks':
+            keytool_help = self.module.run_command([self.keytool_bin, '-importkeystore', '-help'])
+            if '-deststoretype' in keytool_help[1] + keytool_help[2]:
+                import_keystore_cmd.insert(4, "-deststoretype")
+                import_keystore_cmd.insert(5, self.keystore_type)
+
+        keystore_backup = None
+        if self.exists():
+            keystore_backup = self.keystore_path + '.tmpbak'
+            # Preserve properties of the source file
+            self.module.preserved_copy(self.keystore_path, keystore_backup)
+            os.remove(self.keystore_path)
+
+        (rc, import_keystore_out, import_keystore_err) = self.module.run_command(
             import_keystore_cmd, data='%s\n%s\n%s' % (self.password, self.password, self.password), check_rc=False
         )
-        if rc != 0:
-            return self.module.fail_json(msg=import_keystore_out, cmd=import_keystore_cmd, rc=rc)
+
+        self.result = dict(msg=import_keystore_out, cmd=import_keystore_cmd, rc=rc)
+
+        # keytool may return 0 whereas the keystore has not been created.
+        if rc != 0 or not self.exists():
+            if keystore_backup is not None:
+                self.module.preserved_copy(keystore_backup, self.keystore_path)
+                os.remove(keystore_backup)
+            self.result['err'] = import_keystore_err
+            return self.module.fail_json(**self.result)
 
         self.update_permissions()
-        return {
-            'changed': True,
-            'msg': import_keystore_out,
-            'cmd': import_keystore_cmd,
-            'rc': rc
-        }
+        if keystore_backup is not None:
+            os.remove(keystore_backup)
+        self.result['changed'] = True
+        return self.result
 
     def exists(self):
         return os.path.exists(self.keystore_path)
@@ -465,15 +537,15 @@ def create_file(content):
 def hex_decode(s):
     if PY2:
         return s.decode('hex')
-    else:
-        return s.hex()
+    return s.hex()
+
 
+def main():
+    choose_between = (['certificate', 'certificate_path'],
+                      ['private_key', 'private_key_path'])
 
-class ArgumentSpec(object):
-    def __init__(self):
-        self.supports_check_mode = True
-        self.add_file_common_args = True
-        argument_spec = dict(
+    module = AnsibleModule(
+        argument_spec=dict(
             name=dict(type='str', required=True),
             dest=dict(type='path', required=True),
             certificate=dict(type='str', no_log=True),
@@ -483,25 +555,13 @@ def __init__(self):
             private_key_passphrase=dict(type='str', no_log=True),
             password=dict(type='str', required=True, no_log=True),
             ssl_backend=dict(type='str', default='openssl', choices=['openssl', 'cryptography']),
+            keystore_type=dict(type='str', choices=['jks', 'pkcs12']),
             force=dict(type='bool', default=False),
-        )
-        choose_between = (
-            ['certificate', 'certificate_path'],
-            ['private_key', 'private_key_path'],
-        )
-        self.argument_spec = argument_spec
-        self.required_one_of = choose_between
-        self.mutually_exclusive = choose_between
-
-
-def main():
-    spec = ArgumentSpec()
-    module = AnsibleModule(
-        argument_spec=spec.argument_spec,
-        required_one_of=spec.required_one_of,
-        mutually_exclusive=spec.mutually_exclusive,
-        supports_check_mode=spec.supports_check_mode,
-        add_file_common_args=spec.add_file_common_args,
+        ),
+        required_one_of=choose_between,
+        mutually_exclusive=choose_between,
+        supports_check_mode=True,
+        add_file_common_args=True,
     )
     module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C')
 


Test patch:
diff --git a/tests/integration/targets/java_keystore/tasks/tests.yml b/tests/integration/targets/java_keystore/tasks/tests.yml
--- a/tests/integration/targets/java_keystore/tasks/tests.yml
+++ b/tests/integration/targets/java_keystore/tasks/tests.yml
@@ -24,6 +24,7 @@
     private_key_passphrase: "{{ item.passphrase | d(omit) }}"
     password: changeit
     ssl_backend: "{{ ssl_backend }}"
+    keystore_type: "{{ item.keystore_type | d(omit) }}"
   loop: "{{ java_keystore_certs }}"
   check_mode: yes
   register: result_check
@@ -91,6 +92,98 @@
   loop: "{{ java_keystore_new_certs }}"
   register: result_pw_change
 
+
+- name: Create a Java keystore for the given certificates (force keystore type pkcs12, check mode)
+  community.general.java_keystore:
+    <<: *java_keystore_params
+    name: foobar
+    password: hunter2
+    keystore_type: pkcs12
+  loop: "{{ java_keystore_new_certs }}"
+  check_mode: yes
+  register: result_type_pkcs12_check
+
+- name: Create a Java keystore for the given certificates (force keystore type jks, check mode)
+  community.general.java_keystore:
+    <<: *java_keystore_params
+    name: foobar
+    password: hunter2
+    keystore_type: jks
+  loop: "{{ java_keystore_new_certs }}"
+  check_mode: yes
+  register: result_type_jks_check
+
+- name: Create a Java keystore for the given certificates (force keystore type jks)
+  community.general.java_keystore:
+    <<: *java_keystore_params
+    name: foobar
+    password: hunter2
+    keystore_type: jks
+  loop: "{{ java_keystore_new_certs }}"
+  register: result_type_jks
+
+
+- name: Stat keystore (before failure)
+  ansible.builtin.stat:
+    path: "{{ output_dir ~ '/' ~ (item.keyname | d(item.name)) ~ '.jks' }}"
+  loop: "{{ java_keystore_new_certs }}"
+  register: result_stat_before
+
+- name: Fail to create a Java keystore for the given certificates (password too short)
+  community.general.java_keystore:
+    <<: *java_keystore_params
+    name: foobar
+    password: short
+    keystore_type: jks
+  loop: "{{ java_keystore_new_certs }}"
+  register: result_fail_jks
+  ignore_errors: true
+
+- name: Stat keystore (after failure)
+  ansible.builtin.stat:
+    path: "{{ output_dir ~ '/' ~ (item.keyname | d(item.name)) ~ '.jks' }}"
+  loop: "{{ java_keystore_new_certs }}"
+  register: result_stat_after
+
+
+- name: Create a Java keystore for the given certificates (keystore type changed, check mode)
+  community.general.java_keystore:
+    <<: *java_keystore_params
+    name: foobar
+    password: hunter2
+    keystore_type: pkcs12
+  loop: "{{ java_keystore_new_certs }}"
+  check_mode: yes
+  register: result_type_change_check
+
+- name: Create a Java keystore for the given certificates (keystore type changed)
+  community.general.java_keystore:
+    <<: *java_keystore_params
+    name: foobar
+    password: hunter2
+    keystore_type: pkcs12
+  loop: "{{ java_keystore_new_certs }}"
+  register: result_type_change
+
+
+- name: Create a Java keystore for the given certificates (omit keystore type, check mode)
+  community.general.java_keystore:
+    <<: *java_keystore_params
+    name: foobar
+    password: hunter2
+  loop: "{{ java_keystore_new_certs }}"
+  check_mode: yes
+  register: result_type_omit_check
+
+- name: Create a Java keystore for the given certificates (omit keystore type)
+  community.general.java_keystore:
+    <<: *java_keystore_params
+    name: foobar
+    password: hunter2
+  loop: "{{ java_keystore_new_certs }}"
+  register: result_type_omit
+
+
 - name: Check that the remote certificates have not been removed
   ansible.builtin.file:
     path: "{{ output_dir ~ '/' ~ item.name ~ '.pem' }}"
@@ -118,3 +211,25 @@
       - result_alias_change_check is changed
       - result_pw_change is changed
       - result_pw_change_check is changed
+
+      # We don't know if we start from jks or pkcs12 format, anyway check mode
+      # and actual mode must return the same 'changed' state, and 'jks' and
+      # 'pkcs12' must give opposite results on a same host.
+      - result_type_jks_check.changed != result_type_pkcs12_check.changed
+      - result_type_jks_check.changed == result_type_jks.changed
+
+      - result_type_change is changed
+      - result_type_change_check is changed
+      - result_type_omit is not changed
+      - result_type_omit_check is not changed
+
+      # keystore properties must remain the same after failure
+      - result_fail_jks is failed
+      - result_stat_before.results[0].stat.uid == result_stat_after.results[0].stat.uid
+      - result_stat_before.results[1].stat.uid == result_stat_after.results[1].stat.uid
+      - result_stat_before.results[0].stat.gid == result_stat_after.results[0].stat.gid
+      - result_stat_before.results[1].stat.gid == result_stat_after.results[1].stat.gid
+      - result_stat_before.results[0].stat.mode == result_stat_after.results[0].stat.mode
+      - result_stat_before.results[1].stat.mode == result_stat_after.results[1].stat.mode
+      - result_stat_before.results[0].stat.checksum == result_stat_after.results[0].stat.checksum
+      - result_stat_before.results[1].stat.checksum == result_stat_after.results[1].stat.checksum
diff --git a/tests/unit/plugins/modules/system/test_java_keystore.py b/tests/unit/plugins/modules/system/test_java_keystore.py
--- a/tests/unit/plugins/modules/system/test_java_keystore.py
+++ b/tests/unit/plugins/modules/system/test_java_keystore.py
@@ -14,7 +14,25 @@
 from ansible_collections.community.general.tests.unit.compat.mock import patch
 from ansible_collections.community.general.tests.unit.compat.mock import Mock
 from ansible.module_utils.basic import AnsibleModule
-from ansible_collections.community.general.plugins.modules.system.java_keystore import JavaKeystore, ArgumentSpec
+from ansible_collections.community.general.plugins.modules.system.java_keystore import JavaKeystore
+
+
+module_argument_spec = dict(
+    name=dict(type='str', required=True),
+    dest=dict(type='path', required=True),
+    certificate=dict(type='str', no_log=True),
+    certificate_path=dict(type='path'),
+    private_key=dict(type='str', no_log=True),
+    private_key_path=dict(type='path', no_log=False),
+    private_key_passphrase=dict(type='str', no_log=True),
+    password=dict(type='str', required=True, no_log=True),
+    ssl_backend=dict(type='str', default='openssl', choices=['openssl', 'cryptography']),
+    keystore_type=dict(type='str', choices=['jks', 'pkcs12']),
+    force=dict(type='bool', default=False),
+)
+module_supports_check_mode = True
+module_choose_between = (['certificate', 'certificate_path'],
+                         ['private_key', 'private_key_path'])
 
 
 class TestCreateJavaKeystore(ModuleTestCase):
@@ -25,11 +43,13 @@ def setUp(self):
         super(TestCreateJavaKeystore, self).setUp()
 
         orig_exists = os.path.exists
-        self.spec = ArgumentSpec()
         self.mock_create_file = patch('ansible_collections.community.general.plugins.modules.system.java_keystore.create_file')
         self.mock_create_path = patch('ansible_collections.community.general.plugins.modules.system.java_keystore.create_path')
+        self.mock_current_type = patch('ansible_collections.community.general.plugins.modules.system.java_keystore.JavaKeystore.current_type')
         self.mock_run_command = patch('ansible.module_utils.basic.AnsibleModule.run_command')
         self.mock_get_bin_path = patch('ansible.module_utils.basic.AnsibleModule.get_bin_path')
+        self.mock_preserved_copy = patch('ansible.module_utils.basic.AnsibleModule.preserved_copy')
+        self.mock_atomic_move = patch('ansible.module_utils.basic.AnsibleModule.atomic_move')
         self.mock_os_path_exists = patch('os.path.exists',
                                          side_effect=lambda path: True if path == '/path/to/keystore.jks' else orig_exists(path))
         self.mock_selinux_context = patch('ansible.module_utils.basic.AnsibleModule.selinux_context',
@@ -38,8 +58,11 @@ def setUp(self):
                                                   side_effect=lambda path: (False, None))
         self.run_command = self.mock_run_command.start()
         self.get_bin_path = self.mock_get_bin_path.start()
+        self.preserved_copy = self.mock_preserved_copy.start()
+        self.atomic_move = self.mock_atomic_move.start()
         self.create_file = self.mock_create_file.start()
         self.create_path = self.mock_create_path.start()
+        self.current_type = self.mock_current_type.start()
         self.selinux_context = self.mock_selinux_context.start()
         self.is_special_selinux_path = self.mock_is_special_selinux_path.start()
         self.os_path_exists = self.mock_os_path_exists.start()
@@ -49,8 +72,11 @@ def tearDown(self):
         super(TestCreateJavaKeystore, self).tearDown()
         self.mock_create_file.stop()
         self.mock_create_path.stop()
+        self.mock_current_type.stop()
         self.mock_run_command.stop()
         self.mock_get_bin_path.stop()
+        self.mock_preserved_copy.stop()
+        self.mock_atomic_move.stop()
         self.mock_selinux_context.stop()
         self.mock_is_special_selinux_path.stop()
         self.mock_os_path_exists.stop()
@@ -65,8 +91,10 @@ def test_create_jks_success(self):
         ))
 
         module = AnsibleModule(
-            argument_spec=self.spec.argument_spec,
-            supports_check_mode=self.spec.supports_check_mode
+            argument_spec=module_argument_spec,
+            supports_check_mode=module_supports_check_mode,
+            mutually_exclusive=module_choose_between,
+            required_one_of=module_choose_between
         )
 
         with patch('os.remove', return_value=True):
@@ -96,8 +124,10 @@ def test_create_jks_keypass_fail_export_pkcs12(self):
         ))
 
         module = AnsibleModule(
-            argument_spec=self.spec.argument_spec,
-            supports_check_mode=self.spec.supports_check_mode
+            argument_spec=module_argument_spec,
+            supports_check_mode=module_supports_check_mode,
+            mutually_exclusive=module_choose_between,
+            required_one_of=module_choose_between
         )
 
         module.exit_json = Mock()
@@ -106,7 +136,7 @@ def test_create_jks_keypass_fail_export_pkcs12(self):
         with patch('os.remove', return_value=True):
             self.create_path.side_effect = ['/tmp/tmp1cyp12xa']
             self.create_file.side_effect = ['/tmp/tmpvalcrt32', '/tmp/tmpwh4key0c', '']
-            self.run_command.side_effect = [(1, '', ''), (0, '', '')]
+            self.run_command.side_effect = [(1, '', 'Oops'), (0, '', '')]
             self.get_bin_path.side_effect = ['keytool', 'openssl', '']
             jks = JavaKeystore(module)
             jks.create()
@@ -118,6 +148,7 @@ def test_create_jks_keypass_fail_export_pkcs12(self):
                      "-passout", "stdin",
                      "-passin", "stdin"],
                 msg='',
+                err='Oops',
                 rc=1
             )
 
@@ -131,8 +162,10 @@ def test_create_jks_fail_export_pkcs12(self):
         ))
 
         module = AnsibleModule(
-            argument_spec=self.spec.argument_spec,
-            supports_check_mode=self.spec.supports_check_mode
+            argument_spec=module_argument_spec,
+            supports_check_mode=module_supports_check_mode,
+            mutually_exclusive=module_choose_between,
+            required_one_of=module_choose_between
         )
 
         module.exit_json = Mock()
@@ -141,7 +174,7 @@ def test_create_jks_fail_export_pkcs12(self):
         with patch('os.remove', return_value=True):
             self.create_path.side_effect = ['/tmp/tmp1cyp12xa']
             self.create_file.side_effect = ['/tmp/tmpvalcrt32', '/tmp/tmpwh4key0c', '']
-            self.run_command.side_effect = [(1, '', ''), (0, '', '')]
+            self.run_command.side_effect = [(1, '', 'Oops'), (0, '', '')]
             self.get_bin_path.side_effect = ['keytool', 'openssl', '']
             jks = JavaKeystore(module)
             jks.create()
@@ -152,6 +185,7 @@ def test_create_jks_fail_export_pkcs12(self):
                      "-out", "/tmp/tmp1cyp12xa",
                      "-passout", "stdin"],
                 msg='',
+                err='Oops',
                 rc=1
             )
 
@@ -165,8 +199,10 @@ def test_create_jks_fail_import_key(self):
         ))
 
         module = AnsibleModule(
-            argument_spec=self.spec.argument_spec,
-            supports_check_mode=self.spec.supports_check_mode
+            argument_spec=module_argument_spec,
+            supports_check_mode=module_supports_check_mode,
+            mutually_exclusive=module_choose_between,
+            required_one_of=module_choose_between
         )
 
         module.exit_json = Mock()
@@ -175,7 +211,7 @@ def test_create_jks_fail_import_key(self):
         with patch('os.remove', return_value=True):
             self.create_path.side_effect = ['/tmp/tmpgrzm2ah7']
             self.create_file.side_effect = ['/tmp/etacifitrec', '/tmp/yek_etavirp', '']
-            self.run_command.side_effect = [(0, '', ''), (1, '', '')]
+            self.run_command.side_effect = [(0, '', ''), (1, '', 'Oops')]
             self.get_bin_path.side_effect = ['keytool', 'openssl', '']
             jks = JavaKeystore(module)
             jks.create()
@@ -185,6 +221,7 @@ def test_create_jks_fail_import_key(self):
                      "-srckeystore", "/tmp/tmpgrzm2ah7", "-srcstoretype", "pkcs12", "-alias", "test",
                      "-noprompt"],
                 msg='',
+                err='Oops',
                 rc=1
             )
 
@@ -195,20 +232,28 @@ class TestCertChanged(ModuleTestCase):
     def setUp(self):
         """Setup."""
         super(TestCertChanged, self).setUp()
-        self.spec = ArgumentSpec()
         self.mock_create_file = patch('ansible_collections.community.general.plugins.modules.system.java_keystore.create_file')
+        self.mock_current_type = patch('ansible_collections.community.general.plugins.modules.system.java_keystore.JavaKeystore.current_type')
         self.mock_run_command = patch('ansible.module_utils.basic.AnsibleModule.run_command')
         self.mock_get_bin_path = patch('ansible.module_utils.basic.AnsibleModule.get_bin_path')
+        self.mock_preserved_copy = patch('ansible.module_utils.basic.AnsibleModule.preserved_copy')
+        self.mock_atomic_move = patch('ansible.module_utils.basic.AnsibleModule.atomic_move')
         self.run_command = self.mock_run_command.start()
         self.create_file = self.mock_create_file.start()
         self.get_bin_path = self.mock_get_bin_path.start()
+        self.current_type = self.mock_current_type.start()
+        self.preserved_copy = self.mock_preserved_copy.start()
+        self.atomic_move = self.mock_atomic_move.start()
 
     def tearDown(self):
         """Teardown."""
         super(TestCertChanged, self).tearDown()
         self.mock_create_file.stop()
+        self.mock_current_type.stop()
         self.mock_run_command.stop()
         self.mock_get_bin_path.stop()
+        self.mock_preserved_copy.stop()
+        self.mock_atomic_move.stop()
 
     def test_cert_unchanged_same_fingerprint(self):
         set_module_args(dict(
@@ -220,14 +265,17 @@ def test_cert_unchanged_same_fingerprint(self):
         ))
 
         module = AnsibleModule(
-            argument_spec=self.spec.argument_spec,
-            supports_check_mode=self.spec.supports_check_mode
+            argument_spec=module_argument_spec,
+            supports_check_mode=module_supports_check_mode,
+            mutually_exclusive=module_choose_between,
+            required_one_of=module_choose_between
         )
 
         with patch('os.remove', return_value=True):
             self.create_file.side_effect = ['/tmp/placeholder', '']
             self.run_command.side_effect = [(0, 'foo=abcd:1234:efgh', ''), (0, 'SHA256: abcd:1234:efgh', '')]
             self.get_bin_path.side_effect = ['keytool', 'openssl', '']
+            self.current_type.side_effect = ['jks']
             jks = JavaKeystore(module)
             result = jks.cert_changed()
             self.assertFalse(result, 'Fingerprint is identical')
@@ -242,19 +290,22 @@ def test_cert_changed_fingerprint_mismatch(self):
         ))
 
         module = AnsibleModule(
-            argument_spec=self.spec.argument_spec,
-            supports_check_mode=self.spec.supports_check_mode
+            argument_spec=module_argument_spec,
+            supports_check_mode=module_supports_check_mode,
+            mutually_exclusive=module_choose_between,
+            required_one_of=module_choose_between
         )
 
         with patch('os.remove', return_value=True):
             self.create_file.side_effect = ['/tmp/placeholder', '']
             self.run_command.side_effect = [(0, 'foo=abcd:1234:efgh', ''), (0, 'SHA256: wxyz:9876:stuv', '')]
             self.get_bin_path.side_effect = ['keytool', 'openssl', '']
+            self.current_type.side_effect = ['jks']
             jks = JavaKeystore(module)
             result = jks.cert_changed()
             self.assertTrue(result, 'Fingerprint mismatch')
 
-    def test_cert_changed_fail_alias_does_not_exist(self):
+    def test_cert_changed_alias_does_not_exist(self):
         set_module_args(dict(
             certificate='cert-foo',
             private_key='private-foo',
@@ -264,8 +315,10 @@ def test_cert_changed_fail_alias_does_not_exist(self):
         ))
 
         module = AnsibleModule(
-            argument_spec=self.spec.argument_spec,
-            supports_check_mode=self.spec.supports_check_mode
+            argument_spec=module_argument_spec,
+            supports_check_mode=module_supports_check_mode,
+            mutually_exclusive=module_choose_between,
+            required_one_of=module_choose_between
         )
 
         with patch('os.remove', return_value=True):
@@ -287,8 +340,10 @@ def test_cert_changed_password_mismatch(self):
         ))
 
         module = AnsibleModule(
-            argument_spec=self.spec.argument_spec,
-            supports_check_mode=self.spec.supports_check_mode
+            argument_spec=module_argument_spec,
+            supports_check_mode=module_supports_check_mode,
+            mutually_exclusive=module_choose_between,
+            required_one_of=module_choose_between
         )
 
         with patch('os.remove', return_value=True):
@@ -310,8 +365,10 @@ def test_cert_changed_fail_read_cert(self):
         ))
 
         module = AnsibleModule(
-            argument_spec=self.spec.argument_spec,
-            supports_check_mode=self.spec.supports_check_mode
+            argument_spec=module_argument_spec,
+            supports_check_mode=module_supports_check_mode,
+            mutually_exclusive=module_choose_between,
+            required_one_of=module_choose_between
         )
 
         module.exit_json = Mock()
@@ -321,6 +378,7 @@ def test_cert_changed_fail_read_cert(self):
             self.create_file.side_effect = ['/tmp/tmpdj6bvvme', '']
             self.run_command.side_effect = [(1, '', 'Oops'), (0, 'SHA256: wxyz:9876:stuv', '')]
             self.get_bin_path.side_effect = ['keytool', 'openssl', '']
+            self.current_type.side_effect = ['jks']
             jks = JavaKeystore(module)
             jks.cert_changed()
             module.fail_json.assert_called_once_with(
@@ -340,8 +398,10 @@ def test_cert_changed_fail_read_keystore(self):
         ))
 
         module = AnsibleModule(
-            argument_spec=self.spec.argument_spec,
-            supports_check_mode=self.spec.supports_check_mode
+            argument_spec=module_argument_spec,
+            supports_check_mode=module_supports_check_mode,
+            mutually_exclusive=module_choose_between,
+            required_one_of=module_choose_between
         )
 
         module.exit_json = Mock()