Repository: ansible/ansible

Issue:
hardcoded $HOME/.ansible/cp; unexpected ControlPath behavior
##### ISSUE TYPE
 - Bug Report
 - Feature Idea
 - Documentation Report

##### COMPONENT NAME
plugins/connection/ssh.py
constants.py
examples/ansible.cfg

##### ANSIBLE VERSION
```
ansible 2.2.0.0
```

##### CONFIGURATION
ssh_args
control_path

##### OS / ENVIRONMENT
N/A

##### SUMMARY
1. When using the `control_path` variable, the directory portion of the ControlPath option is hard-coded to `$HOME/.ansible/cp` using the named placeholder `%(directory)s`
   * This should be noted in the sample `ansible.cfg` file. The meaning of `%(directory)s` is quite obfuscated.
   * I would prefer a different solution, such as `control_path` being the entire directory/filename but that may not be practical at this time due to existing user config files containing `%(directory)s`. Perhaps a new variable such as SSH_CONTROL_PATH_DIRECTORY could be implemented with a default value of $HOME/.ansible/cp to maintain backwards compatibility?
2. `control_path` can be defined w/o `%(directory)s` but the plugin will still create the directory `$HOME/.ansible/cp` and will test that directory for write permissions instead of `control_path`
   * `-o ControlPath=` can be included in `ssh_args` and this stops the creation of `$HOME/.ansible/cp` but likewise this bypasses the plugin testing that directory for write permissions. This is probably the best work-around solution.
3. `control_path` cannot use environment variables such as `$HOME`. ~/ works in the `ssh_args` command string, I didn't test it in `control_path`, but it seems likely that it would work.
   * With no other changes, this should be documented. (where? sample `ansible.cfg`?)
   * Could ANSIBLE_SSH_CONTROL_PATH be passed through `unfrackpath()` to expand environment variables?

Points 2 and 3 above were causes of this issue: #12151

##### RELEVENT CODE SNIPIT: ssh.py
```
        # Check if ControlPersist is enabled and add a ControlPath if one hasn't
        # already been set.

        controlpersist, controlpath = self._persistence_controls(b_command)

        if controlpersist:
            self._persistent = True

            if not controlpath:
                cpdir = unfrackpath(u'$HOME/.ansible/cp')
                b_cpdir = to_bytes(cpdir, errors='surrogate_or_strict')

                # The directory must exist and be writable.
                makedirs_safe(b_cpdir, 0o700)
                if not os.access(b_cpdir, os.W_OK):
                    raise AnsibleError("Cannot write to ControlPath %s" % to_native(cpdir))

                b_args = (b"-o", b"ControlPath=" + to_bytes(C.ANSIBLE_SSH_CONTROL_PATH % dict(directory=cpdir), errors='surrogate_or_strict'))
```


Discussion:

Resolving patch:
diff --git a/lib/ansible/constants.py b/lib/ansible/constants.py
--- a/lib/ansible/constants.py
+++ b/lib/ansible/constants.py
@@ -299,6 +299,7 @@ def load_config_file():
 # CONNECTION RELATED
 ANSIBLE_SSH_ARGS               = get_config(p, 'ssh_connection', 'ssh_args', 'ANSIBLE_SSH_ARGS', '-C -o ControlMaster=auto -o ControlPersist=60s')
 ANSIBLE_SSH_CONTROL_PATH       = get_config(p, 'ssh_connection', 'control_path', 'ANSIBLE_SSH_CONTROL_PATH', u"%(directory)s/ansible-ssh-%%h-%%p-%%r")
+ANSIBLE_SSH_CONTROL_PATH_DIR   = get_config(p, 'ssh_connection', 'control_path_dir', 'ANSIBLE_SSH_CONTROL_PATH_DIR', u'$HOME/.ansible/cp')
 ANSIBLE_SSH_PIPELINING         = get_config(p, 'ssh_connection', 'pipelining', 'ANSIBLE_SSH_PIPELINING', False, value_type='boolean')
 ANSIBLE_SSH_RETRIES            = get_config(p, 'ssh_connection', 'retries', 'ANSIBLE_SSH_RETRIES', 0, value_type='integer')
 ANSIBLE_SSH_EXECUTABLE         = get_config(p, 'ssh_connection', 'ssh_executable', 'ANSIBLE_SSH_EXECUTABLE', 'ssh')
diff --git a/lib/ansible/plugins/connection/ssh.py b/lib/ansible/plugins/connection/ssh.py
--- a/lib/ansible/plugins/connection/ssh.py
+++ b/lib/ansible/plugins/connection/ssh.py
@@ -228,7 +228,7 @@ def _build_command(self, binary, *other_args):
             self._persistent = True
 
             if not controlpath:
-                cpdir = unfrackpath(u'$HOME/.ansible/cp')
+                cpdir = unfrackpath(C.ANSIBLE_SSH_CONTROL_PATH_DIR)
                 b_cpdir = to_bytes(cpdir, errors='surrogate_or_strict')
 
                 # The directory must exist and be writable.


Test patch: