Hosts File replication

liptech

Active Member
Jan 14, 2021
42
8
28
44
Brasil
Hello everybody,

I imagine this is not a doubt of mine.
The "/etc/hosts" file is an essential tool for those who want to organize access and maintain some control over hosts without depending on a DNS server.

The point is that it has to be uniform (having the same entrances) in each node, which makes us have to manually replicate it for each node.

I have tried to put it on the choosync creating a "hosts" file in "/etc/pve/priv" and creating a symbolic link in the "/etc/" folder, but it looks like Debian does not accept.

So I configured Cron to copy the files, but it is executed every 1m, for me is already a breakthrough but I really wanted to know your opinion about this procedure and if you have a faster way to do it.

And if not recommended also wanted to know why.

Thank you for your attention.
 
To run "ln .... /etc/pve/hosts" the cluster must be up and running and it must quorate already. /etc/pve is a fuse-mounted database of the PVE system.

----
Added later: I just started one node of my test-cluster to check the behavior. Doing it the other way around works without establishing Quorum first, as this reads from /etc/pve only. Make "/etc/pve/hosts" your primary source of truth. Then you could "ln -sf /etc/pve/hosts /etc/hosts".

But then again... the system has to have booted successfully already, which might fail (not tested) for the next boot because then there is no "/etc/hosts" with the above approach, just the dangling soft link! ;-)

"etc/hosts" must contain sufficient information at all times. So my slightly modified approach would be to have "/etc/pve/hosts" and copy it from time time to time (manually or via cron) to "/etc/hosts".

And just to add the correct solution for this artificial problem: once upon a time someone invented DNS. I have two classic "bind"s running, both in my dayjob and in my Homelab, so "/etc/hosts" is not really relevant... :-)
 
Last edited:
To run "ln .... /etc/pve/hosts" the cluster must be up and running and it must quorate already. /etc/pve is a fuse-mounted database of the PVE system.

----
Added later: I just started one node of my test-cluster to check the behavior. Doing it the other way around works without establishing Quorum first, as this reads from /etc/pve only. Make "/etc/pve/hosts" your primary source of truth. Then you could "ln -sf /etc/pve/hosts /etc/hosts".

But then again... the system has to have booted successfully already, which might fail (not tested) for the next boot because then there is no "/etc/hosts" with the above approach, just the dangling soft link! ;-)

"etc/hosts" must contain sufficient information at all times. So my slightly modified approach would be to have "/etc/pve/hosts" and copy it from time time to time (manually or via cron) to "/etc/hosts".

And just to add the correct solution for this artificial problem: once upon a time someone invented DNS. I have two classic "bind"s running, both in my dayjob and in my Homelab, so "/etc/hosts" is not really relevant... :-)
That's exactly what I did.
 
  • Like
Reactions: UdoB
Here it is, I'll test it.

#!/bin/bash

HOSTS_FILE="/etc/hosts"
HASH_FILE="/var/tmp/hosts.hash"
SERIAL_FILE="/var/tmp/hosts.serial"
INVENTORY="/etc/ansible/hosts"
LOG_FILE="/var/log/hosts_sync.log"

calculate_hash() {
grep -v "^# SERIAL:" "$HOSTS_FILE" | md5sum | awk '{print $1}'
}

get_serial() {
grep "^# SERIAL:" "$HOSTS_FILE" | awk '{print $3}' || echo "0"
}

update_serial() {
TODAY=$(date +"%Y%m%d")
CURRENT_SERIAL=$(get_serial)
if [[ "$CURRENT_SERIAL" =~ ^$TODAY([0-9]{4})$ ]]; then
COUNT=$(echo "$CURRENT_SERIAL" | sed "s/$TODAY//")
COUNT=$((COUNT + 1))
else
COUNT=1
fi
NEW_SERIAL="${TODAY}$(printf "%04d" "$COUNT")"
sed -i '/^# SERIAL:/d' "$HOSTS_FILE"
echo "# SERIAL: $NEW_SERIAL" >> "$HOSTS_FILE"
echo "$NEW_SERIAL" > "$SERIAL_FILE"
echo "$(date) - [$HOSTNAME] $NEW_SERIAL" >> "$LOG_FILE"
}

sync_hosts() {
ansible all -i "$INVENTORY" -m copy --args "src=$HOSTS_FILE dest=$HOSTS_FILE owner=root group=root mode=0644" --become
calculate_hash > "$HASH_FILE"
}

recover_from_offline() {
LOCAL_SERIAL=$(get_serial)
REMOTE_SERIAL=$(ansible all -i "$INVENTORY" -m shell --args "grep '^# SERIAL:' /etc/hosts | awk '{print \$3}' || echo '0'" --become -o | awk '{print $2}' | awk 'max<$1 {max=$1} END {print max}')
if [[ "$LOCAL_SERIAL" -lt "$REMOTE_SERIAL" ]]; then
ansible all -i "$INVENTORY" -m fetch --args "src=$HOSTS_FILE dest=/tmp/hosts.remote flat=yes" --become
mv /tmp/hosts.remote "$HOSTS_FILE"
fi
}

if [[ ! -f $HASH_FILE ]]; then
calculate_hash > "$HASH_FILE"
fi

if [[ ! -f $SERIAL_FILE ]]; then
echo "0" > "$SERIAL_FILE"
fi

recover_from_offline

while true; do
NEW_HASH=$(calculate_hash)
OLD_HASH=$(cat "$HASH_FILE")
LOCAL_SERIAL=$(get_serial)
REMOTE_SERIAL=$(ansible all -i "$INVENTORY" -m shell --args "grep '^# SERIAL:' /etc/hosts | awk '{print \$3}' || echo '0'" --become -o | awk '{print $2}' | awk 'max<$1 {max=$1} END {print max}')
if [[ "$LOCAL_SERIAL" -lt "$REMOTE_SERIAL" ]]; then
recover_from_offline
fi
if [[ "$NEW_HASH" != "$OLD_HASH" && "$LOCAL_SERIAL" -ge "$REMOTE_SERIAL" ]]; then
update_serial
sync_hosts
fi
sleep 5
done
 
Another idea I had, is simpler.
#!/bin/bash

SYNC_FILE="/etc/pve/priv/hosts.sync"
HOSTS_FILE="/etc/hosts"
LOG_FILE="/var/log/hosts_sync.log"

sync_hosts() {
echo "$(date) - Sincronizando arquivos..." >> "$LOG_FILE"
if [[ "$SYNC_FILE" -nt "$HOSTS_FILE" ]]; then
cp "$SYNC_FILE" "$HOSTS_FILE"
systemctl restart networking
echo "$(date) - Atualizado /etc/hosts a partir de $SYNC_FILE" >> "$LOG_FILE"
else
cp "$HOSTS_FILE" "$SYNC_FILE"
echo "$(date) - Atualizado $SYNC_FILE a partir de /etc/hosts" >> "$LOG_FILE"
fi
}

if [[ ! -f $SYNC_FILE ]]; then
cp "$HOSTS_FILE" "$SYNC_FILE"
echo "$(date) - Criado arquivo de sincronização inicial." >> "$LOG_FILE"
fi

while true; do
sync_hosts
sleep 5
done
 
I'd still personally go with Puppet-however...

Just make a systemd service one-shot that copies /etc/pve/priv/hosts after the cluster filesystem starts?

If you wanted you can then also set a systemd timer to call it every now and then. The following is completely untested and I have not checked the systemd versions for the elements vs what is actually supported in Proxmox's systemd.



1. Create the systemd Service Unit

Save the following content to /etc/systemd/system/sync-pve-hosts.service:

INI:
[Unit]
Description=Sync PVE cluster hosts file to /etc/hosts
Requires=pve-cluster.service
After=pve-cluster.service
ConditionPathExists=/etc/pve/priv/hosts

[Service]
Type=oneshot
ExecStart=/bin/cp /etc/pve/priv/hosts /etc/hosts

[Install]
WantedBy=multi-user.target


2. Create a systemd Timer (Optional)

To run this service periodically, create a timer unit:

Save this file as /etc/systemd/system/sync-pve-hosts.timer:


INI:
[Unit]
Description=Run sync-pve-hosts.service periodically

[Timer]
OnBootSec=2min
OnUnitActiveSec=1h
Persistent=true

[Install]
WantedBy=timers.target


3. Enable and Start the Service

Bash:
systemctl daemon-reload
systemctl enable sync-pve-hosts.service
systemctl start sync-pve-hosts.service


4. Enable and Start the Timer

If using the timer:

Bash:
systemctl enable sync-pve-hosts.timer
systemctl start sync-pve-hosts.timer
 
  • Like
Reactions: liptech
E
I'd still personally go with Puppet-however...

Just make a systemd service one-shot that copies /etc/pve/priv/hosts after the cluster filesystem starts?

If you wanted you can then also set a systemd timer to call it every now and then. The following is completely untested and I have not checked the systemd versions for the elements vs what is actually supported in Proxmox's systemd.



1. Create the systemd Service Unit

Save the following content to /etc/systemd/system/sync-pve-hosts.service:

INI:
[Unit]
Description=Sync PVE cluster hosts file to /etc/hosts
Requires=pve-cluster.service
After=pve-cluster.service
ConditionPathExists=/etc/pve/priv/hosts

[Service]
Type=oneshot
ExecStart=/bin/cp /etc/pve/priv/hosts /etc/hosts

[Install]
WantedBy=multi-user.target


2. Create a systemd Timer (Optional)

To run this service periodically, create a timer unit:

Save this file as /etc/systemd/system/sync-pve-hosts.timer:


INI:
[Unit]
Description=Run sync-pve-hosts.service periodically

[Timer]
OnBootSec=2min
OnUnitActiveSec=1h
Persistent=true

[Install]
WantedBy=timers.target


3. Enable and Start the Service

Bash:
systemctl daemon-reload
systemctl enable sync-pve-hosts.service
systemctl start sync-pve-hosts.service


4. Enable and Start the Timer

If using the timer:

Bash:
systemctl enable sync-pve-hosts.timer
systemctl start sync-pve-hosts.timer

Simplified "/etc/hosts" Synchronization Using Corosync and Systemd.​


1. Create a systemd service.

Edit the file /etc/systemd/system/hosts-sync.service:

Code:
[Unit]
Description=Automatic /etc/hosts synchronization via Corosync
After=network.target

[Service]
ExecStart=/usr/local/bin/sync_hosts.sh
Restart=always
User=root

[Install]
WantedBy=multi-user.target

2. Create the synchronization script​

Save the following script as /usr/local/bin/sync_hosts.sh:

Code:
#!/bin/bash

SYNC_FILE="/etc/pve/priv/hosts.sync"
HOSTS_FILE="/etc/hosts"
LOG_FILE="/var/log/hosts_sync.log"

if [[ ! -f $SYNC_FILE ]]; then
    cp "$HOSTS_FILE" "$SYNC_FILE"
    echo "$(date) - Criado arquivo de sincronização inicial." >> "$LOG_FILE"
fi

sync_hosts() {
    if ! diff -q "$SYNC_FILE" "$HOSTS_FILE" > /dev/null 2>&1; then
        echo "$(date) - Change detected, synchronizing..." >> "$LOG_FILE"
      
        if [[ "$SYNC_FILE" -nt "$HOSTS_FILE" ]]; then
            cp "$SYNC_FILE" "$HOSTS_FILE"
            echo "$(date) - /etc/hosts updated from $SYNC_FILE" >> "$LOG_FILE"
        else
            cp "$HOSTS_FILE" "$SYNC_FILE"
            echo "$(date) - $SYNC_FILE updated from /etc/hosts" >> "$LOG_FILE"
        fi
    fi
}

if [[ ! -f $SYNC_FILE ]]; then
    cp "$HOSTS_FILE" "$SYNC_FILE"
    echo "$(date) - Initial synchronization file created." >> "$LOG_FILE"
fi

while true; do
    sync_hosts
    sleep 5
done


3. Make the script executable.

Code:
chmod a+x /usr/local/bin/sync_hosts.sh

4. Enable and start the systemd service.

Code:
systemctl daemon-reload
systemctl enable hosts-sync.service
systemctl start hosts-sync.service

Now, any changes to /etc/hosts will be automatically synchronized across the cluster.
 
Last edited:
I would not advise having the service wait for network.target rather thanpve-cluster.service. You need to wait for the cluster file system to start for the sync file to be present. Using network.target won't necessarily achieve this.

If you're planning on updating the hosts file often and need a short residence time for changes to be reflected in the node hosts files, you might just want to use a long-run systemd service using inotifywait instead of a sleep loop. That said, I have no idea if the pmxcfs will support it due to pmxcfs not being a "real POSIX" filesystem. (Edit: just checked, inotifywait does work for the cluster filesystem - neat!)

Personally I would not do a bidirectional sync and would treat the hosts.sync file on the cluster filesystem as the source of truth.

Edit: This would probably work:


Bash:
# Monitor changes on both files and sync accordingly
inotifywait -m -e modify "$HOSTS_FILE" "$SYNC_FILE" 2>/dev/null | while read -r file event; do
    if [[ "$file" == "$HOSTS_FILE" ]]; then
        # /etc/hosts modified, sync to hosts.sync
        cp "$HOSTS_FILE" "$SYNC_FILE"
        echo "$(date) - /etc/hosts updated to $SYNC_FILE" >> "$LOG_FILE"
    elif [[ "$file" == "$SYNC_FILE" ]]; then
        # hosts.sync modified, sync to /etc/hosts
        cp "$SYNC_FILE" "$HOSTS_FILE"
        echo "$(date) - $SYNC_FILE updated to /etc/hosts" >> "$LOG_FILE"
    fi
done
 
Last edited: