A shorter workaround to ensuring NFS/SMB shares are mounted after restart

Having restarted our Home Assistant Green for maintenance I was somewhat disappointed to see that it apparently doesn’t retry mounting its network shares on boot-up if they fail. Since I’d like our setup to be power-outage safe as much as possible, I had to investigate.

My first attempt was to just create an automation to query the entity state of each network mount and asking it to remount the share if it had failed. Unfortunately, not only doesn’t it auto-remount, there also is no way to even track the mount state.

The linked article also highlights some (many) other limitations of the current Home Assistant OS network share system that weren’t relevant to me, but along with an old Home Assistant form post pointed me in the right direction at least: When interacting with the system layer in Home Assistant few things are impossible when using the shell_command integration which allows running arbitrary commands inside the Home Assistant Core docker container as root!

Unlike either of these articles, I instead choose to use the native Home Assistant Supervisor API for managing mount points to query the mount states and issues repeated mount attempts. This is what the “System” section of the Home Assistant WebUI also ends up configuring and means that all mount actions will be performed in exactly the same way as the web interface would perform them.

Solution

Defining shell_commands in configuration.yaml

Setting up the shell_command integration is (intentionally) not possible using the web interface as it is a very advanced feature, so you need to instead open the configuration.yaml file that is exported inside the config network share by the Samba share app (or the config/configuration.yaml file accessible using the File editor app).

Once you’ve located that file add the following content, updating and extending the list of mount_retry_* commands as indicated in the comment:

# Workaround commands to check for and retry network mounts
shell_command:
  # Command to allow reading the current mount state from the Supervisor
  mount_query: 'curl -s --oauth2-bearer "$SUPERVISOR_TOKEN" http://supervisor/mounts'

  # One command per mount that should be remountable
  #
  # The name after `mount_retry_`  msut be the lower-cased version of the mount
  # name visible in the web interface; the name after http://supervisor/mounts/
  # must exactly match the casing of the mount in the web interface. 
  mount_retry_nas: 'curl -s --oauth2-bearer "$SUPERVISOR_TOKEN" -X POST http://supervisor/mounts/NAS/reload'
  mount_retry_nas_backup: 'curl -s --oauth2-bearer "$SUPERVISOR_TOKEN" -X POST http://supervisor/mounts/NAS_Backup/reload'

Now go to Settings → [Menu] → Restart Home AssistantRestart Home Assistant in the web interface and confirm to apply these changes and have the newly defined commands available.

In theory it would be possible to use a template to avoid repeating those mount_retry_* entries in the configuration file, but:

  1. There is no direct way to pass parameters to a shell command, only to substitute the state of a defined entity
    • This could be worked around by defining a hidden text helper entity in the web interface and then updating its state before each invocation of the given shell_command but it isn’t very intutive to say the least.
  2. Using any templates (or newlines for that matter 🙄) in the shell_command definition triggers secure environment mode which doesn’t allow any form of shell substition, however we require $SUPERVISOR_TOKEN to be substituted with the access token from the Home Assistant Core or any and all requests to Supervisor from our script will be rejected with a permission error response
    • This could be worked around by wrapping the whole command in sh -c '…'.

Adding an automation

Using the shell_commands defined above we can now create an automation as originally intended! Go to SettingsAutomations & scenesCreate automationCreate new automation, then click the menu button and select Edit in YAML and copy and paste the following:

alias: Retry failed mounts
description: ""
triggers:
  - trigger: time_pattern
    minutes: "*"
    hours: "*"
    seconds: "0"
conditions: []
actions:
  - alias: Query mount state
    action: shell_command.mount_query
    metadata: {}
    data: {}
    response_variable: mount_query
  - variables:
      mount_states: |
        {{ mount_query.stdout | from_json }}
  - alias: Repeat for each mount
    repeat:
      for_each: |
        {{ mount_states["data"]["mounts"] }}
      sequence:
        - if:
            - condition: template
              value_template: "{{ repeat.item[\"state\"] != \"active\" }}"
              alias: If mount state is not active
          then:
            - action: |
                shell_command.mount_retry_{{ repeat.item["name"] | lower }}
              data: {}
mode: single

This automation triggers every minute and…:

  1. Uses the first shell_command to query the current state of all mounts from the supervisor
  2. Parses the output of the previous step as JSON object
  3. Iterates of every returned mount point, and …
  4. For each mount point checks whether the mount point is active, and …
  5. If the mount point is not active invokes the defined other shell_command based on the name of the broken mount point
    • Note that the automation will abort if you forgot to define a shell_command for a failed mount above!

This means the automation will probe the mount states every minute, but will only attempt to “fix” a mount point if it has actually failed.