15

I need to get the IP of the machine and use it inside my service:

[Unit]
Description=Redmine server
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

# Give a reasonable amount of time for the server to start up/shut down
TimeoutSec=300

[Install]
WantedBy=multi-user.target

How can I execute this command inside the systemd service file and get its result:

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

in order to use its result infor xxx.xxx.xxx.xxx, sth like this:

ExecStart=/usr/bin/ruby /home/redmine/redmine/bin/rails server webrick -e production -p 3000 -b $ip
Giacomo1968
  • 58,727
elekgeek
  • 193

5 Answers5

14

Getting machine hostname with systemd.

While I am not deeply familiar with systemd looking over this tutorial website as well as the official systemd man pages, what you seem to be looking for is a “specifier” value:

Many settings resolve specifiers which may be used to write generic unit files referring to runtime or unit parameters that are replaced when the unit files are loaded.

And the specific specifier I believe would work here is %H which is for “Host name” and described as:

The hostname of the running system at the point in time the unit configuration is loaded.

So checking your example systemd script, change your [Service] chunk to be this:

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

Note the line with the ip=() assignment is gone and the ExecStart command now uses %H instead of $ip.

An idea for getting an IP address with systemd.

That said, it seems that systemd only provides a hostname via the %H “specifier.” Which is odd if you ask me. So while I have no deep experience with systemd, I believe I understand what could be done to achieve the goal of this post.

The key would be to setup an EnvironmentFile for systemd to read. Read up on how to use an EnvironmentFile over here on this site.

So let’s say you created a simple Bash script like this; let’s name it write_ip_to_file.sh and feel free to change the IP address fetching logic of ip=$() to match what works on your setup:

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

All that would do is output the IP address of eth0 to a text file named ip.txt in your home directory. The format would be something like this:

IP=123.456.789.0

Got that? Good. Now in your systemd script, change your [Service] chunk to be something like this; be sure to set [your username] to match the username of the directory where ip.txt is saved:

[Service]
Type=simple
User=redmine
Group=redmine
EnvironmentFile=/home/[your username]/ip.txt
ExecStart=/usr/bin/ruby /home/redmine/redmine/bin/rails server webrick -e production -b $IP -p 3000

And what that would do is load the config in ip.txt and assign $IP the value of 123.456.789.0. I believe that is what you are looking for.

The key factor here is to get write_ip_to_file.sh to run on boot or perhaps even by the systemd script itself.

Another idea for getting an IP address with systemd.

But with that said, I have a better idea (if it works): Move that whole ExecStart command into the Bash file called redmine_start.sh and make sure the system can read and execute it. The contents of redmine_start.sh would be as follows; feel free to change the IP address fetching logic of ip=$() to match what works on your setup:

#!/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

And then change your [Service] chunk to be something like this; be sure to set [your username] to match the username of the directory where redmine_start.sh is saved:

[Service]
Type=simple
User=redmine
Group=redmine
ExecStart=/home/[your username]/redmine_start.sh

If you follow the logic, if all of the logic of ExecStart is contained in redmine_start.sh then you can use that Bash trick to get the IP address, assign it to a variable and then start Redmine there. The systemd script would just be managing when/how to start that.

Getting machine IP address with init.d.

And for the reference of init.d users, I use Ubuntu and when I need to grab a current working system IP address in a Bash or an init.d startup script I run something like this:

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

Of course you need to change /sbin/ifconfig to match the location of ifconfig on your system, and then also change eth0 to match the network interface you want to get the IP address of.

But once adjusted to match your setup and needs, that successfully gets an interface’s IP address and assigns it to the variable ip which can then be accessed as $ip in your script.

Giacomo1968
  • 58,727
8

Maybe this construction will work. Try it:

[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
6

Use host IP addresses and EnvironmentFile

You can write your host IP addresses into /etc/network-environment file using setup-network-environment utility. Then you can run your app following way:

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

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

Source: https://coreos.com/os/docs/latest/using-environment-variables-in-systemd-units.html

3

Use hostname -i to get your (first) IP address and save it in SystemD's environment. You can access this variable with ${HOST_IP}.

Documentation:

  • man hostname for parameters -i and -I
  • man systemctl for set-environment
  • man systemd.service for ExecStartPre

Example

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

networkctl can output JSON with the --json= option set to pretty or short.

So you can get values out of it with jq, nushell or other JSON parsers. I use nu.

For IPv4:

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

Outputs 192.168.1.7

  • networkctl status main --json=short Get the JSON
  • from json parse JSON into nushell data
  • get Addresses select the list with all interface addresses
  • where Family == 2 Ipv4 is here
  • get Address get the list of single addresses
  • each { str join '.' } the address is given as a list of octets, join them into dot-notation address
  • to text return text that can be parsed by other shells etc. If multiple addresses are present, it will split with newlines

For IPv6 its a bit more complex, thanks to networkctl being "special":

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

Outputs fdad:cafe:face:20::2 - I don't have a GUA on this interface

  • where Family == 10 is the key for IPv6 addresses

  • IPv6 addresses are given as a list of unpadded decimal octets like [253, 173, ... 2] which is not something most things will accept as valid without processing

  • In order to turn them into hex notation you have to iterate over them a few times:

    1. $in | into int turn the value string into integer
    2. into binary -c then into unpadded binary
    3. encode hex --lower finally into lowercase hex
  • Now you have an address in hex but its in octets - fd:00 - instead of hextets - fd00 - still not valid IPv6 as far as most things are concerned

    1. chunks 2 split the list into pairs
    2. each { str join | str trim -l -c '0' } join each pair of octets into a hextet and strip leading 0s. Trimming is optional but makes it more readable
    3. str join ':' join the hextets into a valid address
  • str replace -r -a '::+' '::' regex things like :::: => ::. Also optional for human readability

  • Finally you can filter for the address type(s) you want:

    • filter { not ( $in starts-with "fe80" ) } to filter out the LLA(s)
    • filter { $in starts-with "fd" } get the ULA(s)
    • filter { not ( $in starts-with "fe80" ) and not ( $in starts-with "fd" ) } get the GUA(s)

This is an old question, but it was the top result when I searched, so hopefully this saves someone else some time.

Thanks to the nu Discord for some tips with the IPv6 processing.

This is all achievable with jq & sh, I find self-contained nushell scripts more portable:

#!/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 starts-with "fe80" } | to text }, "ula" => { v6 $iface | filter { $in starts-with "fd" } | to text }, "gua" => { v6 $iface | filter { not ( $in starts-with "fe80" ) and not ( $in starts-with "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 }