Updating NetworkManager via the command line

NetworkManager is one of those things in the linux world that many SysAdmins hate. It’s a bit like your mother-in-law; it means well but is really really irritating. On any new system I set up, the first thing I do is get rid of it, and just use the old-school config files way of setting up your networking. As an experienced sysadmin, my mind is always trying to think ahead to ‘how is someone going to troubleshoot this some time in the future’. Config files and ifconfig and route commands are pretty easy and well understood. NetworkManager is something that appears to make things easier but if it doesn’t quite work the way you want it can be a bucket of pain.

Which leads me to this post. I was helping to set up a Fedora 16 system recently that had already been installed … and given that NetworkManager is the default, the sysadmin who set it up left it using NetworkManager. The thing is they had left out the DNS nameserver settings and I needed some name resolution. Of course the easy solution is edit /etc/resolv.conf and add in a nameserver line and in less than 30 seconds (assuming you have the network connectivity in place) you have name resolution. This is what I did and of course it worked and I was happy.

But then there was a power failure at the site and the server rebooted. It came up OK and the first thing I noticed was that /etc/resolv.conf did not have a nameserver entry in it. I knew straight away “Oh, that’s NetworkManager overwriting /etc/resolv.conf on boot”. So what to do to fix it? The normal answer is “Hop on the console, go to whatever Network preferences is called this week and add in the DNS IP to the Wired Connection in NetworkManager”. Unfortunately I did not have console access to this server. I did have a remote ssh connection so I tried logging in with x forwarding on, checked that I could launch an xterm, and tried running nm-applet .. which just hung and did not launch the GUI. I also sudo’d to root, made sure all my xauth and DISPLAY was right, checked that I could launch an xterm, then tried nm-applet … and again ‘nothing’. The thing is I had kind of seem this behaviour before on redhat-ish systems … and come to the conclusion that “PolicyKit is evil”.

So what next? Surely there must be a command line interface to NetworkManager? Nope. The closest thing is nmcli, but it has the great non-feature of not letting you change much. Sure it can give you some great status info, let you enable and disable some stuff, but no-can-do if you want to just update the DNS server.

Some googling later, and I’m finding some info about using dbus-send to query and control NetworkManager. In the examples below each dbus-send command gives you a little bit more info for typing in the next command (eg. the first one returns /org/freedesktop/NetworkManager/ActiveConnection/0 on my system for the list of active connections)

#work out the active connections
 dbus-send --system --print-reply --dest=org.freedesktop.NetworkManager \
/org/freedesktop/NetworkManager org.freedesktop.DBus.Properties.Get \
string:org.freedesktop.NetworkManager string:ActiveConnections
#get the list of devices
 dbus-send --system --print-reply --dest=org.freedesktop.NetworkManager \
 /org/freedesktop/NetworkManager/ActiveConnection/0 \
org.freedesktop.DBus.Properties.Get \
string:org.freedesktop.NetworkManager.Connection.Active string:Devices
#get the list of IP4Config stuff
 dbus-send --system --print-reply --dest=org.freedesktop.NetworkManager \
/org/freedesktop/NetworkManager/Devices/1 \
org.freedesktop.DBus.Properties.Get \
string:org.freedesktop.NetworkManager.Device string:Ip4Config
# print the nameservers. I knew this was the right command as I
#tried it on a test system that already had a DNS server assigned
 dbus-send --system --print-reply --dest=org.freedesktop.NetworkManager \
/org/freedesktop/NetworkManager/IP4Config/0 \
org.freedesktop.DBus.Properties.Get \
string:org.freedesktop.NetworkManager.IP4Config string:Nameservers

OK now, so I could drill down to get the nameserver detail. Great. Surely there is some easy way to then set it? After more googling it eventually transpired that the IP4Config stuff I was getting was a read-only dbussy thing. And that there must be some other way to actually ‘write’ to the settings. After more sleuthing (and using the GUI tool d-feet), I worked out this command;

dbus-send --system --print-reply --dest=org.freedesktop.NetworkManager \
/org/freedesktop/NetworkManager/Settings/0 \
org.freedesktop.NetworkManager.Settings.Connection.GetSettings

That prints out a heap of stuff including my IP address and DNS info, but its all in UINT32 type format, so an IP address like 192.168.0.100 is shown as;

uint32 1677764800

So how did I figure out that ‘Settings/0’ bit? You can get a list of the settings available with;

dbus-send --system --print-reply --dest=org.freedesktop.NetworkManager \
/org/freedesktop/NetworkManager/Settings \
 org.freedesktop.NetworkManager.Settings.ListConnections

OK, now I thought there must be a way of using dbus-send to update the DNS address. I tried a few different ideas that all failed, and then found some stackoverflow comment indicating in the dbus-send man page how ‘D-Bus supports more types than these, but dbus-send currently does not’. Great.

So then I looked at the python examples that come with NetworkManager. I’m not that great at python, but I fudged my way through them. Many of the examples did not run, and  I suspected that the version of NetworkManager I had was different to the one the examples were aimed at. Anyway, the biggest set of hints was in the update-secrets.py example, and eventually I hacked it and came up with the following python script (change-dns.py … Note that you might need to tailor this script if your settings is not Settings/0)

#!/usr/bin/env python
import dbus
import sys
import socket,struct
def dottedQuadToNum(ip):
   "convert decimal dotted quad string to long integer"
   return struct.unpack('<L',socket.inet_aton(ip))[0]

if len(sys.argv) != 2:
   sys.exit("Must supply ip address for dns; eg. 192.168.0.1")

x = dottedQuadToNum(sys.argv[1])
bus = dbus.SystemBus()
proxy = bus.get_object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/Settings/0")

settings = dbus.Interface(proxy, "org.freedesktop.NetworkManager.Settings.Connection")

config = settings.GetSettings()
s_ipv4 = config['ipv4']
s_ipv4['dns'] = dbus.Array([dbus.UInt32(x)], signature=dbus.Signature('u'), variant_level=1)

settings.Update(config)

So now I could run my script (called change-dns.py) with

./change-dns.py 192.168.0.123

And then run that GetSettings dbus-send again to check that it changed.

dbus-send --system --print-reply --dest=org.freedesktop.NetworkManager \
/org/freedesktop/NetworkManager/Settings/0 \
org.freedesktop.NetworkManager.Settings.Connection.GetSettings

So yes it did work, but it does not immediately update /etc/resolv.conf, so I still manually updated /etc/resolv.conf. Checking on a test system, NetworkManager does overwrite /etc/resolv.conf with the correct value after you reboot.

What about if you want to set two nameservers and perhaps a default search (eg. ‘search local’). Try changing the end of the script to accept two IP addresses;

if len(sys.argv) != 3:
   sys.exit("Must supply two ip addresses for dns; eg. 192.168.0.1 192.168.0.2")

x = dottedQuadToNum(sys.argv[1])
y = dottedQuadToNum(sys.argv[2])
bus = dbus.SystemBus()
proxy = bus.get_object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/Settings/0")

settings = dbus.Interface(proxy, "org.freedesktop.NetworkManager.Settings.Connection")

config = settings.GetSettings()
s_ipv4 = config['ipv4']
s_ipv4['dns'] = dbus.Array([dbus.UInt32(x), dbus.UInt32(y)], signature=dbus.Signature('u'), variant_level=1)
s_ipv4['dns-search'] = dbus.Array([dbus.String(u'local')], signature=dbus.Signature('s'), variant_level=1)
settings.Update(config)

Just a side note in case you are trawling the net trying to figure out how you can change an IP address on the command line using dbus, basically the change-dns.py script just needs the ‘s_ipv4[‘dns’] = dbus.Array….’ lines changed to something like;

s_ipv4[‘addresses’] = dbus.Array([dbus.Array([dbus.UInt32(1962977472L), dbus.UInt32(24L), dbus.UInt32(16820416L)], signature=dbus.Signature(‘u’))], signature=dbus.Signature(‘au’), variant_level=1)

The tricky part is figuring out the numbers. There are three numbers (all ending in a capital ‘L’). The first is the IP, the 2nd the netmask, the 3rd the default route. In the example above the IP address is 192.168.0.117. I usually use a programmers calculator, convert that to hex; C0A80075, then reverse all the octets; 7500A8C0 and then convert that to decimal; 1962977472 (of course, its much easier if you use the dottedQuadToNum function in the python script)