Repository: sosreport/sos

Issue:
Dont break execution of commands after a duplicated one
When a list of commands is sent to be executed to a container, the output is collected on a file and a symbolic link is created to that file from a container folder. If a command is duplicated it fails breaking the execution of the rest of the command on the list. 

Reproducer:

- Create a list with several commands to be executed on a container. Be sure that one command is duplicated.
- Execute the sos report ensuring that the plugin is executed.
- The plugin will trigger an error on the console 

> Starting 91/131 plugin_name       [Running: plugin_name] caught exception in plugin method "plugin_name.collect()"

- The output will be appending to the sos_logs/<plugin_name>-plugin-errors.txt showing the exception:


```python 
Traceback (most recent call last):
  File "/usr/lib/python3.9/site-packages/sos/report/__init__.py", line 1331, in collect_plugin
         plug.collect()
  File "/usr/lib/python3.9/site-packages/sos/report/plugins/__init__.py", line 3100, in collect
         self._collect_cmds()
  File "/usr/lib/python3.9/site-packages/sos/report/plugins/__init__.py", line 3077, in _collect_cmds
    self._collect_cmd_output(**soscmd.__dict__)
  File "/usr/lib/python3.9/site-packages/sos/report/plugins/__init__.py", line 2441, in _collect_cmd_output
    self._add_container_cmd_to_manifest(manifest_cmd.copy(),
  File "/usr/lib/python3.9/site-packages/sos/report/plugins/__init__.py", line 2642, in _add_container_cmd_to_manifest
    self.archive.check_path(conlnk, P_FILE)
  File "/usr/lib/python3.9/site-packages/sos/archive.py", line 321, in check_path
    raise ValueError(ve_msg % (dest, "regular file"))
ValueError: path '/var/tmp/sos.d5u_pdoa/sosreport-ctrl-2-0-2023-02-06-ftvoxsn/sos_containers/4eb81ed131e7/sos_commands/plugin_name/command_duplicated' exists and is not a regular file
```



Discussion:
I think this specific error is unexpected within our current handling of duplicated files.

It should only be an error to try and re-create an existing file if the file type differs from what is already present in the archive. E.G. if we have a regular file in the archive, a new regular file should be fine but over-writing the existing archive file with a symlink or somesuch would be an error.

However, if they're being called within the same `add_cmd_output()` call...I'm not sure how the original file is not being created as a regular file, as (non-binary, non-direct-io) commands are written via `Archive.add_string()`.
> It should only be an error to try and re-create an existing file if the file type differs from what is already present in the archive. E.G. if we have a regular file in the archive, a new regular file should be fine but over-writing the existing archive file with a symlink or somesuch would be an error.
> 

IMHO looks a bug in the function `_collect_cmd_output()` when the command is running on a container (container_cmd=True) 

My findings after a more depth analysis, the steps for every command in the list is as follow:

1. Create the command output file calling [here](https://github.com/sosreport/sos/blob/main/sos/report/plugins/__init__.py#L2272). This call will create the command file in /sos_commands/<plugin_name/podman_exec_<container_id>_command

2. As it is a command running inside a container, at the end of the previous function will call [here](https://github.com/sosreport/sos/blob/main/sos/report/plugins/__init__.py#L2624) and it will create the symbolic link from 
  /sos_container/<container_id>/sos_commands/<plugin_name/podman_exec_<container_id>_command ---> /sos_commands/<plugin_name/podman_exec_<container_id>_command 

The critical point is on function [check_path](https://github.com/sosreport/sos/blob/main/sos/archive.py#L257)   where first command doesn't enter into [if statement](https://github.com/sosreport/sos/blob/main/sos/archive.py#L316), but the second command will do, and we are calling this check_path with P_FILE, but the first command write a P_LINK one, so looks like this is the culprit of the raising exception.

So you're right because the existing file differs from what is already present in the archive.

Ah, yep - that'll do it. I had the link<>real location swapped in my head as I was walking through this.


I'll need to test this, but this might be a simple fix of changing [this P_FILE](https://github.com/sosreport/sos/blob/main/sos/report/plugins/__init__.py#L2648) to P_LINK
> I'll need to test this, but this might be a simple fix of changing [this P_FILE](https://github.com/sosreport/sos/blob/main/sos/report/plugins/__init__.py#L2648) to P_LINK

I already tested and it works, but I think we should trust on the check_path return value to create the symbolic link or not, because if the path (symbolic link) already exists, `os.symlink()` will return an `[Error 17] File already exits`. Just to cover the duplicate commands behaviour. check_path will return None when a file already exists, so will be easy to avoid the sym link creation for those cases, wdyt?
Ack. The following `os.symlink()` should be dependent upon the `check_path()` call.

Resolving patch:
diff --git a/sos/report/plugins/__init__.py b/sos/report/plugins/__init__.py
--- a/sos/report/plugins/__init__.py
+++ b/sos/report/plugins/__init__.py
@@ -16,7 +16,7 @@
                            listdir, path_join, bold, file_is_binary,
                            recursive_dict_values_by_key)
 
-from sos.archive import P_FILE
+from sos.archive import P_FILE, P_LINK
 import contextlib
 import os
 import glob
@@ -2645,8 +2645,11 @@ def _add_container_cmd_to_manifest(self, manifest, contup):
         cmdfn = self._mangle_command(cmd)
         conlnk = "%s/%s" % (_cdir, cmdfn)
 
-        self.archive.check_path(conlnk, P_FILE)
-        os.symlink(_outloc, self.archive.dest_path(conlnk))
+        # If check_path return None, it means that the sym link already exits,
+        # so to avoid Error 17, trying to recreate, we will skip creation and
+        # trust on the existing sym link (e.g. duplicate command)
+        if self.archive.check_path(conlnk, P_LINK):
+            os.symlink(_outloc, self.archive.dest_path(conlnk))
 
         manifest['filepath'] = conlnk
         self.manifest.containers[container]['commands'].append(manifest)


Test patch: