Repository: ansible-collections/community.general

Issue:
ini_file: reports change when told to remove non-existing section
### Summary

When using ini_file to remove a section from an ini file, changes are reported even if no changes were done/required.

### Issue Type

Bug Report

### Component Name

ini_file

### Ansible Version

```console (paste below)
$ ansible --version
ansible [core 2.11.6] 
  config file = /home/user/ansible/ansible.cfg
  configured module search path = ['/home/user/ansible/library']
  ansible python module location = /usr/lib/python3/dist-packages/ansible
  ansible collection location = /home/user/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/bin/ansible
  python version = 3.8.10 (default, Nov 26 2021, 20:14:08) [GCC 9.3.0]
  jinja version = 2.10.1
  libyaml = True
```


### Community.general Version

```console (paste below)
$ ansible-galaxy collection list community.general
# /usr/lib/python3/dist-packages/ansible_collections
Collection        Version
----------------- -------
community.general 3.7.0
```


### Configuration

```console (paste below)
$ ansible-config dump --only-changed
ANSIBLE_NOCOWS(/home/user/ansible/ansible.cfg) = True
CALLBACKS_ENABLED(env: ANSIBLE_CALLBACKS_ENABLED) = ['profile_tasks']
DEFAULT_FORKS(/home/user/ansible/ansible.cfg) = 150
DEFAULT_HOST_LIST(/home/user/ansible/ansible.cfg) = ['/home/user/ansible/inventory-production.ini']
DEFAULT_LOCAL_TMP(/home/user/ansible/ansible.cfg) = /home/user/.ansible/tmp/ansible-local-19356977961lflx
DEFAULT_MODULE_PATH(/home/user/ansible/ansible.cfg) = ['/home/user/ansible/library']
DEFAULT_MODULE_UTILS_PATH(/home/user/ansible/ansible.cfg) = ['/home/user/ansible/module_utils']
DEFAULT_POLL_INTERVAL(/home/user/ansible/ansible.cfg) = 15
DEFAULT_REMOTE_USER(/home/user/ansible/ansible.cfg) = ansible
DEFAULT_ROLES_PATH(/home/user/ansible/ansible.cfg) = ['/home/user/ansible/roles']
DEFAULT_STDOUT_CALLBACK(env: ANSIBLE_STDOUT_CALLBACK) = yaml
DEFAULT_TIMEOUT(/home/user/ansible/ansible.cfg) = 45
DEFAULT_VAULT_IDENTITY_LIST(/home/user/ansible/ansible.cfg) = ['prod@vault-prod', 'dev@vault-dev']
DIFF_ALWAYS(env: ANSIBLE_DIFF_ALWAYS) = True
GALAXY_ROLE_SKELETON(/home/user/ansible/ansible.cfg) = /home/user/ansible/skeleton-role
HOST_KEY_CHECKING(/home/user/ansible/ansible.cfg) = False
INVENTORY_ANY_UNPARSED_IS_FAILED(env: ANSIBLE_INVENTORY_ANY_UNPARSED_IS_FAILED) = True
INVENTORY_UNPARSED_IS_FAILED(env: ANSIBLE_INVENTORY_UNPARSED_FAILED) = True
RETRY_FILES_SAVE_PATH(/home/user/ansible/ansible.cfg) = /home/user/ansible/.ansible-retry
```


### OS / Environment

Ubuntu 20.04

### Steps to Reproduce

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)
- name: reset test file
  ansible.builtin.copy:
      content: |
        [section1]
        foo=bar

        [section2]
        bar=foo

        [section3]
        haha=hoho
      dest: /tmp/foo.ini

- name: remove section
  community.general.ini_file:
    path: /tmp/foo.ini
    section: "section2"
    state: absent

- name: remove section again
  community.general.ini_file:
    path: /tmp/foo.ini
    section: "section2"
    state: absent

- name: remove section again w. loop
  community.general.ini_file:
    path: /tmp/foo.ini
    section: "{{ item.section }}"
    state: absent
  with_items:
    - { section: "section2" }
```


### Expected Results

I would expect task `remove section again` and task `remove section again w. loop` to report no changes

### Actual Results

Task `remove section again` and task `remove section again w. loop` reports changes

```console (paste below)
TASK [reset test file] ***********************************************
fredag 04 februar 2022  14:17:20 +0100 (0:00:02.062)       0:00:02.107
--- before: /tmp/foo.ini
+++ after: /home/user/.ansible/tmp/ansible-local-1936585ojv905_r/tmpftrnqd9q
@@ -1,5 +1,8 @@
 [section1]
 foo=bar
  
+[section2]
+bar=foo
+
 [section3]
 haha=hoho

changed: [node01.example.com]

TASK [remove section] ************************************************
fredag 04 februar 2022  14:17:22 +0100 (0:00:01.235)       0:00:03.343
--- before: /tmp/foo.ini (content)
+++ after: /tmp/foo.ini (content)
@@ -1,8 +1,5 @@
 [section1]
 foo=bar
  
-[section2]
-bar=foo
-
 [section3]
 haha=hoho

changed: [node01.example.com]

TASK [remove section again] ******************************************
fredag 04 februar 2022  14:17:22 +0100 (0:00:00.355)       0:00:03.699
changed: [node01.example.com]

TASK [remove section again w. loop] **********************************
fredag 04 februar 2022  14:17:22 +0100 (0:00:00.259)       0:00:03.958
changed: [node01.example.com] => (item={'section': 'section2'})


```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


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

If these files are incorrect, 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/devel/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @jpmens @noseka1
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: notify --->
oblikoamorale at Matrix pointed me here: https://github.com/ansible-collections/community.general/blob/main/plugins/modules/files/ini_file.py#L368
> it doesn't handle this case that section might not exist, and unconditionally sets changed = True even if diff.before and diff.after is the same.

Possible change?:
```
if len(section_lines):
	section_lines = []
	msg = 'section removed'
	changed = True
changed = False
```

Or possibly just 'changed = diff.before != diff.after' at the end instead of hard 'changed = True'
workaround: `changed_when: ini_file_task.diff.before != ini_file_task.diff.after`

Resolving patch:
diff --git a/plugins/modules/files/ini_file.py b/plugins/modules/files/ini_file.py
--- a/plugins/modules/files/ini_file.py
+++ b/plugins/modules/files/ini_file.py
@@ -367,9 +367,10 @@ def do_ini(module, filename, section=None, option=None, values=None,
                     section_lines = new_section_lines
         else:
             # drop the entire section
-            section_lines = []
-            msg = 'section removed'
-            changed = True
+            if section_lines:
+                section_lines = []
+                msg = 'section removed'
+                changed = True
 
     # reassemble the ini_lines after manipulation
     ini_lines = before + section_lines + after


Test patch:
diff --git a/tests/integration/targets/ini_file/tasks/tests/02-values.yml b/tests/integration/targets/ini_file/tasks/tests/02-values.yml
--- a/tests/integration/targets/ini_file/tasks/tests/02-values.yml
+++ b/tests/integration/targets/ini_file/tasks/tests/02-values.yml
@@ -504,6 +504,13 @@
     state: absent
   register: result20
 
+- name: test-values 20 - remove section 'drinks' again to ensure idempotency"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    state: absent
+  register: result20_remove_again  
+
 - name: test-values 20 - read content from output file
   slurp:
     src: "{{ output_file }}"
@@ -517,6 +524,7 @@
   assert:
     that:
       - result20 is changed
+      - result20_remove_again is not changed
       - result20.msg == 'section removed'
       - content20 == "\n"