Repository: StackStorm/st2

Issue:
Fix multiple file support in linux.file_watch.line + black + fstring
When multiple file_watch rules are defined, the last defined file reference is used for all files being watched.  This causes trigger-instances to fail rule enforcement.

Adding the reference to the logging shows `test1.log` has the reference ending with `8c505`
```
2021-11-30 18:50:40,434 140243179888112 INFO file_watch_sensor [-] Added file "/var/log/test1.log" with reference linux.7e55ad75-b10c-44db-b53e-95164a18c505
2021-11-30 18:50:41,459 140243179888112 INFO file_watch_sensor [-] Added file "/var/log/test2.log" with reference linux.590de8c1-c578-4125-9082-2cee03b030a9
```

When the file contents are updated a trigger is emitted by the sensor using the reference of `test2.log` ending in `b030a9`
```
root@u1804:~# st2 trigger-instance get 61a6649f164625c2d94dccb8 -y
id: 61a6649f164625c2d94dccb8
occurrence_time: '2021-11-30T17:51:27.294000Z'
payload:
    file_name: test1.log
    file_path: /var/log/test1.log
    line: Tue Nov 30 18:51:27 CET 2021 dhcp
status: processed
trigger: linux.590de8c1-c578-4125-9082-2cee03b030a9
```

This PR consists of adding a dictionary that is used to track the `path_name` and `reference` pair and looks up the reference for the file that was altered when creating the trigger.

The code is formatted with black and updated to use fstrings since all instances will be using Python 3.6+


Discussion:

Resolving patch:
diff --git a/contrib/linux/sensors/file_watch_sensor.py b/contrib/linux/sensors/file_watch_sensor.py
--- a/contrib/linux/sensors/file_watch_sensor.py
+++ b/contrib/linux/sensors/file_watch_sensor.py
@@ -14,7 +14,6 @@
 # limitations under the License.
 
 import os
-
 import eventlet
 
 from logshipper.tail import Tail
@@ -27,44 +26,46 @@ def __init__(self, sensor_service, config=None):
         super(FileWatchSensor, self).__init__(
             sensor_service=sensor_service, config=config
         )
-        self._trigger = None
-        self._logger = self._sensor_service.get_logger(__name__)
-        self._tail = None
+        self.log = self._sensor_service.get_logger(__name__)
+        self.tail = None
+        self.file_ref = {}
 
     def setup(self):
-        self._tail = Tail(filenames=[])
-        self._tail.handler = self._handle_line
-        self._tail.should_run = True
+        self.tail = Tail(filenames=[])
+        self.tail.handler = self._handle_line
+        self.tail.should_run = True
 
     def run(self):
-        self._tail.run()
+        self.tail.run()
 
     def cleanup(self):
-        if self._tail:
-            self._tail.should_run = False
+        if self.tail:
+            self.tail.should_run = False
 
             try:
-                self._tail.notifier.stop()
+                self.tail.notifier.stop()
             except Exception:
-                self._logger.exception("Unable to stop the tail notifier")
+                self.log.exception("Unable to stop the tail notifier")
 
     def add_trigger(self, trigger):
         file_path = trigger["parameters"].get("file_path", None)
 
         if not file_path:
-            self._logger.error('Received trigger type without "file_path" field.')
+            self.log.error('Received trigger type without "file_path" field.')
             return
 
-        self._trigger = trigger.get("ref", None)
+        trigger = trigger.get("ref", None)
 
-        if not self._trigger:
-            raise Exception("Trigger %s did not contain a ref." % trigger)
+        if not trigger:
+            raise Exception(f"Trigger {trigger} did not contain a ref.")
 
         # Wait a bit to avoid initialization race in logshipper library
         eventlet.sleep(1.0)
 
-        self._tail.add_file(filename=file_path)
-        self._logger.info('Added file "%s"' % (file_path))
+        self.tail.add_file(filename=file_path)
+        self.file_ref[file_path] = trigger
+
+        self.log.info(f"Added file '{file_path}' ({trigger}) to watch list.")
 
     def update_trigger(self, trigger):
         pass
@@ -73,22 +74,28 @@ def remove_trigger(self, trigger):
         file_path = trigger["parameters"].get("file_path", None)
 
         if not file_path:
-            self._logger.error('Received trigger type without "file_path" field.')
+            self.log.error("Received trigger type without 'file_path' field.")
             return
 
-        self._tail.remove_file(filename=file_path)
-        self._trigger = None
+        self.tail.remove_file(filename=file_path)
+        self.file_ref.pop(file_path)
 
-        self._logger.info('Removed file "%s"' % (file_path))
+        self.log.info(f"Removed file '{file_path}' ({trigger}) from watch list.")
 
     def _handle_line(self, file_path, line):
-        trigger = self._trigger
+        if file_path not in self.file_ref:
+            self.log.error(
+                f"No reference found for {file_path}, unable to emit trigger!"
+            )
+            return
+
+        trigger = self.file_ref[file_path]
         payload = {
             "file_path": file_path,
             "file_name": os.path.basename(file_path),
             "line": line,
         }
-        self._logger.debug(
-            "Sending payload %s for trigger %s to sensor_service.", payload, trigger
+        self.log.debug(
+            f"Sending payload {payload} for trigger {trigger} to sensor_service."
         )
         self.sensor_service.dispatch(trigger=trigger, payload=payload)


Test patch: