Repository: saltstack/salt
Pull request: Consolidating beacon configuration mapping code
### What does this PR do?
Move the commonly used code that converts a list to a dictionary into salt.utils.beacons.  Fixing inotify beacon close function to ensure the configuration is converted from the provided list format into a dictionary.

### What issues does this PR fix or reference?
Fixes: #60241 #60755

### Merge requirements satisfied?
**[NOTICE] Bug fixes or features added to Salt require tests.**
<!-- Please review the [test documentation](https://docs.saltstack.com/en/master/topics/tutorials/writing_tests.html) for details on how to implement tests into Salt's test suite. -->
- [ ] Docs
- [x] Changelog - https://docs.saltstack.com/en/master/topics/development/changelog.html
- [x] Tests written/updated

### Commits signed with GPG?
Yes

Please review [Salt's Contributing Guide](https://docs.saltstack.com/en/master/topics/development/contributing.html) for best practices.

See GitHub's [page on GPG signing](https://help.github.com/articles/signing-commits-using-gpg/) for more information about signing commits with GPG.


Problem statement:
[BUG] inotify beacon fails with AttributeError: 'list' object has no attribute 'get'
**Description**
The inotify beacon fails after sending an event and spams the logfile with python stack traces.

**Setup**
```
beacons:
  enabled: true
  watch_important_files:
  - beacon_module: inotify
  - disable_during_state_run: true
  - files:
      /opt/newdir/do-not-touch:
        mask:
        - modify
```

**Steps to Reproduce the behavior**
1) salt minion1 beacons.add watch_important_files "[{'beacon_module': 'inotify'}, {'disable_during_state_run': true}, {'files': {'/opt/newdir/do-not-touch': {'mask': ['modify']}}}]"
2) modify /opt/newdir/do-not-touch

**Expected behavior**
the minion should not raise an error

**Screenshots**
```
2021-05-24 14:29:43,083 [salt.minion      :2947][CRITICAL][32837] The beacon errored: 
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/salt/minion.py", line 2945, in handle_beacons
    beacons = self.process_beacons(self.functions)
  File "/usr/lib/python3/dist-packages/salt/minion.py", line 512, in process_beacons
    return self.beacons.process(
  File "/usr/lib/python3/dist-packages/salt/beacons/__init__.py", line 107, in process
    self.beacons[close_str](b_config[mod])
  File "/usr/lib/python3/dist-packages/salt/loader.py", line 1235, in __call__
    return self.loader.run(run_func, *args, **kwargs)
  File "/usr/lib/python3/dist-packages/salt/loader.py", line 2268, in run
    return self._last_context.run(self._run_as, _func_or_method, *args, **kwargs)
  File "/usr/lib/python3/dist-packages/salt/loader.py", line 2283, in _run_as
    return _func_or_method(*args, **kwargs)
  File "/usr/lib/python3/dist-packages/salt/beacons/inotify.py", line 370, in close
    beacon_name = config.get("_beacon_name", "inotify")
AttributeError: 'list' object has no attribute 'get'
```

**Versions Report**
<details><summary>salt --versions-report</summary>
(Provided by running salt --versions-report. Please also mention any differences in master/minion versions.) 

```
Salt Version:
          Salt: 3003
 
Dependency Versions:
          cffi: Not Installed
      cherrypy: Not Installed
      dateutil: 2.7.3
     docker-py: Not Installed
         gitdb: 2.0.6
     gitpython: 3.0.7
        Jinja2: 2.10.1
       libgit2: Not Installed
      M2Crypto: Not Installed
          Mako: Not Installed
       msgpack: 0.6.2
  msgpack-pure: Not Installed
  mysql-python: Not Installed
     pycparser: Not Installed
      pycrypto: Not Installed
  pycryptodome: 3.6.1
        pygit2: Not Installed
        Python: 3.8.5 (default, Jan 27 2021, 15:41:15)
  python-gnupg: 0.4.5
        PyYAML: 5.3.1
         PyZMQ: 18.1.1
         smmap: 2.0.5
       timelib: Not Installed
       Tornado: 4.5.3
           ZMQ: 4.3.2
 
System Versions:
          dist: ubuntu 20.04 focal
        locale: utf-8
       machine: x86_64
       release: 5.4.0-73-generic
        system: Linux
       version: Ubuntu 20.04 focal
```
</details>

**Additional context**



inotify Beacon Throwing Exceptions on Minion
### Description of Issue
I am running an inotify beacon that looks for file changes and which calls an orchestration that sends me an email when a file is changed. Looking at the journal for salt-minion, I see multiple repeated exceptions.

I am also having some event loops where a beacon detects that the SSHd config changed, then calls an orchestration which highstates the minion and sends me an email. This beacon seems to get stuck in loops where it will detect a change, highstates, and does it again until the minion service is restarted. The beacon sls does set disable_during_state_run as True. This looping problem may not be related to this issue except that it causes these exceptions - the exception occurs when the file is changed.

### Setup: Minion beacons.conf
```#######################################################
# SSH Section
  SSHD_Files:
    - files:
        /etc/ssh/sshd_config:
          mask:
            - modify
    - disable_during_state_run: True
    - beacon_module: inotify
  SSHD_Service:
    - services:
        sshd:
          onchangeonly: True
    - beacon_module: service
    - disable_during_state_run: True
#######################################################
# Some sections in the beacon config do not apply to the minion it is running on, but it is in the minion's beacon config anyway.
  Svc_Agent_Files:
    - files:
        /etc/svc/agent.conf:
          mask:
            - modify
    - disable_during_state_run: True
    - beacon_module: inotify
  Svc_Agent_Service:
    - services:
        svc-agent:
          onchangeonly: True
    - beacon_module: service
    - disable_during_state_run: True
```

### Setup: salt-minion Journal
Hundreds of these exceptions can be called at once.
```
[CRITICAL] The beacon errored:
Traceback (most recent call last):
  File "/usr/lib/python3.6/site-packages/salt/minion.py", line 2945, in handle_beacons
    beacons = self.process_beacons(self.functions)
  File "/usr/lib/python3.6/site-packages/salt/minion.py", line 513, in process_beacons
    b_conf, self.opts["grains"]
  File "/usr/lib/python3.6/site-packages/salt/beacons/__init__.py", line 107, in process
    self.beacons[close_str](b_config[mod])
  File "/usr/lib/python3.6/site-packages/salt/loader.py", line 1235, in __call__
    return self.loader.run(run_func, *args, **kwargs)
  File "/usr/lib/python3.6/site-packages/salt/loader.py", line 2268, in run
    return self._last_context.run(self._run_as, _func_or_method, *args, **kwargs)
  File "/usr/lib/python3.6/site-packages/contextvars/__init__.py", line 38, in run
    return callable(*args, **kwargs)
  File "/usr/lib/python3.6/site-packages/salt/loader.py", line 2283, in _run_as
    return _func_or_method(*args, **kwargs)
  File "/usr/lib/python3.6/site-packages/salt/beacons/inotify.py", line 370, in close
    beacon_name = config.get("_beacon_name", "inotify")
AttributeError: 'list' object has no attribute 'get'
```

### Steps to Reproduce Issue
The exceptions and looping occur when the SSHd config file is modified. It also is likely to occur when other files are modified, though I have not confirmed this.

### Versions Report
```Salt Version:
          Salt: 3003.1
 
Dependency Versions:
          cffi: 1.11.5
      cherrypy: Not Installed
      dateutil: 2.6.1
     docker-py: Not Installed
         gitdb: Not Installed
     gitpython: Not Installed
        Jinja2: 2.10.1
       libgit2: 0.99.0
      M2Crypto: 0.35.2
          Mako: Not Installed
       msgpack: 0.5.6
  msgpack-pure: Not Installed
  mysql-python: Not Installed
     pycparser: 2.14
      pycrypto: Not Installed
  pycryptodome: Not Installed
        pygit2: 1.1.1
        Python: 3.6.8 (default, Mar 19 2021, 05:13:41)
  python-gnupg: Not Installed
        PyYAML: 3.12
         PyZMQ: 19.0.0
         smmap: Not Installed
       timelib: Not Installed
       Tornado: 4.5.3
           ZMQ: 4.3.4
 
System Versions:
          dist: centos 8 
        locale: UTF-8
       machine: x86_64
       release: 4.18.0-305.3.1.el8.x86_64
        system: Linux
       version: CentOS Stream 8```

Hints from issue discussion:
the following patch solves the reported bug:

```
--- inotify.py	2021-03-24 21:53:25.000000000 +0000
+++ /tmp/inotify.py	2021-05-24 15:26:24.761516777 +0000
@@ -366,9 +366,17 @@
     return ret
 
 
-def close(config):
+def _close(config):
     beacon_name = config.get("_beacon_name", "inotify")
     notifier = "{}.notifier".format(beacon_name)
     if notifier in __context__:
         __context__[notifier].stop()
         del __context__[notifier]
+
+def close(config):
+    if isinstance(config, list):
+        for _config in config:
+            _close(_config)
+    else:
+        _close(config)
+
```
but maybe config should always by treated as a list instead of a dict
@provonet Thanks for the report.  Unfortunately with the above steps I am unable to reproduce this, do you have more than one beacon configured or something else not specified above?
@provonet Never mind.  It was further back in the logs.
@provonet So this is definitely a bug, The configuration for the itnotify (and all beacons) should be a list, filled with dictionaries in most cases so we don't need to handle a situation where it isn't a list.

Commit message: Merge branch 'master' into 60241_beacon_dict_to_list_consolidate

Patch to review:
diff --git a/changelog/60241.fixed b/changelog/60241.fixed
new file mode 100644
index 000000000000..21cb0765134f
--- /dev/null
+++ b/changelog/60241.fixed
@@ -0,0 +1 @@
+Move the commonly used code that converts a list to a dictionary into salt.utils.beacons.  Fixing inotify beacon close function to ensure the configuration is converted from the provided list format into a dictionary.
diff --git a/salt/beacons/adb.py b/salt/beacons/adb.py
index 0378e15b4288..5e6a441cd75b 100644
--- a/salt/beacons/adb.py
+++ b/salt/beacons/adb.py
@@ -5,6 +5,7 @@
 """
 import logging
 
+import salt.utils.beacons
 import salt.utils.path
 
 log = logging.getLogger(__name__)
@@ -32,14 +33,13 @@ def validate(config):
         log.info("Configuration for adb beacon must be a list.")
         return False, ("Configuration for adb beacon must be a list.")
 
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
-    if "states" not in _config:
+    if "states" not in config:
         log.info("Configuration for adb beacon must include a states array.")
         return False, ("Configuration for adb beacon must include a states array.")
     else:
-        if not isinstance(_config["states"], list):
+        if not isinstance(config["states"], list):
             log.info("Configuration for adb beacon must include a states array.")
             return False, ("Configuration for adb beacon must include a states array.")
         else:
@@ -55,7 +55,7 @@ def validate(config):
                 "unknown",
                 "missing",
             ]
-            if any(s not in states for s in _config["states"]):
+            if any(s not in states for s in config["states"]):
                 log.info(
                     "Need a one of the following adb " "states: %s", ", ".join(states)
                 )
@@ -93,10 +93,9 @@ def beacon(config):
     log.trace("adb beacon starting")
     ret = []
 
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
-    out = __salt__["cmd.run"]("adb devices", runas=_config.get("user", None))
+    out = __salt__["cmd.run"]("adb devices", runas=config.get("user", None))
 
     lines = out.split("\n")[1:]
     last_state_devices = list(last_state.keys())
@@ -109,17 +108,17 @@ def beacon(config):
             if device not in last_state_devices or (
                 "state" in last_state[device] and last_state[device]["state"] != state
             ):
-                if state in _config["states"]:
+                if state in config["states"]:
                     ret.append({"device": device, "state": state, "tag": state})
                     last_state[device] = {"state": state}
 
-            if "battery_low" in _config:
+            if "battery_low" in config:
                 val = last_state.get(device, {})
                 cmd = "adb -s {} shell cat /sys/class/power_supply/*/capacity".format(
                     device
                 )
                 battery_levels = __salt__["cmd.run"](
-                    cmd, runas=_config.get("user", None)
+                    cmd, runas=config.get("user", None)
                 ).split("\n")
 
                 for l in battery_levels:
@@ -128,8 +127,8 @@ def beacon(config):
                         if "battery" not in val or battery_level != val["battery"]:
                             if (
                                 "battery" not in val
-                                or val["battery"] > _config["battery_low"]
-                            ) and battery_level <= _config["battery_low"]:
+                                or val["battery"] > config["battery_low"]
+                            ) and battery_level <= config["battery_low"]:
                                 ret.append(
                                     {
                                         "device": device,
@@ -149,13 +148,13 @@ def beacon(config):
     # Find missing devices and remove them / send an event
     for device in last_state_devices:
         if device not in found_devices:
-            if "missing" in _config["states"]:
+            if "missing" in config["states"]:
                 ret.append({"device": device, "state": "missing", "tag": "missing"})
 
             del last_state[device]
 
     # Maybe send an event if we don't have any devices
-    if "no_devices_event" in _config and _config["no_devices_event"] is True:
+    if "no_devices_event" in config and config["no_devices_event"] is True:
         if not found_devices and not last_state_extra["no_devices"]:
             ret.append({"tag": "no_devices"})
 
diff --git a/salt/beacons/avahi_announce.py b/salt/beacons/avahi_announce.py
index a3f13d40c87f..9e9a98f00e46 100644
--- a/salt/beacons/avahi_announce.py
+++ b/salt/beacons/avahi_announce.py
@@ -14,6 +14,7 @@
 import logging
 import time
 
+import salt.utils.beacons
 import salt.utils.stringutils
 
 try:
@@ -69,8 +70,8 @@ def validate(config):
     """
     Validate the beacon configuration
     """
-    _config = {}
-    list(map(_config.update, config))
+
+    _config = salt.utils.beacons.list_to_dict(config)
 
     if not isinstance(config, list):
         return False, ("Configuration for avahi_announce beacon must be a list.")
@@ -166,28 +167,27 @@ def beacon(config):
 
     global LAST_GRAINS
 
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
-    if "servicename" in _config:
-        servicename = _config["servicename"]
+    if "servicename" in config:
+        servicename = config["servicename"]
     else:
         servicename = __grains__["host"]
         # Check for hostname change
         if LAST_GRAINS and LAST_GRAINS["host"] != servicename:
             changes["servicename"] = servicename
 
-    if LAST_GRAINS and _config.get("reset_on_change", False):
+    if LAST_GRAINS and config.get("reset_on_change", False):
         # Check for IP address change in the case when we reset on change
         if LAST_GRAINS.get("ipv4", []) != __grains__.get("ipv4", []):
             changes["ipv4"] = __grains__.get("ipv4", [])
         if LAST_GRAINS.get("ipv6", []) != __grains__.get("ipv6", []):
             changes["ipv6"] = __grains__.get("ipv6", [])
 
-    for item in _config["txt"]:
+    for item in config["txt"]:
         changes_key = "txt." + salt.utils.stringutils.to_unicode(item)
-        if _config["txt"][item].startswith("grains."):
-            grain = _config["txt"][item][7:]
+        if config["txt"][item].startswith("grains."):
+            grain = config["txt"][item][7:]
             grain_index = None
             square_bracket = grain.find("[")
             if square_bracket != -1 and grain[-1] == "]":
@@ -206,7 +206,7 @@ def beacon(config):
             ):
                 changes[changes_key] = txt[item]
         else:
-            txt[item] = _enforce_txt_record_maxlen(item, _config["txt"][item])
+            txt[item] = _enforce_txt_record_maxlen(item, config["txt"][item])
 
         if not LAST_GRAINS:
             changes[changes_key] = txt[item]
@@ -214,8 +214,8 @@ def beacon(config):
     if changes:
         if not LAST_GRAINS:
             changes["servicename"] = servicename
-            changes["servicetype"] = _config["servicetype"]
-            changes["port"] = _config["port"]
+            changes["servicetype"] = config["servicetype"]
+            changes["port"] = config["port"]
             changes["ipv4"] = __grains__.get("ipv4", [])
             changes["ipv6"] = __grains__.get("ipv6", [])
             GROUP.AddService(
@@ -223,18 +223,18 @@ def beacon(config):
                 avahi.PROTO_UNSPEC,
                 dbus.UInt32(0),
                 servicename,
-                _config["servicetype"],
+                config["servicetype"],
                 "",
                 "",
-                dbus.UInt16(_config["port"]),
+                dbus.UInt16(config["port"]),
                 avahi.dict_to_txt_array(txt),
             )
             GROUP.Commit()
-        elif _config.get("reset_on_change", False) or "servicename" in changes:
+        elif config.get("reset_on_change", False) or "servicename" in changes:
             # A change in 'servicename' requires a reset because we can only
             # directly update TXT records
             GROUP.Reset()
-            reset_wait = _config.get("reset_wait", 0)
+            reset_wait = config.get("reset_wait", 0)
             if reset_wait > 0:
                 time.sleep(reset_wait)
             GROUP.AddService(
@@ -242,10 +242,10 @@ def beacon(config):
                 avahi.PROTO_UNSPEC,
                 dbus.UInt32(0),
                 servicename,
-                _config["servicetype"],
+                config["servicetype"],
                 "",
                 "",
-                dbus.UInt16(_config["port"]),
+                dbus.UInt16(config["port"]),
                 avahi.dict_to_txt_array(txt),
             )
             GROUP.Commit()
@@ -255,14 +255,14 @@ def beacon(config):
                 avahi.PROTO_UNSPEC,
                 dbus.UInt32(0),
                 servicename,
-                _config["servicetype"],
+                config["servicetype"],
                 "",
                 avahi.dict_to_txt_array(txt),
             )
 
         ret.append({"tag": "result", "changes": changes})
 
-    if _config.get("copy_grains", False):
+    if config.get("copy_grains", False):
         LAST_GRAINS = __grains__.copy()
     else:
         LAST_GRAINS = __grains__
diff --git a/salt/beacons/bonjour_announce.py b/salt/beacons/bonjour_announce.py
index beba3b80e089..27141deb85fb 100644
--- a/salt/beacons/bonjour_announce.py
+++ b/salt/beacons/bonjour_announce.py
@@ -51,8 +51,7 @@ def validate(config):
     """
     Validate the beacon configuration
     """
-    _config = {}
-    list(map(_config.update, config))
+    _config = salt.utils.beacons.list_to_dict(config)
 
     if not isinstance(config, list):
         return False, ("Configuration for bonjour_announce beacon must be a list.")
@@ -149,28 +148,27 @@ def beacon(config):
     global LAST_GRAINS
     global SD_REF
 
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
-    if "servicename" in _config:
-        servicename = _config["servicename"]
+    if "servicename" in config:
+        servicename = config["servicename"]
     else:
         servicename = __grains__["host"]
         # Check for hostname change
         if LAST_GRAINS and LAST_GRAINS["host"] != servicename:
             changes["servicename"] = servicename
 
-    if LAST_GRAINS and _config.get("reset_on_change", False):
+    if LAST_GRAINS and config.get("reset_on_change", False):
         # Check for IP address change in the case when we reset on change
         if LAST_GRAINS.get("ipv4", []) != __grains__.get("ipv4", []):
             changes["ipv4"] = __grains__.get("ipv4", [])
         if LAST_GRAINS.get("ipv6", []) != __grains__.get("ipv6", []):
             changes["ipv6"] = __grains__.get("ipv6", [])
 
-    for item in _config["txt"]:
+    for item in config["txt"]:
         changes_key = "txt." + salt.utils.stringutils.to_unicode(item)
-        if _config["txt"][item].startswith("grains."):
-            grain = _config["txt"][item][7:]
+        if config["txt"][item].startswith("grains."):
+            grain = config["txt"][item][7:]
             grain_index = None
             square_bracket = grain.find("[")
             if square_bracket != -1 and grain[-1] == "]":
@@ -189,7 +187,7 @@ def beacon(config):
             ):
                 changes[changes_key] = txt[item]
         else:
-            txt[item] = _enforce_txt_record_maxlen(item, _config["txt"][item])
+            txt[item] = _enforce_txt_record_maxlen(item, config["txt"][item])
 
         if not LAST_GRAINS:
             changes[changes_key] = txt[item]
@@ -198,14 +196,14 @@ def beacon(config):
         txt_record = pybonjour.TXTRecord(items=txt)
         if not LAST_GRAINS:
             changes["servicename"] = servicename
-            changes["servicetype"] = _config["servicetype"]
-            changes["port"] = _config["port"]
+            changes["servicetype"] = config["servicetype"]
+            changes["port"] = config["port"]
             changes["ipv4"] = __grains__.get("ipv4", [])
             changes["ipv6"] = __grains__.get("ipv6", [])
             SD_REF = pybonjour.DNSServiceRegister(
                 name=servicename,
-                regtype=_config["servicetype"],
-                port=_config["port"],
+                regtype=config["servicetype"],
+                port=config["port"],
                 txtRecord=txt_record,
                 callBack=_register_callback,
             )
@@ -213,18 +211,18 @@ def beacon(config):
             ready = select.select([SD_REF], [], [])
             if SD_REF in ready[0]:
                 pybonjour.DNSServiceProcessResult(SD_REF)
-        elif _config.get("reset_on_change", False) or "servicename" in changes:
+        elif config.get("reset_on_change", False) or "servicename" in changes:
             # A change in 'servicename' requires a reset because we can only
             # directly update TXT records
             SD_REF.close()
             SD_REF = None
-            reset_wait = _config.get("reset_wait", 0)
+            reset_wait = config.get("reset_wait", 0)
             if reset_wait > 0:
                 time.sleep(reset_wait)
             SD_REF = pybonjour.DNSServiceRegister(
                 name=servicename,
-                regtype=_config["servicetype"],
-                port=_config["port"],
+                regtype=config["servicetype"],
+                port=config["port"],
                 txtRecord=txt_record,
                 callBack=_register_callback,
             )
@@ -239,7 +237,7 @@ def beacon(config):
 
         ret.append({"tag": "result", "changes": changes})
 
-    if _config.get("copy_grains", False):
+    if config.get("copy_grains", False):
         LAST_GRAINS = __grains__.copy()
     else:
         LAST_GRAINS = __grains__
diff --git a/salt/beacons/btmp.py b/salt/beacons/btmp.py
index 80078877a77a..92d2562e3dca 100644
--- a/salt/beacons/btmp.py
+++ b/salt/beacons/btmp.py
@@ -204,38 +204,37 @@ def validate(config):
         vstatus = False
         vmsg = "Configuration for btmp beacon must " "be a list."
     else:
-        _config = {}
-        list(map(_config.update, config))
+        config = salt.utils.beacons.list_to_dict(config)
 
-        if "users" in _config:
-            if not isinstance(_config["users"], dict):
+        if "users" in config:
+            if not isinstance(config["users"], dict):
                 vstatus = False
                 vmsg = "User configuration for btmp beacon must " "be a dictionary."
             else:
-                for user in _config["users"]:
-                    _time_range = _config["users"][user].get("time_range", {})
+                for user in config["users"]:
+                    _time_range = config["users"][user].get("time_range", {})
                     vstatus, vmsg = _validate_time_range(_time_range, vstatus, vmsg)
 
             if not vstatus:
                 return vstatus, vmsg
 
-        if "groups" in _config:
-            if not isinstance(_config["groups"], dict):
+        if "groups" in config:
+            if not isinstance(config["groups"], dict):
                 vstatus = False
                 vmsg = "Group configuration for btmp beacon must " "be a dictionary."
             else:
-                for group in _config["groups"]:
-                    _time_range = _config["groups"][group].get("time_range", {})
+                for group in config["groups"]:
+                    _time_range = config["groups"][group].get("time_range", {})
                     vstatus, vmsg = _validate_time_range(_time_range, vstatus, vmsg)
             if not vstatus:
                 return vstatus, vmsg
 
-        if "defaults" in _config:
-            if not isinstance(_config["defaults"], dict):
+        if "defaults" in config:
+            if not isinstance(config["defaults"], dict):
                 vstatus = False
                 vmsg = "Defaults configuration for btmp beacon must " "be a dictionary."
             else:
-                _time_range = _config["defaults"].get("time_range", {})
+                _time_range = config["defaults"].get("time_range", {})
                 vstatus, vmsg = _validate_time_range(_time_range, vstatus, vmsg)
             if not vstatus:
                 return vstatus, vmsg
diff --git a/salt/beacons/cert_info.py b/salt/beacons/cert_info.py
index 86288cd5be9a..7b257e902ccd 100644
--- a/salt/beacons/cert_info.py
+++ b/salt/beacons/cert_info.py
@@ -10,6 +10,7 @@
 import logging
 from datetime import datetime
 
+import salt.utils.beacons
 import salt.utils.files
 
 try:
@@ -37,14 +38,13 @@ def validate(config):
     """
     Validate the beacon configuration
     """
-    _config = {}
-    list(map(_config.update, config))
-
     # Configuration for cert_info beacon should be a list of dicts
     if not isinstance(config, list):
         return False, ("Configuration for cert_info beacon must be a list.")
 
-    if "files" not in _config:
+    config = salt.utils.beacons.list_to_dict(config)
+
+    if "files" not in config:
         return (
             False,
             ("Configuration for cert_info beacon must contain files option."),
@@ -78,12 +78,11 @@ def beacon(config):
     certificates = []
     CryptoError = crypto.Error  # pylint: disable=invalid-name
 
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
-    global_notify_days = _config.get("notify_days", DEFAULT_NOTIFY_DAYS)
+    global_notify_days = config.get("notify_days", DEFAULT_NOTIFY_DAYS)
 
-    for cert_path in _config.get("files", []):
+    for cert_path in config.get("files", []):
         notify_days = global_notify_days
 
         if isinstance(cert_path, dict):
diff --git a/salt/beacons/glxinfo.py b/salt/beacons/glxinfo.py
index ca41d4ae652a..2e6c8674f9c5 100644
--- a/salt/beacons/glxinfo.py
+++ b/salt/beacons/glxinfo.py
@@ -5,6 +5,7 @@
 """
 import logging
 
+import salt.utils.beacons
 import salt.utils.path
 
 log = logging.getLogger(__name__)
@@ -31,10 +32,9 @@ def validate(config):
     if not isinstance(config, list):
         return False, ("Configuration for glxinfo beacon must be a list.")
 
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
-    if "user" not in _config:
+    if "user" not in config:
         return (
             False,
             (
@@ -64,14 +64,13 @@ def beacon(config):
     log.trace("glxinfo beacon starting")
     ret = []
 
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
     retcode = __salt__["cmd.retcode"](
-        "DISPLAY=:0 glxinfo", runas=_config["user"], python_shell=True
+        "DISPLAY=:0 glxinfo", runas=config["user"], python_shell=True
     )
 
-    if "screen_event" in _config and _config["screen_event"]:
+    if "screen_event" in config and config["screen_event"]:
         last_value = last_state.get("screen_available", False)
         screen_available = retcode == 0
         if last_value != screen_available or "screen_available" not in last_state:
diff --git a/salt/beacons/haproxy.py b/salt/beacons/haproxy.py
index 35e2377914bf..bbd5447e658b 100644
--- a/salt/beacons/haproxy.py
+++ b/salt/beacons/haproxy.py
@@ -6,6 +6,8 @@
 """
 import logging
 
+import salt.utils.beacons
+
 log = logging.getLogger(__name__)
 
 __virtualname__ = "haproxy"
@@ -29,24 +31,23 @@ def validate(config):
     if not isinstance(config, list):
         return False, ("Configuration for haproxy beacon must be a list.")
     else:
-        _config = {}
-        list(map(_config.update, config))
+        config = salt.utils.beacons.list_to_dict(config)
 
-        if "backends" not in _config:
+        if "backends" not in config:
             return False, ("Configuration for haproxy beacon requires backends.")
         else:
-            if not isinstance(_config["backends"], dict):
+            if not isinstance(config["backends"], dict):
                 return False, ("Backends for haproxy beacon must be a dictionary.")
             else:
-                for backend in _config["backends"]:
-                    log.debug("_config %s", _config["backends"][backend])
-                    if "servers" not in _config["backends"][backend]:
+                for backend in config["backends"]:
+                    log.debug("config %s", config["backends"][backend])
+                    if "servers" not in config["backends"][backend]:
                         return (
                             False,
                             ("Backends for haproxy beacon require servers."),
                         )
                     else:
-                        _servers = _config["backends"][backend]["servers"]
+                        _servers = config["backends"][backend]["servers"]
                         if not isinstance(_servers, list):
                             return (
                                 False,
@@ -74,11 +75,10 @@ def beacon(config):
     """
     ret = []
 
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
-    for backend in _config.get("backends", ()):
-        backend_config = _config["backends"][backend]
+    for backend in config.get("backends", ()):
+        backend_config = config["backends"][backend]
         threshold = backend_config["threshold"]
         for server in backend_config["servers"]:
             scur = __salt__["haproxy.get_sessions"](server, backend)
diff --git a/salt/beacons/inotify.py b/salt/beacons/inotify.py
index 4d9ffbeb8730..4739df10b42f 100644
--- a/salt/beacons/inotify.py
+++ b/salt/beacons/inotify.py
@@ -109,13 +109,12 @@ def validate(config):
     if not isinstance(config, list):
         return False, "Configuration for inotify beacon must be a list."
     else:
-        _config = {}
-        list(map(_config.update, config))
+        config = salt.utils.beacons.list_to_dict(config)
 
-        if "files" not in _config:
+        if "files" not in config:
             return False, "Configuration for inotify beacon must include files."
         else:
-            if not isinstance(_config["files"], dict):
+            if not isinstance(config["files"], dict):
                 return (
                     False,
                     (
@@ -124,9 +123,9 @@ def validate(config):
                     ),
                 )
 
-            for path in _config.get("files"):
+            for path in config.get("files"):
 
-                if not isinstance(_config["files"][path], dict):
+                if not isinstance(config["files"][path], dict):
                     return (
                         False,
                         (
@@ -137,7 +136,7 @@ def validate(config):
                 else:
                     if not any(
                         j in ["mask", "recurse", "auto_add"]
-                        for j in _config["files"][path]
+                        for j in config["files"][path]
                     ):
                         return (
                             False,
@@ -147,8 +146,8 @@ def validate(config):
                             ),
                         )
 
-                    if "auto_add" in _config["files"][path]:
-                        if not isinstance(_config["files"][path]["auto_add"], bool):
+                    if "auto_add" in config["files"][path]:
+                        if not isinstance(config["files"][path]["auto_add"], bool):
                             return (
                                 False,
                                 (
@@ -157,8 +156,8 @@ def validate(config):
                                 ),
                             )
 
-                    if "recurse" in _config["files"][path]:
-                        if not isinstance(_config["files"][path]["recurse"], bool):
+                    if "recurse" in config["files"][path]:
+                        if not isinstance(config["files"][path]["recurse"], bool):
                             return (
                                 False,
                                 (
@@ -167,8 +166,8 @@ def validate(config):
                                 ),
                             )
 
-                    if "mask" in _config["files"][path]:
-                        if not isinstance(_config["files"][path]["mask"], list):
+                    if "mask" in config["files"][path]:
+                        if not isinstance(config["files"][path]["mask"], list):
                             return (
                                 False,
                                 (
@@ -176,7 +175,7 @@ def validate(config):
                                     "mask must be list."
                                 ),
                             )
-                        for mask in _config["files"][path]["mask"]:
+                        for mask in config["files"][path]["mask"]:
                             if mask not in VALID_MASK:
                                 return (
                                     False,
@@ -257,11 +256,10 @@ def beacon(config):
     whitelist = ["_beacon_name"]
     config = salt.utils.beacons.remove_hidden_options(config, whitelist)
 
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
     ret = []
-    notifier = _get_notifier(_config)
+    notifier = _get_notifier(config)
     wm = notifier._watch_manager
 
     # Read in existing events
@@ -276,11 +274,11 @@ def beacon(config):
             # Find the matching path in config
             path = event.path
             while path != "/":
-                if path in _config.get("files", {}):
+                if path in config.get("files", {}):
                     break
                 path = os.path.dirname(path)
 
-            excludes = _config["files"].get(path, {}).get("exclude", "")
+            excludes = config["files"].get(path, {}).get("exclude", "")
 
             if excludes and isinstance(excludes, list):
                 for exclude in excludes:
@@ -318,10 +316,10 @@ def beacon(config):
 
     # Update existing watches and add new ones
     # TODO: make the config handle more options
-    for path in _config.get("files", ()):
+    for path in config.get("files", ()):
 
-        if isinstance(_config["files"][path], dict):
-            mask = _config["files"][path].get("mask", DEFAULT_MASK)
+        if isinstance(config["files"][path], dict):
+            mask = config["files"][path].get("mask", DEFAULT_MASK)
             if isinstance(mask, list):
                 r_mask = 0
                 for sub in mask:
@@ -331,8 +329,8 @@ def beacon(config):
             else:
                 r_mask = mask
             mask = r_mask
-            rec = _config["files"][path].get("recurse", False)
-            auto_add = _config["files"][path].get("auto_add", False)
+            rec = config["files"][path].get("recurse", False)
+            auto_add = config["files"][path].get("auto_add", False)
         else:
             mask = DEFAULT_MASK
             rec = False
@@ -349,7 +347,7 @@ def beacon(config):
                     if update:
                         wm.update_watch(wd, mask=mask, rec=rec, auto_add=auto_add)
         elif os.path.exists(path):
-            excludes = _config["files"][path].get("exclude", "")
+            excludes = config["files"][path].get("exclude", "")
             excl = None
             if isinstance(excludes, list):
                 excl = []
@@ -367,6 +365,7 @@ def beacon(config):
 
 
 def close(config):
+    config = salt.utils.beacons.list_to_dict(config)
     beacon_name = config.get("_beacon_name", "inotify")
     notifier = "{}.notifier".format(beacon_name)
     if notifier in __context__:
diff --git a/salt/beacons/journald.py b/salt/beacons/journald.py
index d0f211b715ff..f7dce2acabb1 100644
--- a/salt/beacons/journald.py
+++ b/salt/beacons/journald.py
@@ -3,6 +3,7 @@
 """
 import logging
 
+import salt.utils.beacons
 import salt.utils.data
 
 try:
@@ -45,11 +46,10 @@ def validate(config):
     if not isinstance(config, list):
         return (False, "Configuration for journald beacon must be a list.")
     else:
-        _config = {}
-        list(map(_config.update, config))
+        config = salt.utils.beacons.list_to_dict(config)
 
-        for name in _config.get("services", {}):
-            if not isinstance(_config["services"][name], dict):
+        for name in config.get("services", {}):
+            if not isinstance(config["services"][name], dict):
                 return (
                     False,
                     (
@@ -79,23 +79,22 @@ def beacon(config):
     ret = []
     journal = _get_journal()
 
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
     while True:
         cur = journal.get_next()
         if not cur:
             break
 
-        for name in _config.get("services", {}):
+        for name in config.get("services", {}):
             n_flag = 0
-            for key in _config["services"][name]:
+            for key in config["services"][name]:
                 if isinstance(key, str):
                     key = salt.utils.data.decode(key)
                 if key in cur:
-                    if _config["services"][name][key] == cur[key]:
+                    if config["services"][name][key] == cur[key]:
                         n_flag += 1
-            if n_flag == len(_config["services"][name]):
+            if n_flag == len(config["services"][name]):
                 # Match!
                 sub = salt.utils.data.simple_types_filter(cur)
                 sub.update({"tag": name})
diff --git a/salt/beacons/load.py b/salt/beacons/load.py
index 245c3856e710..97b74b0c1cd1 100644
--- a/salt/beacons/load.py
+++ b/salt/beacons/load.py
@@ -4,6 +4,7 @@
 import logging
 import os
 
+import salt.utils.beacons
 import salt.utils.platform
 
 log = logging.getLogger(__name__)
@@ -29,11 +30,10 @@ def validate(config):
     if not isinstance(config, list):
         return False, ("Configuration for load beacon must be a list.")
     else:
-        _config = {}
-        list(map(_config.update, config))
+        config = salt.utils.beacons.list_to_dict(config)
 
-        if "emitatstartup" in _config:
-            if not isinstance(_config["emitatstartup"], bool):
+        if "emitatstartup" in config:
+            if not isinstance(config["emitatstartup"], bool):
                 return (
                     False,
                     (
@@ -42,8 +42,8 @@ def validate(config):
                     ),
                 )
 
-        if "onchangeonly" in _config:
-            if not isinstance(_config["onchangeonly"], bool):
+        if "onchangeonly" in config:
+            if not isinstance(config["onchangeonly"], bool):
                 return (
                     False,
                     (
@@ -52,11 +52,11 @@ def validate(config):
                     ),
                 )
 
-        if "averages" not in _config:
+        if "averages" not in config:
             return False, ("Averages configuration is required for load beacon.")
         else:
 
-            if not any(j in ["1m", "5m", "15m"] for j in _config.get("averages", {})):
+            if not any(j in ["1m", "5m", "15m"] for j in config.get("averages", {})):
                 return (
                     False,
                     (
@@ -66,7 +66,7 @@ def validate(config):
                 )
 
             for item in ["1m", "5m", "15m"]:
-                if not isinstance(_config["averages"][item], list):
+                if not isinstance(config["averages"][item], list):
                     return (
                         False,
                         (
@@ -76,7 +76,7 @@ def validate(config):
                         ),
                     )
                 else:
-                    if len(_config["averages"][item]) != 2:
+                    if len(config["averages"][item]) != 2:
                         return (
                             False,
                             (
@@ -125,25 +125,24 @@ def beacon(config):
     """
     log.trace("load beacon starting")
 
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
     # Default config if not present
-    if "emitatstartup" not in _config:
-        _config["emitatstartup"] = True
-    if "onchangeonly" not in _config:
-        _config["onchangeonly"] = False
+    if "emitatstartup" not in config:
+        config["emitatstartup"] = True
+    if "onchangeonly" not in config:
+        config["onchangeonly"] = False
 
     ret = []
     avgs = os.getloadavg()
     avg_keys = ["1m", "5m", "15m"]
     avg_dict = dict(zip(avg_keys, avgs))
 
-    if _config["onchangeonly"]:
+    if config["onchangeonly"]:
         if not LAST_STATUS:
             for k in ["1m", "5m", "15m"]:
                 LAST_STATUS[k] = avg_dict[k]
-            if not _config["emitatstartup"]:
+            if not config["emitatstartup"]:
                 log.debug("Don't emit because emitatstartup is False")
                 return ret
 
@@ -151,49 +150,49 @@ def beacon(config):
 
     # Check each entry for threshold
     for k in ["1m", "5m", "15m"]:
-        if k in _config.get("averages", {}):
-            if _config["onchangeonly"]:
+        if k in config.get("averages", {}):
+            if config["onchangeonly"]:
                 # Emit if current is more that threshold and old value less
                 # that threshold
-                if float(avg_dict[k]) > float(_config["averages"][k][1]) and float(
+                if float(avg_dict[k]) > float(config["averages"][k][1]) and float(
                     LAST_STATUS[k]
-                ) < float(_config["averages"][k][1]):
+                ) < float(config["averages"][k][1]):
                     log.debug(
                         "Emit because %f > %f and last was " "%f",
                         float(avg_dict[k]),
-                        float(_config["averages"][k][1]),
+                        float(config["averages"][k][1]),
                         float(LAST_STATUS[k]),
                     )
                     send_beacon = True
                     break
                 # Emit if current is less that threshold and old value more
                 # that threshold
-                if float(avg_dict[k]) < float(_config["averages"][k][0]) and float(
+                if float(avg_dict[k]) < float(config["averages"][k][0]) and float(
                     LAST_STATUS[k]
-                ) > float(_config["averages"][k][0]):
+                ) > float(config["averages"][k][0]):
                     log.debug(
                         "Emit because %f < %f and last was" "%f",
                         float(avg_dict[k]),
-                        float(_config["averages"][k][0]),
+                        float(config["averages"][k][0]),
                         float(LAST_STATUS[k]),
                     )
                     send_beacon = True
                     break
             else:
                 # Emit no matter LAST_STATUS
-                if float(avg_dict[k]) < float(_config["averages"][k][0]) or float(
+                if float(avg_dict[k]) < float(config["averages"][k][0]) or float(
                     avg_dict[k]
-                ) > float(_config["averages"][k][1]):
+                ) > float(config["averages"][k][1]):
                     log.debug(
                         "Emit because %f < %f or > " "%f",
                         float(avg_dict[k]),
-                        float(_config["averages"][k][0]),
-                        float(_config["averages"][k][1]),
+                        float(config["averages"][k][0]),
+                        float(config["averages"][k][1]),
                     )
                     send_beacon = True
                     break
 
-    if _config["onchangeonly"]:
+    if config["onchangeonly"]:
         for k in ["1m", "5m", "15m"]:
             LAST_STATUS[k] = avg_dict[k]
 
diff --git a/salt/beacons/log_beacon.py b/salt/beacons/log_beacon.py
index c3efafa0a94f..9aeee9ec4e1a 100644
--- a/salt/beacons/log_beacon.py
+++ b/salt/beacons/log_beacon.py
@@ -6,6 +6,7 @@
 """
 import logging
 
+import salt.utils.beacons
 import salt.utils.files
 import salt.utils.platform
 
@@ -47,14 +48,13 @@ def validate(config):
     """
     Validate the beacon configuration
     """
-    _config = {}
-    list(map(_config.update, config))
-
     # Configuration for log beacon should be a list of dicts
     if not isinstance(config, list):
         return False, ("Configuration for log beacon must be a list.")
 
-    if "file" not in _config:
+    config = salt.utils.beacons.list_to_dict(config)
+
+    if "file" not in config:
         return False, ("Configuration for log beacon must contain file option.")
     return True, "Valid beacon configuration"
 
@@ -91,19 +91,18 @@ def beacon(config):
                 goodbye/world: # tag added to beacon event tag.
                   regex: .*good-bye.* # match good-bye string anywhere in the log entry.
     """
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
     ret = []
 
-    if "file" not in _config:
+    if "file" not in config:
         event = SKEL.copy()
         event["tag"] = "global"
         event["error"] = "file not defined in config"
         ret.append(event)
         return ret
 
-    with salt.utils.files.fopen(_config["file"], "r") as fp_:
+    with salt.utils.files.fopen(config["file"], "r") as fp_:
         loc = __context__.get(LOC_KEY, 0)
         if loc == 0:
             fp_.seek(0, 2)
@@ -118,13 +117,13 @@ def beacon(config):
         log.info("txt %s", txt)
 
         d = {}
-        for tag in _config.get("tags", {}):
-            if "regex" not in _config["tags"][tag]:
+        for tag in config.get("tags", {}):
+            if "regex" not in config["tags"][tag]:
                 continue
-            if not _config["tags"][tag]["regex"]:
+            if not config["tags"][tag]["regex"]:
                 continue
             try:
-                d[tag] = re.compile(r"{}".format(_config["tags"][tag]["regex"]))
+                d[tag] = re.compile(r"{}".format(config["tags"][tag]["regex"]))
             except Exception as e:  # pylint: disable=broad-except
                 event = SKEL.copy()
                 event["tag"] = tag
diff --git a/salt/beacons/memusage.py b/salt/beacons/memusage.py
index 71110c178ad5..cc058ace31bb 100644
--- a/salt/beacons/memusage.py
+++ b/salt/beacons/memusage.py
@@ -8,6 +8,8 @@
 import logging
 import re
 
+import salt.utils.beacons
+
 try:
     import psutil
 
@@ -35,10 +37,9 @@ def validate(config):
     if not isinstance(config, list):
         return False, ("Configuration for memusage beacon must be a list.")
     else:
-        _config = {}
-        list(map(_config.update, config))
+        config = salt.utils.beacons.list_to_dict(config)
 
-        if "percent" not in _config:
+        if "percent" not in config:
             return False, ("Configuration for memusage beacon requires percent.")
 
     return True, "Valid beacon configuration"
@@ -59,13 +60,12 @@ def beacon(config):
     """
     ret = []
 
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
     _current_usage = psutil.virtual_memory()
 
     current_usage = _current_usage.percent
-    monitor_usage = _config["percent"]
+    monitor_usage = config["percent"]
     if "%" in monitor_usage:
         monitor_usage = re.sub("%", "", monitor_usage)
     monitor_usage = float(monitor_usage)
diff --git a/salt/beacons/network_info.py b/salt/beacons/network_info.py
index b49ff04c5b75..e0a3b96fdaf4 100644
--- a/salt/beacons/network_info.py
+++ b/salt/beacons/network_info.py
@@ -5,6 +5,8 @@
 """
 import logging
 
+import salt.utils.beacons
+
 try:
     import salt.utils.psutil_compat as psutil
 
@@ -69,11 +71,10 @@ def validate(config):
         return False, ("Configuration for network_info beacon must be a list.")
     else:
 
-        _config = {}
-        list(map(_config.update, config))
+        config = salt.utils.beacons.list_to_dict(config)
 
-        for item in _config.get("interfaces", {}):
-            if not isinstance(_config["interfaces"][item], dict):
+        for item in config.get("interfaces", {}):
+            if not isinstance(config["interfaces"][item], dict):
                 return (
                     False,
                     (
@@ -82,7 +83,7 @@ def validate(config):
                     ),
                 )
             else:
-                if not any(j in VALID_ITEMS for j in _config["interfaces"][item]):
+                if not any(j in VALID_ITEMS for j in config["interfaces"][item]):
                     return (
                         False,
                         ("Invalid configuration item in Beacon configuration."),
@@ -140,17 +141,16 @@ def beacon(config):
     """
     ret = []
 
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
     log.debug("psutil.net_io_counters %s", psutil.net_io_counters)
 
     _stats = psutil.net_io_counters(pernic=True)
 
     log.debug("_stats %s", _stats)
-    for interface in _config.get("interfaces", {}):
+    for interface in config.get("interfaces", {}):
         if interface in _stats:
-            interface_config = _config["interfaces"][interface]
+            interface_config = config["interfaces"][interface]
             _if_stats = _stats[interface]
             _diff = False
             for attr in __attrs:
diff --git a/salt/beacons/network_settings.py b/salt/beacons/network_settings.py
index 55a3a93aa2e0..d2855535d028 100644
--- a/salt/beacons/network_settings.py
+++ b/salt/beacons/network_settings.py
@@ -10,6 +10,7 @@
 import re
 
 import salt.loader
+import salt.utils.beacons
 
 try:
     from pyroute2 import IPDB
@@ -77,10 +78,9 @@ def validate(config):
     if not isinstance(config, list):
         return False, ("Configuration for network_settings beacon must be a list.")
     else:
-        _config = {}
-        list(map(_config.update, config))
+        config = salt.utils.beacons.list_to_dict(config)
 
-        interfaces = _config.get("interfaces", {})
+        interfaces = config.get("interfaces", {})
         if isinstance(interfaces, list):
             # Old syntax
             return (
@@ -92,7 +92,7 @@ def validate(config):
             )
 
         for item in interfaces:
-            if not isinstance(_config["interfaces"][item], dict):
+            if not isinstance(config["interfaces"][item], dict):
                 return (
                     False,
                     (
@@ -100,7 +100,7 @@ def validate(config):
                         " must be a dictionary."
                     ),
                 )
-            if not all(j in ATTRS for j in _config["interfaces"][item]):
+            if not all(j in ATTRS for j in config["interfaces"][item]):
                 return False, ("Invalid attributes in beacon configuration.")
     return True, "Valid beacon configuration"
 
@@ -166,8 +166,7 @@ def beacon(config):
                   promiscuity:
 
     """
-    _config = {}
-    list(map(_config.update, config))
+    _config = salt.utils.beacons.list_to_dict(config)
 
     ret = []
     interfaces = []
@@ -205,8 +204,8 @@ def beacon(config):
     if expanded_config:
         _config["interfaces"].update(expanded_config["interfaces"])
 
-        # config updated so update _config
-        list(map(_config.update, config))
+        # config updated so update config
+        _config = salt.utils.beacons.list_to_dict(config)
 
     log.debug("interfaces %s", interfaces)
     for interface in interfaces:
diff --git a/salt/beacons/proxy_example.py b/salt/beacons/proxy_example.py
index b6dc69b3d6f3..a7cd3072ec1f 100644
--- a/salt/beacons/proxy_example.py
+++ b/salt/beacons/proxy_example.py
@@ -9,6 +9,7 @@
 """
 import logging
 
+import salt.utils.beacons
 import salt.utils.http
 
 # Important: If used with salt-proxy
@@ -54,9 +55,8 @@ def beacon(config):
     # please be advised that doing CPU or IO intensive
     # operations in this method will cause the beacon loop
     # to block.
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
-    beacon_url = "{}{}".format(__opts__["proxy"]["url"], _config["endpoint"])
+    beacon_url = "{}{}".format(__opts__["proxy"]["url"], config["endpoint"])
     ret = salt.utils.http.query(beacon_url, decode_type="json", decode=True)
     return [ret["dict"]]
diff --git a/salt/beacons/ps.py b/salt/beacons/ps.py
index cbb8d28d2585..d2e1c7932333 100644
--- a/salt/beacons/ps.py
+++ b/salt/beacons/ps.py
@@ -3,6 +3,8 @@
 """
 import logging
 
+import salt.utils.beacons
+
 try:
     import salt.utils.psutil_compat as psutil
 
@@ -30,13 +32,12 @@ def validate(config):
     if not isinstance(config, list):
         return False, ("Configuration for ps beacon must be a list.")
     else:
-        _config = {}
-        list(map(_config.update, config))
+        config = salt.utils.beacons.list_to_dict(config)
 
-        if "processes" not in _config:
+        if "processes" not in config:
             return False, ("Configuration for ps beacon requires processes.")
         else:
-            if not isinstance(_config["processes"], dict):
+            if not isinstance(config["processes"], dict):
                 return False, ("Processes for ps beacon must be a dictionary.")
 
     return True, "Valid beacon configuration"
@@ -70,16 +71,15 @@ def beacon(config):
         if _name not in procs:
             procs.append(_name)
 
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
-    for process in _config.get("processes", {}):
+    for process in config.get("processes", {}):
         ret_dict = {}
-        if _config["processes"][process] == "running":
+        if config["processes"][process] == "running":
             if process in procs:
                 ret_dict[process] = "Running"
                 ret.append(ret_dict)
-        elif _config["processes"][process] == "stopped":
+        elif config["processes"][process] == "stopped":
             if process not in procs:
                 ret_dict[process] = "Stopped"
                 ret.append(ret_dict)
diff --git a/salt/beacons/salt_monitor.py b/salt/beacons/salt_monitor.py
index b049e56dc191..e40e0b6c6b50 100644
--- a/salt/beacons/salt_monitor.py
+++ b/salt/beacons/salt_monitor.py
@@ -24,6 +24,7 @@
         - test.ping
         - interval: 3600 # seconds
 """
+import salt.utils.beacons
 
 
 def _parse_args(args_kwargs_dict):
@@ -38,15 +39,14 @@ def _parse_args(args_kwargs_dict):
 
 
 def validate(config):
-    _config = {}
-    list(map(_config.update, config))
-    if isinstance(_config["salt_fun"], str):
+    config = salt.utils.beacons.list_to_dict(config)
+    if isinstance(config["salt_fun"], str):
         # a simple str is taking as the single function with no args / kwargs
-        fun = _config["salt_fun"]
+        fun = config["salt_fun"]
         if fun not in __salt__:
             return False, "{} not in __salt__".format(fun)
     else:
-        for entry in _config["salt_fun"]:
+        for entry in config["salt_fun"]:
             if isinstance(entry, dict):
                 # check dict is of correct form
                 fun, args_kwargs_dict = next(iter(entry.items()))
@@ -88,16 +88,15 @@ def validate(config):
 
 def beacon(config):
     events = []
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
-    if isinstance(_config["salt_fun"], str):
+    if isinstance(config["salt_fun"], str):
         # support for single salt_fun with no args / kwargs supplied as str
-        fun = _config["salt_fun"]
+        fun = config["salt_fun"]
         ret = __salt__[fun]()
         return [{"salt_fun": fun, "ret": ret}]
     # else, we should have an iterable
-    for entry in _config["salt_fun"]:
+    for entry in config["salt_fun"]:
         if isinstance(entry, dict):
             fun, args_kwargs_dict = list(entry.items())[0]
             args, kwargs = _parse_args(args_kwargs_dict)
diff --git a/salt/beacons/salt_proxy.py b/salt/beacons/salt_proxy.py
index d9ddd035c200..f70905de76d5 100644
--- a/salt/beacons/salt_proxy.py
+++ b/salt/beacons/salt_proxy.py
@@ -6,6 +6,8 @@
 """
 import logging
 
+import salt.utils.beacons
+
 log = logging.getLogger(__name__)
 
 
@@ -39,13 +41,12 @@ def validate(config):
         return False, ("Configuration for salt_proxy beacon must be a list.")
 
     else:
-        _config = {}
-        list(map(_config.update, config))
+        config = salt.utils.beacons.list_to_dict(config)
 
-        if "proxies" not in _config:
+        if "proxies" not in config:
             return False, ("Configuration for salt_proxy beacon requires proxies.")
         else:
-            if not isinstance(_config["proxies"], dict):
+            if not isinstance(config["proxies"], dict):
                 return False, ("Proxies for salt_proxy beacon must be a dictionary.")
     return True, "Valid beacon configuration"
 
@@ -64,7 +65,6 @@ def beacon(config):
     """
     log.trace("salt proxy beacon called")
 
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
-    return _run_proxy_processes(_config["proxies"])
+    return _run_proxy_processes(config["proxies"])
diff --git a/salt/beacons/sensehat.py b/salt/beacons/sensehat.py
index 4edb04782691..ae7e05eef5ac 100644
--- a/salt/beacons/sensehat.py
+++ b/salt/beacons/sensehat.py
@@ -11,6 +11,8 @@
 import logging
 import re
 
+import salt.utils.beacons
+
 log = logging.getLogger(__name__)
 
 
@@ -26,10 +28,9 @@ def validate(config):
     if not isinstance(config, list):
         return False, ("Configuration for sensehat beacon must be a list.")
     else:
-        _config = {}
-        list(map(_config.update, config))
+        config = salt.utils.beacons.list_to_dict(config)
 
-        if "sensors" not in _config:
+        if "sensors" not in config:
             return False, ("Configuration for sensehat beacon requires sensors.")
 
     return True, "Valid beacon configuration"
@@ -62,16 +63,15 @@ def beacon(config):
     ret = []
     min_default = {"humidity": "0", "pressure": "0", "temperature": "-273.15"}
 
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
-    for sensor in _config.get("sensors", {}):
+    for sensor in config.get("sensors", {}):
         sensor_function = "sensehat.get_{}".format(sensor)
         if sensor_function not in __salt__:
             log.error("No sensor for meassuring %s. Skipping.", sensor)
             continue
 
-        sensor_config = _config["sensors"][sensor]
+        sensor_config = config["sensors"][sensor]
         if isinstance(sensor_config, list):
             sensor_min = str(sensor_config[0])
             sensor_max = str(sensor_config[1])
diff --git a/salt/beacons/service.py b/salt/beacons/service.py
index 5092a302b961..f8281ff53165 100644
--- a/salt/beacons/service.py
+++ b/salt/beacons/service.py
@@ -5,6 +5,8 @@
 import os
 import time
 
+import salt.utils.beacons
+
 log = logging.getLogger(__name__)
 
 LAST_STATUS = {}
@@ -20,13 +22,12 @@ def validate(config):
     if not isinstance(config, list):
         return False, ("Configuration for service beacon must be a list.")
     else:
-        _config = {}
-        list(map(_config.update, config))
+        config = salt.utils.beacons.list_to_dict(config)
 
-        if "services" not in _config:
+        if "services" not in config:
             return False, ("Configuration for service beacon requires services.")
         else:
-            if not isinstance(_config["services"], dict):
+            if not isinstance(config["services"], dict):
                 return (
                     False,
                     (
@@ -34,8 +35,8 @@ def validate(config):
                         "be a dictionary."
                     ),
                 )
-            for config_item in _config["services"]:
-                if not isinstance(_config["services"][config_item], dict):
+            for config_item in config["services"]:
+                if not isinstance(config["services"][config_item], dict):
                     return (
                         False,
                         (
@@ -109,13 +110,12 @@ def beacon(config):
                   uncleanshutdown: /run/nginx.pid
     """
     ret = []
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
-    for service in _config.get("services", {}):
+    for service in config.get("services", {}):
         ret_dict = {}
 
-        service_config = _config["services"][service]
+        service_config = config["services"][service]
 
         ret_dict[service] = {"running": __salt__["service.status"](service)}
         ret_dict["service_name"] = service
diff --git a/salt/beacons/smartos_imgadm.py b/salt/beacons/smartos_imgadm.py
index d2b23c11b5bf..f29bd8ff223f 100644
--- a/salt/beacons/smartos_imgadm.py
+++ b/salt/beacons/smartos_imgadm.py
@@ -19,6 +19,8 @@
 """
 import logging
 
+import salt.utils.beacons
+
 __virtualname__ = "imgadm"
 
 IMGADM_STATE = {
@@ -71,10 +73,9 @@ def beacon(config):
     if IMGADM_STATE["first_run"]:
         log.info("Applying configuration for imgadm beacon")
 
-        _config = {}
-        list(map(_config.update, config))
+        config = salt.utils.beacons.list_to_dict(config)
 
-        if "startup_import_event" not in _config or not _config["startup_import_event"]:
+        if "startup_import_event" not in config or not config["startup_import_event"]:
             IMGADM_STATE["images"] = current_images
 
     # NOTE: import events
diff --git a/salt/beacons/smartos_vmadm.py b/salt/beacons/smartos_vmadm.py
index 155b5b81071c..22026635c0cd 100644
--- a/salt/beacons/smartos_vmadm.py
+++ b/salt/beacons/smartos_vmadm.py
@@ -19,6 +19,8 @@
 """
 import logging
 
+import salt.utils.beacons
+
 __virtualname__ = "vmadm"
 
 VMADM_STATE = {
@@ -73,10 +75,9 @@ def beacon(config):
     if VMADM_STATE["first_run"]:
         log.info("Applying configuration for vmadm beacon")
 
-        _config = {}
-        list(map(_config.update, config))
+        config = salt.utils.beacons.list_to_dict(config)
 
-        if "startup_create_event" not in _config or not _config["startup_create_event"]:
+        if "startup_create_event" not in config or not config["startup_create_event"]:
             VMADM_STATE["vms"] = current_vms
 
     # NOTE: create events
diff --git a/salt/beacons/swapusage.py b/salt/beacons/swapusage.py
index a4949d9491a0..2a2f40cccd0e 100644
--- a/salt/beacons/swapusage.py
+++ b/salt/beacons/swapusage.py
@@ -8,6 +8,8 @@
 import logging
 import re
 
+import salt.utils.beacons
+
 try:
     import psutil
 
@@ -35,10 +37,9 @@ def validate(config):
     if not isinstance(config, list):
         return False, ("Configuration for swapusage beacon must be a list.")
     else:
-        _config = {}
-        list(map(_config.update, config))
+        config = salt.utils.beacons.list_to_dict(config)
 
-        if "percent" not in _config:
+        if "percent" not in config:
             return False, ("Configuration for swapusage beacon requires percent.")
 
     return True, "Valid beacon configuration"
@@ -59,13 +60,12 @@ def beacon(config):
     """
     ret = []
 
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
     _current_usage = psutil.swap_memory()
 
     current_usage = _current_usage.percent
-    monitor_usage = _config["percent"]
+    monitor_usage = config["percent"]
     if "%" in monitor_usage:
         monitor_usage = re.sub("%", "", monitor_usage)
     monitor_usage = float(monitor_usage)
diff --git a/salt/beacons/telegram_bot_msg.py b/salt/beacons/telegram_bot_msg.py
index da5f9aa10f33..7e868d57ac07 100644
--- a/salt/beacons/telegram_bot_msg.py
+++ b/salt/beacons/telegram_bot_msg.py
@@ -6,6 +6,8 @@
 """
 import logging
 
+import salt.utils.beacons
+
 try:
     import telegram
 
@@ -34,18 +36,17 @@ def validate(config):
     if not isinstance(config, list):
         return False, ("Configuration for telegram_bot_msg beacon must be a list.")
 
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
     if not all(
-        _config.get(required_config) for required_config in ["token", "accept_from"]
+        config.get(required_config) for required_config in ["token", "accept_from"]
     ):
         return (
             False,
             ("Not all required configuration for telegram_bot_msg are set."),
         )
 
-    if not isinstance(_config.get("accept_from"), list):
+    if not isinstance(config.get("accept_from"), list):
         return (
             False,
             (
@@ -73,15 +74,14 @@ def beacon(config):
 
     """
 
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
     log.debug("telegram_bot_msg beacon starting")
     ret = []
     output = {}
     output["msgs"] = []
 
-    bot = telegram.Bot(_config["token"])
+    bot = telegram.Bot(config["token"])
     updates = bot.get_updates(limit=100, timeout=0, network_delay=10)
 
     log.debug("Num updates: %d", len(updates))
@@ -96,7 +96,7 @@ def beacon(config):
         if update.update_id > latest_update_id:
             latest_update_id = update.update_id
 
-        if message.chat.username in _config["accept_from"]:
+        if message.chat.username in config["accept_from"]:
             output["msgs"].append(message.to_dict())
 
     # mark in the server that previous messages are processed
diff --git a/salt/beacons/twilio_txt_msg.py b/salt/beacons/twilio_txt_msg.py
index 72c5170f5ce8..2ce9f72691b6 100644
--- a/salt/beacons/twilio_txt_msg.py
+++ b/salt/beacons/twilio_txt_msg.py
@@ -3,6 +3,8 @@
 """
 import logging
 
+import salt.utils.beacons
+
 try:
     import twilio
 
@@ -36,12 +38,9 @@ def validate(config):
     if not isinstance(config, list):
         return False, ("Configuration for twilio_txt_msg beacon must be a list.")
     else:
-        _config = {}
-        list(map(_config.update, config))
+        config = salt.utils.beacons.list_to_dict(config)
 
-        if not all(
-            x in _config for x in ("account_sid", "auth_token", "twilio_number")
-        ):
+        if not all(x in config for x in ("account_sid", "auth_token", "twilio_number")):
             return (
                 False,
                 (
@@ -70,18 +69,15 @@ def beacon(config):
     """
     log.trace("twilio_txt_msg beacon starting")
 
-    _config = {}
-    list(map(_config.update, config))
+    config = salt.utils.beacons.list_to_dict(config)
 
     ret = []
-    if not all(
-        [_config["account_sid"], _config["auth_token"], _config["twilio_number"]]
-    ):
+    if not all([config["account_sid"], config["auth_token"], config["twilio_number"]]):
         return ret
     output = {}
     output["texts"] = []
-    client = TwilioRestClient(_config["account_sid"], _config["auth_token"])
-    messages = client.messages.list(to=_config["twilio_number"])
+    client = TwilioRestClient(config["account_sid"], config["auth_token"])
+    messages = client.messages.list(to=config["twilio_number"])
     log.trace("Num messages: %d", len(messages))
     if not messages:
         log.trace("Twilio beacon has no texts")
diff --git a/salt/beacons/wtmp.py b/salt/beacons/wtmp.py
index 59feb4e87d53..ca820f225697 100644
--- a/salt/beacons/wtmp.py
+++ b/salt/beacons/wtmp.py
@@ -121,6 +121,7 @@
 import os
 import struct
 
+import salt.utils.beacons
 import salt.utils.files
 import salt.utils.stringutils
 
@@ -233,38 +234,37 @@ def validate(config):
         vstatus = False
         vmsg = "Configuration for wtmp beacon must be a list."
     else:
-        _config = {}
-        list(map(_config.update, config))
+        config = salt.utils.beacons.list_to_dict(config)
 
-        if "users" in _config:
-            if not isinstance(_config["users"], dict):
+        if "users" in config:
+            if not isinstance(config["users"], dict):
                 vstatus = False
                 vmsg = "User configuration for wtmp beacon must " "be a dictionary."
             else:
-                for user in _config["users"]:
-                    _time_range = _config["users"][user].get("time_range", {})
+                for user in config["users"]:
+                    _time_range = config["users"][user].get("time_range", {})
                     vstatus, vmsg = _validate_time_range(_time_range, vstatus, vmsg)
 
             if not vstatus:
                 return vstatus, vmsg
 
-        if "groups" in _config:
-            if not isinstance(_config["groups"], dict):
+        if "groups" in config:
+            if not isinstance(config["groups"], dict):
                 vstatus = False
                 vmsg = "Group configuration for wtmp beacon must " "be a dictionary."
             else:
-                for group in _config["groups"]:
-                    _time_range = _config["groups"][group].get("time_range", {})
+                for group in config["groups"]:
+                    _time_range = config["groups"][group].get("time_range", {})
                     vstatus, vmsg = _validate_time_range(_time_range, vstatus, vmsg)
             if not vstatus:
                 return vstatus, vmsg
 
-        if "defaults" in _config:
-            if not isinstance(_config["defaults"], dict):
+        if "defaults" in config:
+            if not isinstance(config["defaults"], dict):
                 vstatus = False
                 vmsg = "Defaults configuration for wtmp beacon must " "be a dictionary."
             else:
-                _time_range = _config["defaults"].get("time_range", {})
+                _time_range = config["defaults"].get("time_range", {})
                 vstatus, vmsg = _validate_time_range(_time_range, vstatus, vmsg)
             if not vstatus:
                 return vstatus, vmsg
diff --git a/salt/utils/beacons.py b/salt/utils/beacons.py
index 8e2d474ceec1..630ee3874f2b 100644
--- a/salt/utils/beacons.py
+++ b/salt/utils/beacons.py
@@ -14,3 +14,13 @@ def remove_hidden_options(config, whitelist):
             if func.startswith("_") and func not in whitelist:
                 config.remove(entry)
     return config
+
+
+def list_to_dict(config):
+    """
+    Convert list based beacon configuration
+    into a dictionary.
+    """
+    _config = {}
+    list(map(_config.update, config))
+    return _config
diff --git a/tests/pytests/unit/beacons/test_haproxy.py b/tests/pytests/unit/beacons/test_haproxy.py
index 859e708ad8fd..57c45b6e68bf 100644
--- a/tests/pytests/unit/beacons/test_haproxy.py
+++ b/tests/pytests/unit/beacons/test_haproxy.py
@@ -4,10 +4,14 @@
 
     HAProxy beacon test cases
 """
+import logging
+
 import pytest
 import salt.beacons.haproxy as haproxy
 from tests.support.mock import MagicMock, patch
 
+log = logging.getLogger(__name__)
+
 
 @pytest.fixture
 def configure_loader_modules():
@@ -44,6 +48,7 @@ def test_threshold_reached():
     mock = MagicMock(return_value=46)
     with patch.dict(haproxy.__salt__, {"haproxy.get_sessions": mock}):
         ret = haproxy.beacon(config)
+        log.debug("=== ret %s ===", ret)
         assert ret == [{"threshold": 45, "scur": 46, "server": "web1"}]
 
 


Code review comments:
diff_hunk: @@ -44,6 +48,7 @@ def test_threshold_reached():
     mock = MagicMock(return_value=46)
     with patch.dict(haproxy.__salt__, {"haproxy.get_sessions": mock}):
         ret = haproxy.beacon(config)
+        log.debug("=== ret %s ===", ret)
path: tests/pytests/unit/beacons/test_haproxy.py
text: @user1:
remove log statement.