Repository: ansible-collections/community.general

Issue:
interfaces_file: Regular expression characters not escaped
<!--- Verify first that your issue is not already reported on GitHub -->
<!--- Also test if the latest release and devel branch are affected too -->
<!--- Complete *all* sections as described, this form is processed automatically -->

##### SUMMARY
<!--- Explain the problem briefly below -->
When replacing a value in /etc/network/interfaces that contains, for example, `||`, the new line is garbage. This is caused by the old value being used directly in a regular expression here:
https://github.com/ansible-collections/community.general/blob/4e56347fc1d79d8f0a1e8f663eca58ee17fb8e47/plugins/modules/system/interfaces_file.py#L287

##### ISSUE TYPE
- Bug Report

##### COMPONENT NAME
<!--- Write the short name of the module, plugin, task or feature below, use your best guess if unsure -->
interfaces_file

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes -->
```paste below
ansible 2.9.10                                                                                                                                                                                                                                
  config file = /home/jpl/ansible/ansible.cfg
  configured module search path = ['/home/jpl/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/jpl/.local/lib/python3.6/site-packages/ansible
  executable location = /home/jpl/.local/bin/ansible
  python version = 3.6.9 (default, Jul 17 2020, 12:50:27) [GCC 8.4.0]
```

##### CONFIGURATION
<!--- Paste verbatim output from "ansible-config dump --only-changed" between quotes -->
```paste below
```

##### OS / ENVIRONMENT
<!--- Provide all relevant information below, e.g. target OS versions, network device firmware, etc. -->
Target Debian Buster, but probably irrelevant

##### STEPS TO REPRODUCE
<!--- Describe exactly how to reproduce the problem, using a minimal test-case -->

<!--- Paste example playbooks or commands between quotes below -->
```yaml
- hosts: all
  gather_facts: no
  tasks:
    - interfaces_file:
        iface: dummy0
        option: description
        value: 'Funny || name'

    - interfaces_file:
        iface: dummy0
        option: description
        value: 'Another || funny || name'
```

<!--- HINT: You can paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- Describe what you expected to happen when running the steps above -->
/etc/network/interfaces:
```
iface dummy0 inet manual
    [...]
    description Another || funny || name
```

##### ACTUAL RESULTS
<!--- Describe what actually happened. If possible run with extra verbosity (-vvvv) -->

/etc/network/interfaces:
```
iface dummy0 inet manual
    [...]
    descriptionAnother || funny || name Funny || name
```

<!--- Paste verbatim command output between quotes -->
```paste below

```


Discussion:
Files identified in the description:
* [`plugins/modules/system/interfaces_file.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/system/interfaces_file.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 @MorrisA @bcoca @d-little @flynn1973 @gforster @hryamzik @kairoaraujo @marvin-sinister @mator @molekuul @obourdon @ramooncamacho @wtcross
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
I just noticed that interfaces_file expects "post-down" to actually be "down", so my example is treated as a value that should be replaced instead of added. If I use `option: down`, it works as intended.

However, the bug still exists in all other options. Will change the example.
Should be easy to fix (if the problem is in the line you say), by replacing that line with
```.py
old_value_position = re.search(r"\s+".join([re.escape(part) for part in old_value.split()]), old_line[prefix_start + optionLen:])
```
(with appropriate line breaks to avoid linting errors :) )

@jplitza do you want to create a PR?
@felixfontein I have had terrible experiences with PRs for Ansible, having to re-create them several times in different projects because of restructuring, having to rebase them due to lack of response, and having to write changelogs without any guidance as to what to write where.

So with all due respect: No, I do not want to create a PR.
i can't reproduce it on debian10 `Debian GNU/Linux 10 (buster)` with the development/git version of ansible:

```
$ ansible --version
[WARNING]: You are running the development version of Ansible. You should only run Ansible from "devel" if you are modifying the Ansible engine, or trying out
features under development. This is a rapidly changing source of code and can become unstable at any point.
ansible 2.11.0.dev0
  config file = /home/mator/ansible.dev/ansible.cfg
  configured module search path = ['/home/mator/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/mator/ansible.dev/ansible/lib/ansible
  ansible collection location = /home/mator/ansible.dev/collections
  executable location = /home/mator/ansible.dev/ansible/bin/ansible
  python version = 3.8.5 (default, Jul 27 2020, 08:42:51) [GCC 10.1.0]
```

just before playbook run:

```
root@debian10:/etc/network# tail -n 2 interfaces

iface dummy0 inet manual
```

running:

```
$ cat playbooks/debian-interfaces.yml
- hosts: debian10
  gather_facts: no
  tasks:
    - interfaces_file:
        iface: dummy0
        option: description
        value: 'Funny || name'

    - interfaces_file:
        iface: dummy0
        option: down
        value: 'Another || funny || name'

$ ansible-playbook -b playbooks/debian-interfaces.yml
[WARNING]: You are running the development version of Ansible. You should only run Ansible from "devel" if you are modifying the Ansible engine, or trying out
features under development. This is a rapidly changing source of code and can become unstable at any point.

PLAY [debian10] ****************************************************************************************************************************************************

TASK [interfaces_file] *********************************************************************************************************************************************
changed: [debian10]

TASK [interfaces_file] *********************************************************************************************************************************************
changed: [debian10]

PLAY RECAP *********************************************************************************************************************************************************
debian10                   : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

checking:

```
root@debian10:/etc/network# tail -n 3 interfaces
iface dummy0 inet manual
    description Funny || name
    down Another || funny || name
```
@mator Sorry, I messed up the example. The second task should of course also modify "description" and not "down". Fixed.
@jplitza 
thanks, was able to reproduce...

@felixfontein , 
yep, that line 

```
old_value_position = re.search(r"\s+".join([re.escape(part) for part in old_value.split()]), old_line[prefix_start + optionLen:])
```

fixes it for me... want me to submit PR ?
> @felixfontein I have had terrible experiences with PRs for Ansible, having to re-create them several times in different projects because of restructuring, having to rebase them due to lack of response,

I guess you are refering to ansible/ansible#68362, #88, #89, ansible-collections/community.network#7 and ansible-collections/community.network#6. You unfortunately hit two different problems:
1. The large restructuring / move from a huge repo to a lot of smaller repos (where c.g and c.n are the two largest chunks) (TBH I was assuming that you were aware about the on-going restructuring since you pretty quickly created PRs in c.g long before it was announced to be ready);
2. The unfortunate truth that there are a lot of modules which have no maintainers, no active maintainers, or nobody who cares about community PRs. I don't know what's the exact case for edgeos, but it looks to me like that almost everything in community.network is pretty much dead and gets no reviews. (Your edgeos PR was not the first and not the last.)

I'm sorry that your experience was so terrible, especially for the edgeos PRs. I would have liked to review the PR's content, but I have no knowledge about edgeos at all, so I had no idea (and still not really have one) whether your PR is correct, needs changes, or is even plain wrong.

> and having to write changelogs without any guidance as to what to write where.

You did ask and got suggestions and a link to an example.

> So with all due respect: No, I do not want to create a PR.

That's too bad, because in this case, your change is a lot easier to judge without needing domain knowledge (i.e. I would dare to merge it), and there are active maintainers (@mator :+1:). But if you don't want to, that's also ok. @mator if you want, feel free to create a PR.

Resolving patch:
diff --git a/plugins/modules/system/interfaces_file.py b/plugins/modules/system/interfaces_file.py
--- a/plugins/modules/system/interfaces_file.py
+++ b/plugins/modules/system/interfaces_file.py
@@ -284,7 +284,7 @@ def setInterfaceOption(module, lines, iface, option, raw_value, state, address_f
                     address_family = target_option['address_family']
                     prefix_start = old_line.find(option)
                     optionLen = len(option)
-                    old_value_position = re.search(r"\s+".join(old_value.split()), old_line[prefix_start + optionLen:])
+                    old_value_position = re.search(r"\s+".join(map(re.escape, old_value.split())), old_line[prefix_start + optionLen:])
                     start = old_value_position.start() + prefix_start + optionLen
                     end = old_value_position.end() + prefix_start + optionLen
                     line = old_line[:start] + value + old_line[end:]


Test patch: