Как получить IP-адрес машины в файле службы “systemd”

Вопрос или проблема

Мне нужно получить IP машины и использовать его в моем сервисе:

[Unit]
Description=Сервер Redmine
After=syslog.target
After=network.target

[Service]
Type=simple
User=redmine
Group=redmine
ip="$(/sbin/ip -o -4 addr list eno16777736 | awk '{print $4}' | cut -d/ -f1)"
ExecStart=/usr/bin/ruby /home/redmine/redmine/bin/rails server webrick -e production -b $ip -p 3000

# Дать разумное количество времени для запуска/остановки сервера
TimeoutSec=300

[Install]
WantedBy=multi-user.target

Как я могу выполнить эту команду внутри файла сервиса systemd и получить её результат:

ip=$(/sbin/ip -o -4 addr list eno16777736 | awk '{print $4}' | cut -d/ -f1)

чтобы использовать его результат в инфо xxx.xxx.xxx.xxx, что-то вроде этого:

ExecStart=/usr/bin/ruby /home/redmine/redmine/bin/rails server webrick -e production -p 3000 -b $ip

Получение имени хоста машины с помощью systemd.

Хотя я не очень знаком с systemd, просмотр этого учебного сайта, а также официальных руководств systemd показывает, что вам нужно значение “specifier”:

Многие настройки разрешают спецификаторы, которые могут использоваться для написания универсальных файлов юнитов, ссылающихся на параметры запуска или параметры юнита, которые заменяются при загрузке файлов юнитов.

И конкретный спецификатор, который, по моему мнению, подойдет здесь, это %H, который используется для “Имени хоста” и описывается как:

Имя хоста работающей системы в момент загрузки конфигурации юнита.

Таким образом, в вашем примере скрипта systemd измените ваш блок [Service] на следующий:

[Service]
Type=simple
User=redmine
Group=redmine
ExecStart=/usr/bin/ruby /home/redmine/redmine/bin/rails server webrick -e production -b %H -p 3000

Обратите внимание, что строка с присвоением ip=() исчезает, а команда ExecStart теперь использует %H вместо $ip.

Идея получения IP-адреса с помощью systemd.

Стоит отметить, что systemd предоставляет только имя хоста через спецификатор %H. Что странно, если вы спросите меня. Поэтому, хотя у меня нет глубокого опыта работы с systemd, я думаю, что понимаю, что можно сделать, чтобы достичь цели этого поста.

Ключевым моментом будет настройка EnvironmentFile для чтения systemd. Узнайте, как использовать файл EnvironmentFile на этом сайте.

Итак, скажем, вы создали простой Bash скрипт, такой как этот; назовем его write_ip_to_file.sh и не стесняйтесь изменять логику получения IP-адреса ip=$(), чтобы она соответствовала вашей настройке:

#!/bin/bash
ip=$(/sbin/ifconfig eth0 | awk '/inet addr/ {split ($2,A,":"); print A[2]}');
echo IP=$ip > ~/ip.txt;

Все, что он делает, это выводит IP-адрес eth0 в текстовый файл с именем ip.txt в вашем домашнем каталоге. Формат будет таким:

IP=123.456.789.0

Поняли? Отлично. Теперь в вашем скрипте systemd измените ваш блок [Service] на следующий; убедитесь, что вы установили [ваше имя пользователя], чтобы оно соответствовало имени пользователя каталога, в котором сохранен ip.txt:

[Service]
Type=simple
User=redmine
Group=redmine
EnvironmentFile=/home/[ваше имя пользователя]/ip.txt
ExecStart=/usr/bin/ruby /home/redmine/redmine/bin/rails server webrick -e production -b $IP -p 3000

И это загрузит конфигурацию в ip.txt и присвоит $IP значение 123.456.789.0. Я думаю, что это то, что вы ищете.

Ключевой фактор здесь – заставить write_ip_to_file.sh запускаться при старте системы или, возможно, даже самим скриптом systemd.

Еще одна идея для получения IP-адреса с помощью systemd.

Но при этом у меня есть идея получше (если она работает): Переместить всю команду ExecStart в Bash-файл под названием redmine_start.sh и убедиться, что система может его читать и выполнять. Содержимое redmine_start.sh будет следующим; не стесняйтесь изменить логику получения IP-адреса ip=$(), чтобы она соответствовала вашей настройке:

#!/bin/bash
ip=$(/sbin/ifconfig eth0 | awk '/inet addr/ {split ($2,A,":"); print A[2]}');
/usr/bin/ruby /home/redmine/redmine/bin/rails server webrick -e production -b $IP -p 3000

А затем измените ваш блок [Service] на примерно такое; убедитесь, что вы установили [ваше имя пользователя], чтобы оно соответствовало имени пользователя каталога, в котором сохранен redmine_start.sh:

[Service]
Type=simple
User=redmine
Group=redmine
ExecStart=/home/[ваше имя пользователя]/redmine_start.sh

Если следовать логике, если вся логика ExecStart содержится в redmine_start.sh, то вы можете использовать этот Bash трюк для получения IP-адреса, присвоения его переменной и запуска Redmine там. Скрипт systemd будет лишь управлять тем, когда и как его запускать.

Получение IP-адреса машины с помощью init.d.

И для справки для пользователей init.d, я использую Ubuntu, и когда мне нужно получить текущий рабочий IP-адрес системы в Bash или стартовом скрипте init.d, я запускаю что-то вроде этого:

ip=$(/sbin/ifconfig eth0 | awk '/inet addr/ {split ($2,A,":"); print A[2]}')

Конечно, вам нужно изменить /sbin/ifconfig, чтобы оно соответствовало расположению ifconfig в вашей системе, и затем изменить eth0, чтобы оно соответствовало сетевому интерфейсу, IP-адрес которого вы хотите получить.

Но как только вы подстроили это под ваш настройки и нужды, это успешно получает IP-адрес интерфейса и присваивает его переменной ip, к которой затем можно получить доступ как $ip в вашем скрипте.

Возможно, эта конструкция сработает. Попробуйте:

[Service]
Type=simple
User=redmine
Group=redmine
PermissionsStartOnly=true
ExecStartPre=/bin/bash -c "/bin/systemctl set-environment ip=$(/sbin/ip -o -4 addr list eno16777736 | awk '{print $4}' | cut -d/ -f1)"
ExecStart=/usr/bin/ruby /home/redmine/redmine/bin/rails server webrick -e production -b ${ip} -p 3000

Используйте IP-адреса хоста и EnvironmentFile

Вы можете записать IP-адреса вашего хоста в файл /etc/network-environment с помощью утилиты setup-network-environment. Затем вы можете запустить свое приложение следующим образом:

[Unit]
Requires=setup-network-environment.service
After=setup-network-environment.service

[Service]
EnvironmentFile=/etc/network-environment
ExecStart=/opt/bin/kubelet --hostname_override=${DEFAULT_IPV4}

Источник: https://coreos.com/os/docs/latest/using-environment-variables-in-systemd-units.html

Используйте hostname -i, чтобы получить ваш (первый) IP-адрес и сохранить его в окружении SystemD. Вы можете получить доступ к этой переменной с помощью ${HOST_IP}.

Документация:

  • man hostname для параметров -i и -I
  • man systemctl для set-environment
  • man systemd.service для ExecStartPre

Пример

[Service]
ExecStartPre=/bin/sh -c "systemctl set-environment HOST_IP=$(hostname -i)"

networkctl может выводить JSON, если параметр --json= установлен в pretty или short.

Поэтому вы можете извлечь значения с помощью jq, nushell или других парсеров JSON. Я использую nu.

Для IPv4:

networkctl status main --json=short
| from json | get Addresses | where Family == 2
| get Address | each { str join '.' } | to text

Выводит 192.168.1.7

  • networkctl status main --json=short Получить JSON
  • from json преобразовать JSON в данные nushell
  • get Addresses выбрать список со всеми адресами интерфейса
  • where Family == 2 Ipv4 здесь
  • get Address получить список одиночных адресов
  • each { str join '.' } адрес представлен в виде списка октетов, соедините их в адрес в нотации через точки
  • to text вернуть текст, который может быть разобран другими оболочками и т.д. Если присутствуют несколько адресов, они будут разделены новыми строками

Для IPv6 это немного сложнее, благодаря networkctl за “специальное”:

networkctl status dns --json=short
| from json | get Addresses | where Family == 10 | get Address
| each {
    each {
        $in | into int | into binary -c | encode hex --lower
    } | chunks 2 | each {
        str join | str trim -l -c '0' 
    } | str join ':'
} | str replace -r -a '::+' '::'
| filter { not ( $in starts-with "fe80" ) } | to text

Выводит fdad:cafe:face:20::2 – на этом интерфейсе нет GUA

  • where Family == 10 – ключ для IPv6 адресов

  • IPv6 адреса представлены в виде списка неподготовленных десятичных октетов, таких как [253, 173, ... 2], что не является чем-то, что большинство вещей примет как допустимое без обработки

  • Чтобы превратить их в шестнадцатеричную нотацию, вам нужно пройти по ним несколько раз:

    1. $in | into int превратить строку значения в целое число
    2. into binary -c затем в неподготовленное двоичное
    3. encode hex --lower наконец, в шестнадцатеричное в нижнем регистре
  • Теперь у вас есть адрес в шестнадцатеричном формате, но он в октетах – fd:00 – вместо гексетов – fd00 – всё ещё не допустимый IPv6 в понимании большинства вещей

    1. chunks 2 разбить список на пары
    2. each { str join | str trim -l -c '0' } объединить каждую пару октетов в гекстанавливают и очистить ведущие 0. Обрезка необязательна, но делает её более читабельной
    3. str join ':' соединить гекстанавливают в допустимый адрес
  • str replace -r -a '::+' '::' regex от {{\’::::\’ -> ‘::’}}. Также опционально для читаемости

  • Наконец, вы можете фильтровать типы адресов, которые вам нужны:

    • filter { not ( $in starts-with "fe80" ) } чтобы исключить LLA(и)
    • filter { $in starts-with "fd" } получить ULA(и)
    • filter { not ( $in starts-with "fe80" ) and not ( $in starts-with "fd" ) } получить GUA(и)

Это старый вопрос, но он был верхним результатом, когда я искал, так что, надеюсь, это сэкономит кому-то время.

Благодаря дискорд-каналу nu за пару советов по обработке IPv6.

Всё это достижимо с помощью jq & sh, но я нахожу автономные nushell скрипты более переносимыми:

#!/usr/bin/env nu
# networkd-ip.nu
def "main help" []: nothing -> nothing {
  print "
Usage:
  v4 DEVICE       Print IPv4 addresses of DEVICE, newline-separated

  v6 DEVICE [OPTIONS ...]
                  Print IPv6 addresses of DEVICE, newline-separated
Options:
  -t --type lla|ula|gua
                  The type of IPv6 addresses to return

  help            Show this help
"
}

def "main v4" [
  iface: string
]: nothing -> any {
  networkctl status $iface --json=short
  | from json | get Addresses | where Family == 2
  | get Address | each { str join '.' } | to text
}

def "main v6" [
  iface: string
  --type (-t): string
]: nothing -> any {
  def v6 [iface: string]: nothing -> list {
    networkctl status dns --json=short
    | from json | get Addresses | where Family == 10 | get Address
    | each {
        each {
            $in | into int | into binary -c | encode hex --lower
        } | chunks 2 | each {
            str join | str trim -l -c '0' 
        } | str join ':'
    } | str replace -r -a '::+' '::'
  }
  if not ( $type == null ) {
    match $type {
      "lla" => {
        v6 $iface | filter { $in.startswith "fe80" } | to text
      },
      "ula" => {
        v6 $_iface | filter { $in.startswith "fd" } | to text
      },
      "gua" => {
        v6 $iface | filter {
          not ( $in.startswith "fe80" ) and not ( $in.startswith "fd" )
        } | to text
      },
      _ => {
        print $"Error: ($type) is not a valid IPv6 address type"
        print "    -t --type lla|ula|gua"
      }
    }
  } else {
    v6 $iface | to text
  }
}

def main []: nothing -> nothing {
  main help
}


Ответ или решение

Чтобы получить IP-адрес машины в файле службы systemd, вам нужно использовать несколько подходов, поскольку системные службы не предоставляют прямого способа исполнения команд оболочки. Ниже я описываю несколько методик, которые помогут вам добиться этой задачи.

Использование ExecStartPre и set-environment

Один из наиболее эффективных способов — это использование директивы ExecStartPre вместе с systemctl set-environment. Этот подход позволяет заранее установить переменные окружения, доступные в команде ExecStart.

[Unit]
Description=Redmine server
After=network.target

[Service]
Type=simple
User=redmine
Group=redmine
PermissionsStartOnly=true
ExecStartPre=/bin/bash -c "systemctl set-environment IP_ADDRESS=$(hostname -i)"
ExecStart=/usr/bin/ruby /home/redmine/redmine/bin/rails server webrick -e production -b ${IP_ADDRESS} -p 3000

[Install]
WantedBy=multi-user.target

Здесь:

  • ExecStartPre используется для выполнения команды перед тем, как начнется основная служебная задача.
  • hostname -i используется для получения IP-адреса. Замените, если необходимо, другой командой для получения IP.

Использование EnvironmentFile

Еще один подход это использовать EnvironmentFile. Вы можете создать файл скрипта, который будет записывать IP-адрес в файл перед запуском сервиса. Например:

Создайте скрипт write_ip_to_file.sh:

#!/bin/bash
ip=$(hostname -i)
echo "IP_ADDRESS=$ip" > /etc/redmine_ip.env

Измените файл службы systemd:

[Unit]
Description=Redmine server
After=network.target

[Service]
Type=simple
User=redmine
Group=redmine
EnvironmentFile=/etc/redmine_ip.env
ExecStart=/usr/bin/ruby /home/redmine/redmine/bin/rails server webrick -e production -b ${IP_ADDRESS} -p 3000

[Install]
WantedBy=multi-user.target

Плюсы этого подхода:

  • Позволяет более явное управление переменными окружения.
  • Облегчает отладку.

Использование сторонних инструментов

Вы также можете рассмотреть использование сторонних утилит или скриптов на nu или jq для извлечения IP-адреса, особенно если требуется более сложная логика обработки сетевых данных.

Однако, системные службы systemd в первую очередь должны быть простыми и надежными, следовательно, использование дополнительных инструментов, таких как jq, может усложнить и повысить требовательность к конфигурации среды.

Заключение

Для надежной и управляемой конфигурации службы systemd, рекомендуется минимально задействовать внешние команды и сосредоточиться на настройках, которые легко тестировать и поддерживать. Каждый из предложенных методов имеет свои особенности, и выбор подхода зависит от конкретной архитектуры и требований вашего окружения.

Оцените материал
Добавить комментарий

Капча загружается...