Skip to content

Instantly share code, notes, and snippets.

@alex-eri
Created August 6, 2020 11:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alex-eri/f1302ebad4d7a936faedfffee4a475a4 to your computer and use it in GitHub Desktop.
Save alex-eri/f1302ebad4d7a936faedfffee4a475a4 to your computer and use it in GitHub Desktop.
# NetworkManager - a library to make interacting with the NetworkManager daemon
# easier.
#
# (C)2011-2017 Dennis Kaarsemaker
# License: zlib
import copy
import dbus
import dbus.service
import os
import six
import socket
import struct
import sys
import time
import warnings
import weakref
import xml.etree.ElementTree as etree
class ObjectVanished(Exception):
def __init__(self, obj):
self.obj = obj
super(ObjectVanished, self).__init__(obj.object_path)
class SignalDispatcher(object):
def __init__(self):
self.handlers = {}
self.args = {}
self.interfaces = set()
self.setup = False
def setup_signals(self):
if not self.setup:
bus = dbus.SystemBus()
for interface in self.interfaces:
bus.add_signal_receiver(self.handle_signal, dbus_interface=interface, interface_keyword='interface', member_keyword='signal', path_keyword='path')
self.setup = True
self.listen_for_restarts()
def listen_for_restarts(self):
# If we have a mainloop, listen for disconnections
if not NMDbusInterface.last_disconnect and dbus.get_default_main_loop():
dbus.SystemBus().add_signal_receiver(self.handle_restart, 'NameOwnerChanged', 'org.freedesktop.DBus')
NMDbusInterface.last_disconnect = 1
def add_signal_receiver(self, interface, signal, obj, func, args, kwargs):
self.setup_signals()
key = (interface, signal)
if key not in self.handlers:
self.handlers[key] = []
self.handlers[key].append((obj, func, args, kwargs))
def handle_signal(self, *args, **kwargs):
key = (kwargs['interface'], kwargs['signal'])
skwargs = {}
sargs = []
if key not in self.handlers:
return
try:
sender = fixups.base_to_python(kwargs['path'])
for arg, (name, signature) in zip(args, self.args[key]):
if name:
skwargs[name] = fixups.to_python(type(sender).__name__, kwargs['signal'], name, arg, signature)
else:
# Older NetworkManager versions don't supply attribute names. Hope for the best.
sargs.append(fixups.to_python(type(sender).__name__, kwargs['signal'], None, arg, signature))
except dbus.exceptions.DBusException:
# This happens if the sender went away. Tough luck, no signal for you.
return
to_delete = []
for pos, (match, receiver, rargs, rkwargs) in enumerate(self.handlers[key]):
try:
match == sender
except ObjectVanished:
to_delete.append(pos)
continue
if match == sender:
rkwargs['interface'] = kwargs['interface']
rkwargs['signal'] = kwargs['signal']
rkwargs.update(skwargs)
receiver(sender, *(sargs + rargs), **rkwargs)
for pos in reversed(to_delete):
self.handlers[key].pop(pos)
def handle_restart(self, name, old, new):
if str(new) == "" or str(name) != 'org.freedesktop.NetworkManager':
return
NMDbusInterface.last_disconnect = time.time()
time.sleep(1) # Give NetworkManager a bit of time to start and rediscover itself.
for key in self.handlers:
val, self.handlers[key] = self.handlers[key], []
for obj, func, args, kwargs in val:
try:
# This resets the object path if needed
obj.proxy
self.add_signal_receiver(key[0], key[1], obj, func, args, kwargs)
except ObjectVanished:
pass
SignalDispatcher = SignalDispatcher()
# We completely dynamically generate all classes using introspection data. As
# this is done at import time, use a special dbus connection that does not get
# in the way of setting a mainloop and doing async stuff later.
init_bus = dbus.SystemBus(private=True)
xml_cache = {}
class NMDbusInterfaceType(type):
"""Metaclass that generates our classes based on introspection data"""
dbus_service = 'org.freedesktop.NetworkManager'
def __new__(type_, name, bases, attrs):
attrs['dbus_service'] = type_.dbus_service
attrs['properties'] = []
attrs['introspection_data'] = None
attrs['signals'] = []
# Derive the interface name from the name of the class, but let classes
# override it if needed
if 'interface_names' not in attrs and 'NMDbusInterface' not in name:
attrs['interface_names'] = ['org.freedesktop.NetworkManager.%s' % name]
for base in bases:
if hasattr(base, 'interface_names'):
attrs['interface_names'] = ['%s.%s' % (base.interface_names[0], name)] + base.interface_names
break
else:
for base in bases:
if hasattr(base, 'interface_names'):
attrs['interface_names'] += base.interface_names
break
if 'interface_names' in attrs:
SignalDispatcher.interfaces.update(attrs['interface_names'])
# If we know where to find this object, let's introspect it and
# generate properties and methods
if 'object_path' in attrs and attrs['object_path']:
proxy = init_bus.get_object(type_.dbus_service, attrs['object_path'])
attrs['introspection_data'] = proxy.Introspect(dbus_interface='org.freedesktop.DBus.Introspectable')
root = etree.fromstring(attrs['introspection_data'])
for element in root:
if element.tag == 'interface' and element.attrib['name'] in attrs['interface_names']:
for item in element:
if item.tag == 'property':
attrs[item.attrib['name']] = type_.make_property(name, element.attrib['name'], item.attrib)
attrs['properties'].append(item.attrib['name'])
elif item.tag == 'method':
aname = item.attrib['name']
if aname in attrs:
aname = '_' + aname
attrs[aname] = type_.make_method(name, element.attrib['name'], item.attrib, list(item))
elif item.tag == 'signal':
SignalDispatcher.args[(element.attrib['name'], item.attrib['name'])] = [(arg.attrib.get('name',None), arg.attrib['type']) for arg in item]
attrs['On' + item.attrib['name']] = type_.make_signal(name, element.attrib['name'], item.attrib)
attrs['signals'].append(item.attrib['name'])
klass = super(NMDbusInterfaceType, type_).__new__(type_, name, bases, attrs)
return klass
@staticmethod
def make_property(klass, interface, attrib):
name = attrib['name']
def get_func(self):
try:
data = self.proxy.Get(interface, name, dbus_interface='org.freedesktop.DBus.Properties')
except dbus.exceptions.DBusException as e:
if e.get_dbus_name() == 'org.freedesktop.DBus.Error.UnknownMethod':
raise ObjectVanished(self)
raise
return fixups.to_python(klass, 'Get', name, data, attrib['type'])
if attrib['access'] == 'read':
return property(get_func)
def set_func(self, value):
value = fixups.to_dbus(klass, 'Set', name, value, attrib['type'])
try:
return self.proxy.Set(interface, name, value, dbus_interface='org.freedesktop.DBus.Properties')
except dbus.exceptions.DBusException as e:
if e.get_dbus_name() == 'org.freedesktop.DBus.Error.UnknownMethod':
raise ObjectVanished(self)
raise
return property(get_func, set_func)
@staticmethod
def make_method(klass, interface, attrib, args):
name = attrib['name']
outargs = [x for x in args if x.tag == 'arg' and x.attrib['direction'] == 'out']
outargstr = ', '.join([x.attrib['name'] for x in outargs]) or 'ret'
args = [x for x in args if x.tag == 'arg' and x.attrib['direction'] == 'in']
argstr = ', '.join([x.attrib['name'] for x in args])
ret = {}
code = "def %s(self%s):\n" % (name, ', ' + argstr if argstr else '')
for arg in args:
argname = arg.attrib['name']
signature = arg.attrib['type']
code += " %s = fixups.to_dbus('%s', '%s', '%s', %s, '%s')\n" % (argname, klass, name, argname, argname, signature)
code += " try:\n"
code += " %s = dbus.Interface(self.proxy, '%s').%s(%s)\n" % (outargstr, interface, name, argstr)
code += " except dbus.exceptions.DBusException as e:\n"
code += " if e.get_dbus_name() == 'org.freedesktop.DBus.Error.UnknownMethod':\n"
code += " raise ObjectVanished(self)\n"
code += " raise\n"
for arg in outargs:
argname = arg.attrib['name']
signature = arg.attrib['type']
code += " %s = fixups.to_python('%s', '%s', '%s', %s, '%s')\n" % (argname, klass, name, argname, argname, signature)
code += " return (%s)" % outargstr
exec(code, globals(), ret)
return ret[name]
@staticmethod
def make_signal(klass, interface, attrib):
name = attrib['name']
ret = {}
code = "def On%s(self, func, *args, **kwargs):" % name
code += " SignalDispatcher.add_signal_receiver('%s', '%s', self, func, list(args), kwargs)" % (interface, name)
exec(code, globals(), ret)
return ret['On' + name]
@six.add_metaclass(NMDbusInterfaceType)
class NMDbusInterface(object):
object_path = None
last_disconnect = 0
is_transient = False
def __new__(klass, object_path=None):
# If we didn't introspect this one at definition time, let's do it now.
if object_path and not klass.introspection_data:
proxy = dbus.SystemBus().get_object(klass.dbus_service, object_path)
klass.introspection_data = proxy.Introspect(dbus_interface='org.freedesktop.DBus.Introspectable')
root = etree.fromstring(klass.introspection_data)
for element in root:
if element.tag == 'interface' and element.attrib['name'] in klass.interface_names:
for item in element:
if item.tag == 'property':
setattr(klass, item.attrib['name'], type(klass).make_property(klass.__name__, element.attrib['name'], item.attrib))
klass.properties.append(item.attrib['name'])
elif item.tag == 'method':
aname = item.attrib['name']
if hasattr(klass, aname):
aname = '_' + aname
setattr(klass, aname, type(klass).make_method(klass.__name__, element.attrib['name'], item.attrib, list(item)))
elif item.tag == 'signal':
SignalDispatcher.args[(element.attrib['name'], item.attrib['name'])] = [(arg.attrib.get('name',None), arg.attrib['type']) for arg in item]
setattr(klass, 'On' + item.attrib['name'], type(klass).make_signal(klass.__name__, element.attrib['name'], item.attrib))
klass.signals.append(item.attrib['name'])
SignalDispatcher.listen_for_restarts()
return super(NMDbusInterface, klass).__new__(klass)
def __init__(self, object_path=None):
if isinstance(object_path, NMDbusInterface):
object_path = object_path.object_path
self.object_path = self.object_path or object_path
self._proxy = None
def __eq__(self, other):
return isinstance(other, NMDbusInterface) and self.object_path and other.object_path == self.object_path
@property
def proxy(self):
if not self._proxy:
self._proxy = dbus.SystemBus().get_object(self.dbus_service, self.object_path)
self._proxy.created = time.time()
elif self._proxy.created < self.last_disconnect:
if self.is_transient:
raise ObjectVanished(self)
obj = type(self)(self.object_path)
if obj.object_path != self.object_path:
self.object_path = obj.object_path
self._proxy = dbus.SystemBus().get_object(self.dbus_service, self.object_path)
self._proxy.created = time.time()
return self._proxy
# Backwards compatibility interface
def connect_to_signal(self, signal, handler, *args, **kwargs):
return getattr(self, 'On' + signal)(handler, *args, **kwargs)
class TransientNMDbusInterface(NMDbusInterface):
is_transient = True
class NetworkManager(NMDbusInterface):
interface_names = ['org.freedesktop.NetworkManager']
object_path = '/org/freedesktop/NetworkManager'
# noop method for backward compatibility. It is no longer necessary to call
# this but let's not break code that does so.
def auto_reconnect(self):
pass
class Statistics(NMDbusInterface):
object_path = '/org/freedesktop/NetworkManager/Statistics'
class Settings(NMDbusInterface):
object_path = '/org/freedesktop/NetworkManager/Settings'
class AgentManager(NMDbusInterface):
object_path = '/org/freedesktop/NetworkManager/AgentManager'
class Connection(NMDbusInterface):
interface_names = ['org.freedesktop.NetworkManager.Settings.Connection']
has_secrets = ['802-1x', '802-11-wireless-security', 'cdma', 'gsm', 'pppoe', 'vpn']
def __init__(self, object_path):
super(Connection, self).__init__(object_path)
self.uuid = self.GetSettings()['connection']['uuid']
def GetSecrets(self, name=None):
settings = self.GetSettings()
if name is None:
name = settings['connection']['type']
name = settings[name].get('security', name)
try:
return self._GetSecrets(name)
except dbus.exceptions.DBusException as e:
if e.get_dbus_name() != 'org.freedesktop.NetworkManager.AgentManager.NoSecrets':
raise
return {key: {} for key in settings}
@staticmethod
def all():
return Settings.ListConnections()
def __eq__(self, other):
return isinstance(other, type(self)) and self.uuid == other.uuid
class ActiveConnection(TransientNMDbusInterface):
interface_names = ['org.freedesktop.NetworkManager.Connection.Active']
def __new__(klass, object_path):
if klass == ActiveConnection:
# Automatically turn this into a VPNConnection if needed
obj = dbus.SystemBus().get_object(klass.dbus_service, object_path)
if obj.Get('org.freedesktop.NetworkManager.Connection.Active', 'Vpn', dbus_interface='org.freedesktop.DBus.Properties'):
return VPNConnection.__new__(VPNConnection, object_path)
return super(ActiveConnection, klass).__new__(klass, object_path)
def __eq__(self, other):
return isinstance(other, type(self)) and self.Uuid == other.Uuid
class VPNConnection(ActiveConnection):
interface_names = ['org.freedesktop.NetworkManager.VPN.Connection']
class Device(NMDbusInterface):
interface_names = ['org.freedesktop.NetworkManager.Device', 'org.freedesktop.NetworkManager.Device.Statistics']
def __new__(klass, object_path):
if klass == Device:
# Automatically specialize the device
try:
obj = dbus.SystemBus().get_object(klass.dbus_service, object_path)
klass = device_class(obj.Get('org.freedesktop.NetworkManager.Device', 'DeviceType', dbus_interface='org.freedesktop.DBus.Properties'))
return klass.__new__(klass, object_path)
except ObjectVanished:
pass
return super(Device, klass).__new__(klass, object_path)
@staticmethod
def all():
return NetworkManager.Devices
def __eq__(self, other):
return isinstance(other, type(self)) and self.IpInterface == other.IpInterface
# Backwards compatibility method. Devices now auto-specialize, so this is
# no longer needed. But code may use it.
def SpecificDevice(self):
return self
def device_class(typ):
return {
NM_DEVICE_TYPE_ADSL: Adsl,
NM_DEVICE_TYPE_BOND: Bond,
NM_DEVICE_TYPE_BRIDGE: Bridge,
NM_DEVICE_TYPE_BT: Bluetooth,
NM_DEVICE_TYPE_ETHERNET: Wired,
NM_DEVICE_TYPE_GENERIC: Generic,
NM_DEVICE_TYPE_INFINIBAND: Infiniband,
NM_DEVICE_TYPE_IP_TUNNEL: IPTunnel,
NM_DEVICE_TYPE_MACVLAN: Macvlan,
NM_DEVICE_TYPE_MODEM: Modem,
NM_DEVICE_TYPE_OLPC_MESH: OlpcMesh,
NM_DEVICE_TYPE_TEAM: Team,
NM_DEVICE_TYPE_TUN: Tun,
NM_DEVICE_TYPE_VETH: Veth,
NM_DEVICE_TYPE_VLAN: Vlan,
NM_DEVICE_TYPE_VXLAN: Vxlan,
NM_DEVICE_TYPE_WIFI: Wireless,
NM_DEVICE_TYPE_WIMAX: Wimax,
NM_DEVICE_TYPE_MACSEC: MacSec,
NM_DEVICE_TYPE_DUMMY: Dummy,
NM_DEVICE_TYPE_PPP: PPP,
NM_DEVICE_TYPE_OVS_INTERFACE: OvsIf,
NM_DEVICE_TYPE_OVS_PORT: OvsPort,
NM_DEVICE_TYPE_OVS_BRIDGE: OvsBridge,
NM_DEVICE_TYPE_WIFIP2P: WifiP2P
}.get(typ, Generic)
class Adsl(Device): pass
class Bluetooth(Device): pass
class Bond(Device): pass
class Bridge(Device): pass
class Generic(Device): pass
class Infiniband(Device): pass
class IPTunnel(Device): pass
class Macvlan(Device): pass
class Modem(Device): pass
class OlpcMesh(Device): pass
class Team(Device): pass
class Tun(Device): pass
class Veth(Device): pass
class Vlan(Device): pass
class Vxlan(Device): pass
class Wimax(Device): pass
class Wired(Device): pass
class Wireless(Device): pass
class MacSec(Device): pass
class Dummy(Device): pass
class PPP(Device): pass
class OvsIf(Device): pass
class OvsPort(Device): pass
class OvsBridge(Device): pass
class WifiP2P(Device): pass
class NSP(TransientNMDbusInterface):
interface_names = ['org.freedesktop.NetworkManager.Wimax.NSP']
class AccessPoint(NMDbusInterface):
@staticmethod
def all():
for device in Device.all():
if isinstance(device, Wireless):
for ap in device.AccessPoints:
yield ap
def __eq__(self, other):
return isinstance(other, type(self)) and self.HwAddress == other.HwAddress
class IP4Config(TransientNMDbusInterface): pass
class IP6Config(TransientNMDbusInterface): pass
class DHCP4Config(TransientNMDbusInterface): pass
class DHCP6Config(TransientNMDbusInterface): pass
# Evil hack to work around not being able to specify a method name in the
# dbus.service.method decorator.
class SecretAgentType(type(dbus.service.Object)):
def __new__(type_, name, bases, attrs):
if bases != (dbus.service.Object,):
attrs['GetSecretsImpl'] = attrs.pop('GetSecrets')
return super(SecretAgentType, type_).__new__(type_, name, bases, attrs)
@six.add_metaclass(SecretAgentType)
class SecretAgent(dbus.service.Object):
object_path = '/org/freedesktop/NetworkManager/SecretAgent'
interface_name = 'org.freedesktop.NetworkManager.SecretAgent'
def __init__(self, identifier):
self.identifier = identifier
dbus.service.Object.__init__(self, dbus.SystemBus(), self.object_path)
AgentManager.Register(self.identifier)
@dbus.service.method(dbus_interface=interface_name, in_signature='a{sa{sv}}osasu', out_signature='a{sa{sv}}')
def GetSecrets(self, connection, connection_path, setting_name, hints, flags):
settings = fixups.to_python('SecretAgent', 'GetSecrets', 'connection', connection, 'a{sa{sv}}')
connection = fixups.to_python('SecretAgent', 'GetSecrets', 'connection_path', connection_path, 'o')
setting_name = fixups.to_python('SecretAgent', 'GetSecrets', 'setting_name', setting_name, 's')
hints = fixups.to_python('SecretAgent', 'GetSecrets', 'hints', hints, 'as')
return self.GetSecretsImpl(settings, connection, setting_name, hints, flags)
# These two are interfaces that must be provided to NetworkManager. Keep them
# as comments for documentation purposes.
#
# class PPP(NMDbusInterface): pass
# class VPNPlugin(NMDbusInterface):
# interface_names = ['org.freedesktop.NetworkManager.VPN.Plugin']
def const(prefix, val):
prefix = 'NM_' + prefix.upper() + '_'
for key, vval in globals().items():
if 'REASON' in key and 'REASON' not in prefix:
continue
if key.startswith(prefix) and val == vval:
return key.replace(prefix,'').lower()
raise ValueError("No constant found for %s* with value %d", (prefix, val))
# Several fixer methods to make the data easier to handle in python
# - SSID sent/returned as bytes (only encoding tried is utf-8)
# - IP, Mac address and route metric encoding/decoding
class fixups(object):
@staticmethod
def to_dbus(klass, method, arg, val, signature):
if arg in ('connection' 'properties') and signature == 'a{sa{sv}}':
settings = copy.deepcopy(val)
for key in settings:
if 'mac-address' in settings[key]:
settings[key]['mac-address'] = fixups.mac_to_dbus(settings[key]['mac-address'])
if 'cloned-mac-address' in settings[key]:
settings[key]['cloned-mac-address'] = fixups.mac_to_dbus(settings[key]['cloned-mac-address'])
if 'bssid' in settings[key]:
settings[key]['bssid'] = fixups.mac_to_dbus(settings[key]['bssid'])
for cert in ['ca-cert', 'client-cert', 'phase2-ca-cert', 'phase2-client-cert', 'private-key']:
if cert in settings[key]:
settings[key][cert] = fixups.cert_to_dbus(settings[key][cert])
if 'ssid' in settings.get('802-11-wireless', {}):
settings['802-11-wireless']['ssid'] = fixups.ssid_to_dbus(settings['802-11-wireless']['ssid'])
if 'ipv4' in settings:
if 'address-data' in settings['ipv4']:
for item in settings['ipv4']['address-data']:
item['prefix'] = dbus.UInt32(item['prefix'])
settings['ipv4']['address-data'] = dbus.Array(
settings['ipv4']['address-data'],
signature=dbus.Signature('a{sv}'))
if 'addresses' in settings['ipv4']:
settings['ipv4']['addresses'] = [fixups.addrconf_to_dbus(addr,socket.AF_INET) for addr in settings['ipv4']['addresses']]
if 'routes' in settings['ipv4']:
settings['ipv4']['routes'] = [fixups.route_to_dbus(route,socket.AF_INET) for route in settings['ipv4']['routes']]
if 'dns' in settings['ipv4']:
settings['ipv4']['dns'] = [fixups.addr_to_dbus(addr,socket.AF_INET) for addr in settings['ipv4']['dns']]
if 'ipv6' in settings:
if 'addresses' in settings['ipv6']:
settings['ipv6']['addresses'] = [fixups.addrconf_to_dbus(addr,socket.AF_INET6) for addr in settings['ipv6']['addresses']]
if 'routes' in settings['ipv6']:
settings['ipv6']['routes'] = [fixups.route_to_dbus(route,socket.AF_INET6) for route in settings['ipv6']['routes']]
if 'dns' in settings['ipv6']:
settings['ipv6']['dns'] = [fixups.addr_to_dbus(addr,socket.AF_INET6) for addr in settings['ipv6']['dns']]
# Get rid of empty arrays/dicts. dbus barfs on them (can't guess
# signatures), and if they were to get through, NetworkManager
# ignores them anyway.
for key in list(settings.keys()):
if isinstance(settings[key], dict):
for key2 in list(settings[key].keys()):
if settings[key][key2] in ({}, []):
del settings[key][key2]
if settings[key] in ({}, []):
del settings[key]
val = settings
return fixups.base_to_dbus(val)
@staticmethod
def base_to_dbus(val):
if isinstance(val, NMDbusInterface):
return val.object_path
if hasattr(val.__class__, 'mro'):
for klass in val.__class__.mro():
if klass.__module__ in ('dbus', '_dbus_bindings'):
return val
if hasattr(val, '__iter__') and not isinstance(val, six.string_types):
if hasattr(val, 'items'):
return dict([(x, fixups.base_to_dbus(y)) for x, y in val.items()])
else:
return [fixups.base_to_dbus(x) for x in val]
return val
@staticmethod
def to_python(klass, method, arg, val, signature):
val = fixups.base_to_python(val)
klass_af = {'IP4Config': socket.AF_INET, 'IP6Config': socket.AF_INET6}.get(klass, socket.AF_INET)
if method == 'Get':
if arg == 'Ip4Address':
return fixups.addr_to_python(val, socket.AF_INET)
if arg == 'Ip6Address':
return fixups.addr_to_python(val, socket.AF_INET6)
if arg == 'Ssid':
return fixups.ssid_to_python(val)
if arg == 'Strength':
return fixups.strength_to_python(val)
if arg == 'Addresses':
return [fixups.addrconf_to_python(addr, klass_af) for addr in val]
if arg == 'Routes':
return [fixups.route_to_python(route, klass_af) for route in val]
if arg in ('Nameservers', 'WinsServers'):
return [fixups.addr_to_python(addr, klass_af) for addr in val]
if arg == 'Options':
for key in val:
if key.startswith('requested_'):
val[key] = bool(int(val[key]))
elif val[key].isdigit():
val[key] = int(val[key])
elif key in ('domain_name_servers', 'ntp_servers', 'routers'):
val[key] = val[key].split()
return val
if method == 'GetSettings':
if 'ssid' in val.get('802-11-wireless', {}):
val['802-11-wireless']['ssid'] = fixups.ssid_to_python(val['802-11-wireless']['ssid'])
for key in val:
val_ = val[key]
if 'mac-address' in val_:
val_['mac-address'] = fixups.mac_to_python(val_['mac-address'])
if 'cloned-mac-address' in val_:
val_['cloned-mac-address'] = fixups.mac_to_python(val_['cloned-mac-address'])
if 'bssid' in val_:
val_['bssid'] = fixups.mac_to_python(val_['bssid'])
if 'ipv4' in val:
val['ipv4']['addresses'] = [fixups.addrconf_to_python(addr,socket.AF_INET) for addr in val['ipv4']['addresses']]
val['ipv4']['routes'] = [fixups.route_to_python(route,socket.AF_INET) for route in val['ipv4']['routes']]
val['ipv4']['dns'] = [fixups.addr_to_python(addr,socket.AF_INET) for addr in val['ipv4']['dns']]
if 'ipv6' in val:
val['ipv6']['addresses'] = [fixups.addrconf_to_python(addr,socket.AF_INET6) for addr in val['ipv6']['addresses']]
val['ipv6']['routes'] = [fixups.route_to_python(route,socket.AF_INET6) for route in val['ipv6']['routes']]
val['ipv6']['dns'] = [fixups.addr_to_python(addr,socket.AF_INET6) for addr in val['ipv6']['dns']]
return val
if method == 'PropertiesChanged':
for prop in val:
val[prop] = fixups.to_python(klass, 'Get', prop, val[prop], None)
return val
@staticmethod
def base_to_python(val):
if isinstance(val, dbus.ByteArray):
return "".join([str(x) for x in val])
if isinstance(val, (dbus.Array, list, tuple)):
return [fixups.base_to_python(x) for x in val]
if isinstance(val, (dbus.Dictionary, dict)):
return dict([(fixups.base_to_python(x), fixups.base_to_python(y)) for x,y in val.items()])
if isinstance(val, dbus.ObjectPath):
for obj in (NetworkManager, Settings, AgentManager):
if val == obj.object_path:
return obj
if val.startswith('/org/freedesktop/NetworkManager/'):
classname = val.split('/')[4]
classname = {
'Settings': 'Connection',
'Devices': 'Device',
}.get(classname, classname)
return globals()[classname](val)
if val == '/':
return None
if isinstance(val, (dbus.Signature, dbus.String)):
return six.text_type(val)
if isinstance(val, dbus.Boolean):
return bool(val)
if isinstance(val, (dbus.Int16, dbus.UInt16, dbus.Int32, dbus.UInt32, dbus.Int64, dbus.UInt64)):
return int(val)
if isinstance(val, dbus.Byte):
return six.int2byte(int(val))
return val
@staticmethod
def ssid_to_python(ssid):
try:
return bytes().join(ssid).decode('utf-8')
except UnicodeDecodeError:
ssid = bytes().join(ssid).decode('utf-8', 'replace')
warnings.warn("Unable to decode ssid %s properly" % ssid, UnicodeWarning)
return ssid
@staticmethod
def ssid_to_dbus(ssid):
if isinstance(ssid, six.text_type):
ssid = ssid.encode('utf-8')
return [dbus.Byte(x) for x in ssid]
@staticmethod
def strength_to_python(strength):
return struct.unpack('B', strength)[0]
@staticmethod
def mac_to_python(mac):
return "%02X:%02X:%02X:%02X:%02X:%02X" % tuple([ord(x) for x in mac])
@staticmethod
def mac_to_dbus(mac):
return [dbus.Byte(int(x, 16)) for x in mac.split(':')]
@staticmethod
def addrconf_to_python(addrconf,family):
addr, netmask, gateway = addrconf
return [
fixups.addr_to_python(addr,family),
netmask,
fixups.addr_to_python(gateway,family)
]
@staticmethod
def addrconf_to_dbus(addrconf,family):
addr, netmask, gateway = addrconf
if (family == socket.AF_INET):
return [
fixups.addr_to_dbus(addr,family),
fixups.mask_to_dbus(netmask),
fixups.addr_to_dbus(gateway,family)
]
else:
return dbus.Struct(
(
fixups.addr_to_dbus(addr,family),
fixups.mask_to_dbus(netmask),
fixups.addr_to_dbus(gateway,family)
), signature = 'ayuay'
)
@staticmethod
def addr_to_python(addr,family):
if (family == socket.AF_INET):
return socket.inet_ntop(family,struct.pack('I', addr))
else:
return socket.inet_ntop(family,b''.join(addr))
@staticmethod
def addr_to_dbus(addr,family):
if (family == socket.AF_INET):
return dbus.UInt32(struct.unpack('I', socket.inet_pton(family,addr))[0])
else:
return dbus.ByteArray(socket.inet_pton(family,addr))
@staticmethod
def mask_to_dbus(mask):
return dbus.UInt32(mask)
@staticmethod
def route_to_python(route,family):
addr, netmask, gateway, metric = route
return [
fixups.addr_to_python(addr,family),
netmask,
fixups.addr_to_python(gateway,family),
metric
]
@staticmethod
def route_to_dbus(route,family):
addr, netmask, gateway, metric = route
return [
fixups.addr_to_dbus(addr,family),
fixups.mask_to_dbus(netmask),
fixups.addr_to_dbus(gateway,family),
metric
]
@staticmethod
def cert_to_dbus(cert):
if not isinstance(cert, bytes):
if not cert.startswith('file://'):
cert = 'file://' + cert
cert = cert.encode('utf-8') + b'\0'
return [dbus.Byte(x) for x in cert]
# Turn NetworkManager and Settings into singleton objects
NetworkManager = NetworkManager()
Settings = Settings()
AgentManager = AgentManager()
init_bus.close()
del init_bus
del xml_cache
# Constants below are generated with makeconstants.py. Do not edit manually.
NM_CAPABILITY_TEAM = 1
NM_STATE_UNKNOWN = 0
NM_STATE_ASLEEP = 10
NM_STATE_DISCONNECTED = 20
NM_STATE_DISCONNECTING = 30
NM_STATE_CONNECTING = 40
NM_STATE_CONNECTED_LOCAL = 50
NM_STATE_CONNECTED_SITE = 60
NM_STATE_CONNECTED_GLOBAL = 70
NM_CONNECTIVITY_UNKNOWN = 0
NM_CONNECTIVITY_NONE = 1
NM_CONNECTIVITY_PORTAL = 2
NM_CONNECTIVITY_LIMITED = 3
NM_CONNECTIVITY_FULL = 4
NM_DEVICE_TYPE_UNKNOWN = 0
NM_DEVICE_TYPE_ETHERNET = 1
NM_DEVICE_TYPE_WIFI = 2
NM_DEVICE_TYPE_UNUSED1 = 3
NM_DEVICE_TYPE_UNUSED2 = 4
NM_DEVICE_TYPE_BT = 5
NM_DEVICE_TYPE_OLPC_MESH = 6
NM_DEVICE_TYPE_WIMAX = 7
NM_DEVICE_TYPE_MODEM = 8
NM_DEVICE_TYPE_INFINIBAND = 9
NM_DEVICE_TYPE_BOND = 10
NM_DEVICE_TYPE_VLAN = 11
NM_DEVICE_TYPE_ADSL = 12
NM_DEVICE_TYPE_BRIDGE = 13
NM_DEVICE_TYPE_GENERIC = 14
NM_DEVICE_TYPE_TEAM = 15
NM_DEVICE_TYPE_TUN = 16
NM_DEVICE_TYPE_IP_TUNNEL = 17
NM_DEVICE_TYPE_MACVLAN = 18
NM_DEVICE_TYPE_VXLAN = 19
NM_DEVICE_TYPE_VETH = 20
NM_DEVICE_TYPE_MACSEC = 21
NM_DEVICE_TYPE_DUMMY = 22
NM_DEVICE_TYPE_PPP = 23
NM_DEVICE_TYPE_OVS_INTERFACE = 24
NM_DEVICE_TYPE_OVS_PORT = 25
NM_DEVICE_TYPE_OVS_BRIDGE = 26
NM_DEVICE_TYPE_WIFIP2P = 30
NM_DEVICE_CAP_NONE = 0
NM_DEVICE_CAP_NM_SUPPORTED = 1
NM_DEVICE_CAP_CARRIER_DETECT = 2
NM_DEVICE_CAP_IS_SOFTWARE = 4
NM_DEVICE_CAP_SRIOV = 8
NM_WIFI_DEVICE_CAP_NONE = 0
NM_WIFI_DEVICE_CAP_CIPHER_WEP40 = 1
NM_WIFI_DEVICE_CAP_CIPHER_WEP104 = 2
NM_WIFI_DEVICE_CAP_CIPHER_TKIP = 4
NM_WIFI_DEVICE_CAP_CIPHER_CCMP = 8
NM_WIFI_DEVICE_CAP_WPA = 16
NM_WIFI_DEVICE_CAP_RSN = 32
NM_WIFI_DEVICE_CAP_AP = 64
NM_WIFI_DEVICE_CAP_ADHOC = 128
NM_WIFI_DEVICE_CAP_FREQ_VALID = 256
NM_WIFI_DEVICE_CAP_FREQ_2GHZ = 512
NM_WIFI_DEVICE_CAP_FREQ_5GHZ = 1024
NM_802_11_AP_FLAGS_NONE = 0
NM_802_11_AP_FLAGS_PRIVACY = 1
NM_802_11_AP_FLAGS_WPS = 2
NM_802_11_AP_FLAGS_WPS_PBC = 4
NM_802_11_AP_FLAGS_WPS_PIN = 8
NM_802_11_AP_SEC_NONE = 0
NM_802_11_AP_SEC_PAIR_WEP40 = 1
NM_802_11_AP_SEC_PAIR_WEP104 = 2
NM_802_11_AP_SEC_PAIR_TKIP = 4
NM_802_11_AP_SEC_PAIR_CCMP = 8
NM_802_11_AP_SEC_GROUP_WEP40 = 16
NM_802_11_AP_SEC_GROUP_WEP104 = 32
NM_802_11_AP_SEC_GROUP_TKIP = 64
NM_802_11_AP_SEC_GROUP_CCMP = 128
NM_802_11_AP_SEC_KEY_MGMT_PSK = 256
NM_802_11_AP_SEC_KEY_MGMT_802_1X = 512
NM_802_11_MODE_UNKNOWN = 0
NM_802_11_MODE_ADHOC = 1
NM_802_11_MODE_INFRA = 2
NM_802_11_MODE_AP = 3
NM_BT_CAPABILITY_NONE = 0
NM_BT_CAPABILITY_DUN = 1
NM_BT_CAPABILITY_NAP = 2
NM_DEVICE_MODEM_CAPABILITY_NONE = 0
NM_DEVICE_MODEM_CAPABILITY_POTS = 1
NM_DEVICE_MODEM_CAPABILITY_CDMA_EVDO = 2
NM_DEVICE_MODEM_CAPABILITY_GSM_UMTS = 4
NM_DEVICE_MODEM_CAPABILITY_LTE = 8
NM_WIMAX_NSP_NETWORK_TYPE_UNKNOWN = 0
NM_WIMAX_NSP_NETWORK_TYPE_HOME = 1
NM_WIMAX_NSP_NETWORK_TYPE_PARTNER = 2
NM_WIMAX_NSP_NETWORK_TYPE_ROAMING_PARTNER = 3
NM_DEVICE_STATE_UNKNOWN = 0
NM_DEVICE_STATE_UNMANAGED = 10
NM_DEVICE_STATE_UNAVAILABLE = 20
NM_DEVICE_STATE_DISCONNECTED = 30
NM_DEVICE_STATE_PREPARE = 40
NM_DEVICE_STATE_CONFIG = 50
NM_DEVICE_STATE_NEED_AUTH = 60
NM_DEVICE_STATE_IP_CONFIG = 70
NM_DEVICE_STATE_IP_CHECK = 80
NM_DEVICE_STATE_SECONDARIES = 90
NM_DEVICE_STATE_ACTIVATED = 100
NM_DEVICE_STATE_DEACTIVATING = 110
NM_DEVICE_STATE_FAILED = 120
NM_DEVICE_STATE_REASON_NONE = 0
NM_DEVICE_STATE_REASON_UNKNOWN = 1
NM_DEVICE_STATE_REASON_NOW_MANAGED = 2
NM_DEVICE_STATE_REASON_NOW_UNMANAGED = 3
NM_DEVICE_STATE_REASON_CONFIG_FAILED = 4
NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE = 5
NM_DEVICE_STATE_REASON_IP_CONFIG_EXPIRED = 6
NM_DEVICE_STATE_REASON_NO_SECRETS = 7
NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8
NM_DEVICE_STATE_REASON_SUPPLICANT_CONFIG_FAILED = 9
NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED = 10
NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT = 11
NM_DEVICE_STATE_REASON_PPP_START_FAILED = 12
NM_DEVICE_STATE_REASON_PPP_DISCONNECT = 13
NM_DEVICE_STATE_REASON_PPP_FAILED = 14
NM_DEVICE_STATE_REASON_DHCP_START_FAILED = 15
NM_DEVICE_STATE_REASON_DHCP_ERROR = 16
NM_DEVICE_STATE_REASON_DHCP_FAILED = 17
NM_DEVICE_STATE_REASON_SHARED_START_FAILED = 18
NM_DEVICE_STATE_REASON_SHARED_FAILED = 19
NM_DEVICE_STATE_REASON_AUTOIP_START_FAILED = 20
NM_DEVICE_STATE_REASON_AUTOIP_ERROR = 21
NM_DEVICE_STATE_REASON_AUTOIP_FAILED = 22
NM_DEVICE_STATE_REASON_MODEM_BUSY = 23
NM_DEVICE_STATE_REASON_MODEM_NO_DIAL_TONE = 24
NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER = 25
NM_DEVICE_STATE_REASON_MODEM_DIAL_TIMEOUT = 26
NM_DEVICE_STATE_REASON_MODEM_DIAL_FAILED = 27
NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED = 28
NM_DEVICE_STATE_REASON_GSM_APN_FAILED = 29
NM_DEVICE_STATE_REASON_GSM_REGISTRATION_NOT_SEARCHING = 30
NM_DEVICE_STATE_REASON_GSM_REGISTRATION_DENIED = 31
NM_DEVICE_STATE_REASON_GSM_REGISTRATION_TIMEOUT = 32
NM_DEVICE_STATE_REASON_GSM_REGISTRATION_FAILED = 33
NM_DEVICE_STATE_REASON_GSM_PIN_CHECK_FAILED = 34
NM_DEVICE_STATE_REASON_FIRMWARE_MISSING = 35
NM_DEVICE_STATE_REASON_REMOVED = 36
NM_DEVICE_STATE_REASON_SLEEPING = 37
NM_DEVICE_STATE_REASON_CONNECTION_REMOVED = 38
NM_DEVICE_STATE_REASON_USER_REQUESTED = 39
NM_DEVICE_STATE_REASON_CARRIER = 40
NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED = 41
NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE = 42
NM_DEVICE_STATE_REASON_MODEM_NOT_FOUND = 43
NM_DEVICE_STATE_REASON_BT_FAILED = 44
NM_DEVICE_STATE_REASON_GSM_SIM_NOT_INSERTED = 45
NM_DEVICE_STATE_REASON_GSM_SIM_PIN_REQUIRED = 46
NM_DEVICE_STATE_REASON_GSM_SIM_PUK_REQUIRED = 47
NM_DEVICE_STATE_REASON_GSM_SIM_WRONG = 48
NM_DEVICE_STATE_REASON_INFINIBAND_MODE = 49
NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED = 50
NM_DEVICE_STATE_REASON_BR2684_FAILED = 51
NM_DEVICE_STATE_REASON_MODEM_MANAGER_UNAVAILABLE = 52
NM_DEVICE_STATE_REASON_SSID_NOT_FOUND = 53
NM_DEVICE_STATE_REASON_SECONDARY_CONNECTION_FAILED = 54
NM_DEVICE_STATE_REASON_DCB_FCOE_FAILED = 55
NM_DEVICE_STATE_REASON_TEAMD_CONTROL_FAILED = 56
NM_DEVICE_STATE_REASON_MODEM_FAILED = 57
NM_DEVICE_STATE_REASON_MODEM_AVAILABLE = 58
NM_DEVICE_STATE_REASON_SIM_PIN_INCORRECT = 59
NM_DEVICE_STATE_REASON_NEW_ACTIVATION = 60
NM_DEVICE_STATE_REASON_PARENT_CHANGED = 61
NM_DEVICE_STATE_REASON_PARENT_MANAGED_CHANGED = 62
NM_DEVICE_STATE_REASON_OVSDB_FAILED = 63
NM_DEVICE_STATE_REASON_IP_ADDRESS_DUPLICATE = 64
NM_DEVICE_STATE_REASON_IP_METHOD_UNSUPPORTED = 65
NM_METERED_UNKNOWN = 0
NM_METERED_YES = 1
NM_METERED_NO = 2
NM_METERED_GUESS_YES = 3
NM_METERED_GUESS_NO = 4
NM_ACTIVE_CONNECTION_STATE_UNKNOWN = 0
NM_ACTIVE_CONNECTION_STATE_ACTIVATING = 1
NM_ACTIVE_CONNECTION_STATE_ACTIVATED = 2
NM_ACTIVE_CONNECTION_STATE_DEACTIVATING = 3
NM_ACTIVE_CONNECTION_STATE_DEACTIVATED = 4
NM_ACTIVE_CONNECTION_STATE_REASON_UNKNOWN = 0
NM_ACTIVE_CONNECTION_STATE_REASON_NONE = 1
NM_ACTIVE_CONNECTION_STATE_REASON_USER_DISCONNECTED = 2
NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED = 3
NM_ACTIVE_CONNECTION_STATE_REASON_SERVICE_STOPPED = 4
NM_ACTIVE_CONNECTION_STATE_REASON_IP_CONFIG_INVALID = 5
NM_ACTIVE_CONNECTION_STATE_REASON_CONNECT_TIMEOUT = 6
NM_ACTIVE_CONNECTION_STATE_REASON_SERVICE_START_TIMEOUT = 7
NM_ACTIVE_CONNECTION_STATE_REASON_SERVICE_START_FAILED = 8
NM_ACTIVE_CONNECTION_STATE_REASON_NO_SECRETS = 9
NM_ACTIVE_CONNECTION_STATE_REASON_LOGIN_FAILED = 10
NM_ACTIVE_CONNECTION_STATE_REASON_CONNECTION_REMOVED = 11
NM_ACTIVE_CONNECTION_STATE_REASON_DEPENDENCY_FAILED = 12
NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_REALIZE_FAILED = 13
NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_REMOVED = 14
NM_SECRET_AGENT_GET_SECRETS_FLAG_NONE = 0
NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION = 1
NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW = 2
NM_SECRET_AGENT_GET_SECRETS_FLAG_USER_REQUESTED = 4
NM_SECRET_AGENT_GET_SECRETS_FLAG_WPS_PBC_ACTIVE = 8
NM_SECRET_AGENT_GET_SECRETS_FLAG_ONLY_SYSTEM = 2147483648
NM_SECRET_AGENT_GET_SECRETS_FLAG_NO_ERRORS = 1073741824
NM_IP_TUNNEL_MODE_UNKNOWN = 0
NM_IP_TUNNEL_MODE_IPIP = 1
NM_IP_TUNNEL_MODE_GRE = 2
NM_IP_TUNNEL_MODE_SIT = 3
NM_IP_TUNNEL_MODE_ISATAP = 4
NM_IP_TUNNEL_MODE_VTI = 5
NM_IP_TUNNEL_MODE_IP6IP6 = 6
NM_IP_TUNNEL_MODE_IPIP6 = 7
NM_IP_TUNNEL_MODE_IP6GRE = 8
NM_IP_TUNNEL_MODE_VTI6 = 9
NM_CHECKPOINT_CREATE_FLAG_NONE = 0
NM_CHECKPOINT_CREATE_FLAG_DESTROY_ALL = 1
NM_CHECKPOINT_CREATE_FLAG_DELETE_NEW_CONNECTIONS = 2
NM_CHECKPOINT_CREATE_FLAG_DISCONNECT_NEW_DEVICES = 4
NM_ROLLBACK_RESULT_OK = 0
NM_ROLLBACK_RESULT_ERR_NO_DEVICE = 1
NM_ROLLBACK_RESULT_ERR_DEVICE_UNMANAGED = 2
NM_ROLLBACK_RESULT_ERR_FAILED = 3
NM_ACTIVATION_STATE_FLAG_NONE = 0
NM_ACTIVATION_STATE_FLAG_IS_MASTER = 1
NM_ACTIVATION_STATE_FLAG_IS_SLAVE = 2
NM_ACTIVATION_STATE_FLAG_LAYER2_READY = 4
NM_ACTIVATION_STATE_FLAG_IP4_READY = 8
NM_ACTIVATION_STATE_FLAG_IP6_READY = 16
NM_ACTIVATION_STATE_FLAG_MASTER_HAS_SLAVES = 32
NM_SETTINGS_UPDATE2_FLAG_NONE = 0
NM_SETTINGS_UPDATE2_FLAG_TO_DISK = 1
NM_SETTINGS_UPDATE2_FLAG_IN_MEMORY = 2
NM_SETTINGS_UPDATE2_FLAG_IN_MEMORY_DETACHED = 4
NM_SETTINGS_UPDATE2_FLAG_IN_MEMORY_ONLY = 8
NM_SETTINGS_UPDATE2_FLAG_VOLATILE = 16
NM_SETTINGS_UPDATE2_FLAG_BLOCK_AUTOCONNECT = 32
NM_VPN_SERVICE_STATE_UNKNOWN = 0
NM_VPN_SERVICE_STATE_INIT = 1
NM_VPN_SERVICE_STATE_SHUTDOWN = 2
NM_VPN_SERVICE_STATE_STARTING = 3
NM_VPN_SERVICE_STATE_STARTED = 4
NM_VPN_SERVICE_STATE_STOPPING = 5
NM_VPN_SERVICE_STATE_STOPPED = 6
NM_VPN_CONNECTION_STATE_UNKNOWN = 0
NM_VPN_CONNECTION_STATE_PREPARE = 1
NM_VPN_CONNECTION_STATE_NEED_AUTH = 2
NM_VPN_CONNECTION_STATE_CONNECT = 3
NM_VPN_CONNECTION_STATE_IP_CONFIG_GET = 4
NM_VPN_CONNECTION_STATE_ACTIVATED = 5
NM_VPN_CONNECTION_STATE_FAILED = 6
NM_VPN_CONNECTION_STATE_DISCONNECTED = 7
NM_VPN_CONNECTION_STATE_REASON_UNKNOWN = 0
NM_VPN_CONNECTION_STATE_REASON_NONE = 1
NM_VPN_CONNECTION_STATE_REASON_USER_DISCONNECTED = 2
NM_VPN_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED = 3
NM_VPN_CONNECTION_STATE_REASON_SERVICE_STOPPED = 4
NM_VPN_CONNECTION_STATE_REASON_IP_CONFIG_INVALID = 5
NM_VPN_CONNECTION_STATE_REASON_CONNECT_TIMEOUT = 6
NM_VPN_CONNECTION_STATE_REASON_SERVICE_START_TIMEOUT = 7
NM_VPN_CONNECTION_STATE_REASON_SERVICE_START_FAILED = 8
NM_VPN_CONNECTION_STATE_REASON_NO_SECRETS = 9
NM_VPN_CONNECTION_STATE_REASON_LOGIN_FAILED = 10
NM_VPN_CONNECTION_STATE_REASON_CONNECTION_REMOVED = 11
NM_VPN_PLUGIN_FAILURE_LOGIN_FAILED = 0
NM_VPN_PLUGIN_FAILURE_CONNECT_FAILED = 1
NM_VPN_PLUGIN_FAILURE_BAD_IP_CONFIG = 2
NM_SECRET_AGENT_ERROR_NOT_AUTHORIZED = 0
NM_SECRET_AGENT_ERROR_INVALID_CONNECTION = 1
NM_SECRET_AGENT_ERROR_USER_CANCELED = 2
NM_SECRET_AGENT_ERROR_AGENT_CANCELED = 3
NM_SECRET_AGENT_ERROR_INTERNAL_ERROR = 4
NM_SECRET_AGENT_ERROR_NO_SECRETS = 5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment