You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1793 lines
59 KiB

#!/usr/bin/python
# -*- coding: utf-8 -*-
# TODO:
# - IPv6
# - Proxy
# - Fix connman's PropertyChanged when changing off/manual -> dhcp,
# gateway is not updated.
import dbus
import dbus.service
import logging
import argparse
import os.path
9 years ago
# For python2 backwards compatibility
try:
import configparser
except ImportError:
import ConfigParser as configparser
try:
import efl.evas as evas
import efl.ecore as ecore
9 years ago
import efl.edje
from efl.dbus_mainloop import DBusEcoreMainLoop
import efl.elementary as elm
from efl.elementary import ELM_POLICY_QUIT, \
ELM_POLICY_QUIT_LAST_WINDOW_CLOSED
from efl.elementary.window import Window, ELM_WIN_BASIC, \
ELM_WIN_DIALOG_BASIC
from efl.elementary.background import Background
from efl.elementary.box import Box
from efl.elementary.label import Label
from efl.elementary.naviframe import Naviframe
from efl.elementary.popup import Popup
from efl.elementary.button import Button
from efl.elementary.scroller import Scroller, ELM_SCROLLER_POLICY_OFF, \
ELM_SCROLLER_POLICY_AUTO
from efl.elementary.check import Check
from efl.elementary.progressbar import Progressbar
from efl.elementary.genlist import Genlist, GenlistItemClass
from efl.elementary.segment_control import SegmentControl
from efl.elementary.frame import Frame
from efl.elementary.entry import Entry
from efl.elementary.icon import Icon
from efl.elementary.layout import Layout
from efl.elementary.theme import Theme
except:
import elementary as elm
9 years ago
import evas
import ecore
import edje
from e_dbus import DBusEcoreMainLoop
from elementary import Window, Background, Box, Label, Naviframe, Popup, \
Button, Scroller, Check, Progressbar, Genlist, GenlistItemClass, \
SegmentControl, Frame, Entry, Icon, Layout, Theme, ELM_WIN_BASIC, \
ELM_WIN_DIALOG_BASIC, ELM_POLICY_QUIT, ELM_SCROLLER_POLICY_OFF, \
ELM_SCROLLER_POLICY_AUTO, ELM_POLICY_QUIT_LAST_WINDOW_CLOSED
dbus_ml = DBusEcoreMainLoop()
bus = dbus.SystemBus(mainloop=dbus_ml)
log = logging.getLogger("econnman")
log_handler = logging.StreamHandler()
log_formatter = logging.Formatter(
"%(relativeCreated)d %(levelname)s %(name)s: %(message)s"
)
log_handler.setFormatter(log_formatter)
log.addHandler(log_handler)
manager = None
CONF_FILE = "/var/lib/connman/econnman.config"
configs = None
EXPAND_BOTH = (evas.EVAS_HINT_EXPAND, evas.EVAS_HINT_EXPAND)
EXPAND_HORIZ = (evas.EVAS_HINT_EXPAND, 0.0)
FILL_BOTH = (evas.EVAS_HINT_FILL, evas.EVAS_HINT_FILL)
9 years ago
########################################################################
# Debug helpers:
def dbus_variant_to_str(v):
if isinstance(v, dbus.String):
v = '"%s"' % (str(v),)
elif isinstance(v, dbus.Boolean):
v = str(bool(v))
elif isinstance(v, (dbus.Dictionary, dbus.Struct)):
v = "{%s}" % (dbus_dict_to_str(v),)
elif isinstance(v, dbus.Array):
v = "[%s]" % (dbus_array_to_str(v),)
elif isinstance(v, dbus.ObjectPath):
v = str(v)
elif isinstance(v, (dbus.Byte, dbus.Int16, dbus.Int32, dbus.Int64,
dbus.UInt16, dbus.UInt32, dbus.UInt64)):
v = int(v)
elif isinstance(v, dbus.Double):
v = float(v)
else:
v = repr(v)
return v
9 years ago
def dbus_dict_to_str(d):
9 years ago
"""Help debug by converting a dbus.Dictionary to a string in a shorter
form.
"""
s = []
for k, v in d.items():
s.append("%s=%s" % (k, dbus_variant_to_str(v)))
return ", ".join(s)
9 years ago
def dbus_array_to_str(a):
9 years ago
"""Help debug by converting a complex structure to a string in shorter
form.
"""
return ", ".join(dbus_variant_to_str(x) for x in a)
9 years ago
def dbus_array_of_dict_to_str(a):
"""Help debug by converting a complex structure to a string in a
shorter form with only the keys, not the value.
"""
s = []
for k, v in a:
s.append(str(k))
return ", ".join(s)
class ObjectView(object):
"""Base for viewing a complex object.
Implementors must set:
- bus_interface: to assign to self.bus_obj
- create_view(properties): to create the specific view widgets
- on_property_changed(name, value): to update view widgets
Provided automatically by this class:
- path: object path
- bus_obj: proxy object with specific interface to remote bus object
- obj: main toplevel view object
- box: main toplevel view box
"""
bus_interface = None
def __init__(self, parent, path, properties):
self.path = path
self.bus_obj = dbus.Interface(bus.get_object("net.connman", path),
self.bus_interface)
self.sig_ch = self.bus_obj.connect_to_signal("PropertyChanged",
self.on_property_changed)
self.obj = Scroller(parent)
self.obj.on_del_add(self._deleted)
self.obj.size_hint_weight = EXPAND_BOTH
self.obj.policy_set(ELM_SCROLLER_POLICY_OFF,
ELM_SCROLLER_POLICY_AUTO)
self.obj.bounce_set(False, True)
self.obj.content_min_limit(True, False)
self.box = Box(self.obj)
self.box.size_hint_weight = EXPAND_HORIZ
self.box.horizontal = False
self.create_view(properties)
self.obj.content = self.box
for k in properties:
self.on_property_changed(k, properties[k])
def _deleted(self, obj):
log.debug("View deleted %s (%s)", self.__class__.__name__, self.path)
self.sig_ch.remove()
self.bus_obj = None
self.sig_ch = None
self.obj = None
def create_view(self, properties):
log.critical("must be implemented!")
pass
def on_property_changed(self, name, value):
log.critical("must be implemented!")
def add_check(self, box, label, callback=None):
obj = Check(box)
obj.size_hint_weight = EXPAND_HORIZ
obj.size_hint_align = FILL_BOTH
obj.text = label
obj.show()
box.pack_end(obj)
if callback:
obj.callback_changed_add(callback)
return obj
def add_button(self, box, label, callback):
obj = Button(box)
obj.size_hint_weight = EXPAND_HORIZ
obj.size_hint_align = FILL_BOTH
obj.text = label
obj.show()
obj.callback_clicked_add(callback)
box.pack_end(obj)
return obj
def add_label(self, box, label):
lb = Label(box)
lb.size_hint_weight = EXPAND_HORIZ
lb.size_hint_align = FILL_BOTH
lb.text = label
lb.show()
box.pack_end(lb)
return lb
def add_progress(self, box, label):
pb = Progressbar(box)
pb.size_hint_weight = EXPAND_HORIZ
pb.size_hint_align = FILL_BOTH
pb.text = label
pb.show()
box.pack_end(pb)
return pb
def add_segment_control(self, box, options, callback):
sc = SegmentControl(box)
sc.size_hint_weight = EXPAND_HORIZ
sc.size_hint_align = FILL_BOTH
items = {}
for o in options:
items[o] = sc.item_add(None, o)
sc.show()
box.pack_end(sc)
sc.callback_changed_add(callback)
return sc, items
def add_frame_and_box(self, box, label):
fr = Frame(box)
fr.size_hint_weight = EXPAND_HORIZ
fr.size_hint_align = FILL_BOTH
fr.text = label
fr.show()
box.pack_end(fr)
bx = Box(fr)
bx.size_hint_weight = EXPAND_HORIZ
bx.size_hint_align = FILL_BOTH
bx.horizontal = False
bx.show()
fr.content = bx
return fr, bx
def add_label_and_entry(self, box, label, callback=None):
lb = self.add_label(box, label)
en = Entry(box)
en.size_hint_weight = EXPAND_HORIZ
en.size_hint_align = FILL_BOTH
en.single_line = True
en.scrollable = True
en.show()
box.pack_end(en)
if callback:
en.callback_activated_add(callback)
return lb, en
9 years ago
#######################################################################
# Config Files Helper:
def config_file_setup():
global configs
configs = configparser.RawConfigParser()
configs.optionxform = str
try:
9 years ago
fd = open(CONF_FILE, 'r', encoding='utf8')
configs.readfp(fd)
fd.close()
except IOError:
9 years ago
popup_error(
win,
"Cannot read configuration file",
"Econnman cannot read the coniguration file \"" + CONF_FILE +
"\", used by connman to configure your ieee802.1x networks. "
"Make sure the user running connman is able to read/write it."
)
configs = None
raise IOError
9 years ago
def config_del(name):
global configs
secname = 'service_' + name
9 years ago
if configs is None:
try:
config_file_setup()
except IOError:
return
if configs.has_section(secname):
configs.remove_section(secname)
config_write(name)
9 years ago
def config_set(name, key, value):
global configs
secname = 'service_' + name
9 years ago
if configs is None:
try:
config_file_setup()
except IOError:
return
if not configs.has_section(secname):
configs.add_section(secname)
configs.set(secname, 'Type', 'wifi')
configs.set(secname, 'Name', name)
9 years ago
if value is not None:
configs.set(secname, key, value)
elif configs.has_option(secname, key):
configs.remove_option(secname, key)
config_write(name)
9 years ago
def config_get(name):
global configs
9 years ago
if configs is None:
try:
config_file_setup()
except IOError:
return None
for sec in configs.sections():
9 years ago
if configs.has_option(sec, 'Name') and \
configs.get(sec, 'Name') == name:
return sec
else:
return None
9 years ago
def config_option_get(secname, key):
if configs.has_option(secname, key):
return configs.get(secname, key)
return None
9 years ago
def config_exists(name):
if config_get(name):
return True
else:
return False
9 years ago
def config_write(name):
global configs
try:
with open(CONF_FILE, 'w', encoding='utf8') as configfile:
configs.write(configfile)
except IOError:
9 years ago
popup_error(
win,
"Cannot write configuration file",
"Econnman cannot write the coniguration file \"" + CONF_FILE +
"\", used by connman to configure your ieee802.1x networks. "
"Make sure the user running connman is able to read/write it."
)
########################################################################
# Views:
class OfflineModeMonitor(object):
"""Monitors the Manager's OfflineMode property as a Toggle.
The toggle reflects the server state but can be changed by the
user to set the property remotely.
"""
def __init__(self, win):
self.obj = Check(win)
self.obj.style = "toggle"
self.obj.part_text_set("on", "Offline")
self.obj.part_text_set("off", "Online")
self.obj.callback_changed_add(self._on_user_changed)
self.obj.on_del_add(self._deleted)
def on_reply(properties):
for name, value in properties.items():
log.debug("property %s: %s", name, value)
self._property_changed(name, value)
def on_error(exc):
popup_fatal(win, "Failed to get ConnMan Properties", str(exc))
manager.GetProperties(reply_handler=on_reply,
error_handler=on_error)
self.sig_ch = manager.connect_to_signal("PropertyChanged",
self._property_changed)
def _deleted(self, obj):
self.sig_ch.remove()
self.obj = None
self.sig_ch = None
def _property_changed(self, name, value):
log.debug("property %s: %s", name, value)
if name == "OfflineMode":
self.obj.state = bool(value)
def _on_user_changed(self, obj):
state = obj.state
9 years ago
def on_reply():
log.info("Set OfflineMode=%s", state)
def on_error(exc):
log.error("Failed to set OfflineMode=%s: %s", state, exc)
obj.state = not state
popup_error(self.obj, "Failed to Apply Offline Mode",
exc.get_dbus_message())
manager.SetProperty("OfflineMode", dbus.Boolean(state),
reply_handler=on_reply, error_handler=on_error)
class TechList(object):
"""Provides a Genlist with the Technologies supported.
It will call manager's GetTechnologies() and then keep it updated
with TechnologyAdded and TechnologyRemoved signals, as well as the
technologies properties with PropertyChanged.
Selecting an item will call C{on_selected(path, tech_properties)}.
"""
def __init__(self, parent, on_selected=None):
self.techs = {}
self.items = {}
self.obj = Genlist(parent)
self.obj.on_del_add(self._deleted)
self.on_selected = on_selected
self.obj.callback_selected_add(self._tech_selected)
9 years ago
self.sig_added = manager.connect_to_signal(
"TechnologyAdded",
self._tech_added
)
self.sig_removed = manager.connect_to_signal(
"TechnologyRemoved",
self._tech_removed
)
self.sig_propch = bus.add_signal_receiver(
self._tech_changed,
"PropertyChanged",
"net.connman.Technology",
"net.connman",
path_keyword='path'
)
self.itc = GenlistItemClass(
item_style="default",
text_get_func=self._item_text_get,
content_get_func=self._item_content_get
)
manager.GetTechnologies(
reply_handler=self._get_techs_reply,
error_handler=self._get_techs_error
)
def _deleted(self, lst):
self.sig_added.remove()
self.sig_removed.remove()
self.sig_propch.remove()
self.obj = None
self.sig_added = None
self.sig_removed = None
self.sig_propch = None
self.techs.clear()
self.items.clear()
def _get_techs_reply(self, techs):
log.debug("Got technologies: %s", dbus_array_of_dict_to_str(techs))
for path, properties in techs:
self._tech_added(path, properties)
def _get_techs_error(self, exc):
log.error("Failed to GetTechnologies(): %s", exc)
popup_error(self.obj, "Failed to get Technologies",
exc.get_dbus_message())
def _tech_added(self, path, properties):
path = str(path)
log.debug("Added %s: %s", path, dbus_dict_to_str(properties))
self.techs[path] = properties
self.items[path] = self.obj.item_append(self.itc, path)
def _tech_changed(self, name, value, path):
path = str(path)
log.debug("Changed %s: %s=%s", path, name, value)
t = self.techs.get(path)
if not t:
return
t[name] = value
it = self.items.get(path)
if not it:
return
it.update()
def _tech_removed(self, path):
path = str(path)
log.debug("Removed %s", path)
try:
del self.techs[path]
except KeyError:
pass
try:
it = self.items.pop(path)
it.delete()
except KeyError:
pass
def _tech_selected(self, lst, item):
item.selected = False
if not self.on_selected:
return
path = item.data
t = self.techs.get(path)
if t:
self.on_selected(path, t)
def _item_text_get(self, obj, part, item_data):
if part != "elm.text":
return None
t = self.techs.get(item_data)
if not t:
return "Unknown"
return t.get("Name", item_data[len("/net/connman/technology/"):])
def _item_content_get(self, obj, part, item_data):
if part == "elm.swallow.end":
ic = Icon(obj)
ic.standard = "arrow_right"
return ic
if part != "elm.swallow.icon":
return
t = self.techs.get(item_data)
if not t:
return None
ic = Icon(obj)
if t.get("Connected", False):
ic.standard = "connman-tech-connected"
elif t.get("Powered", False):
ic.standard = "connman-tech-powered"
else:
ic.standard = "connman-tech-offline"
return ic
class TechView(ObjectView):
"""Provides a detailed view of the technology given by C{path}.
The C{properties} argument is used to populate the current state,
which will be updated with net.connman.Technology.PropertyChanged
signal that it will listen.
User updates will be automatically applied to the server.
"""
bus_interface = "net.connman.Technology"
def create_view(self, properties):
self.powered = self.add_check(self.box, "Powered",
self._on_user_powered)
self.connected = self.add_check(self.box, "Connected")
self.connected.disabled = True
self.scan = self.add_button(self.box, "Scan", self._scan)
fr, bx = self.add_frame_and_box(self.box, "Tethering")
self.tethering = self.add_check(bx, "Enabled",
self._on_user_tethering)
lb, self.identifier = self.add_label_and_entry(bx, "Identifier:")
lb, self.passphrase = self.add_label_and_entry(bx, "Passphrase:")
self.tethering_apply = self.add_button(bx, "Apply Tethering",
self._tethering_apply)
def _on_user_powered(self, obj):
state = bool(self.powered.state)
9 years ago
def on_reply():
log.info("Set %s Powered=%s", self.path, state)
def on_error(exc):
log.error("Could not set %s Powered=%s: %s", self.path, state, exc)
obj.state = not state
popup_error(self.obj, "Failed to Apply Powered",
exc.get_dbus_message())
9 years ago
self.bus_obj.SetProperty(
"Powered", dbus.Boolean(state),
reply_handler=on_reply, error_handler=on_error
)
def _scan(self, obj):
def on_reply():
log.debug("Scanned %s", self.path)
self.scan.disabled = False
self.scan.text = "Scan"
def on_error(exc):
log.error("Could not scan %s", exc)
self.scan.disabled = False
self.scan.text = "Scan"
self.bus_obj.Scan(reply_handler=on_reply, error_handler=on_error)
self.scan.disabled = True
self.scan.text = "Scanning..."
def _on_user_tethering(self, obj):
state = bool(obj.state)
self.identifier.disabled = not state
self.passphrase.disabled = not state
def _tethering_apply(self, obj):
9 years ago
self.to_apply = [
("TetheringIdentifier", self.identifier.text),
("TetheringPassphrase", self.passphrase.text),
("Tethering", dbus.Boolean(self.tethering.state)),
]
def apply_next():
if not self.to_apply:
return
name, value = self.to_apply.pop(0)
self.bus_obj.SetProperty(name, value,
reply_handler=on_reply,
error_handler=on_error)
def on_reply():
log.debug("Applied tethering %s", self.path)
self.tethering_apply.disabled = False
self.tethering_apply.text = "Apply Tethering"
apply_next()
def on_error(exc):
log.error("Could not apply tethering %s", exc)
self.tethering_apply.disabled = False
self.tethering_apply.text = "Apply Tethering"
popup_error(self.obj, "Failed to Apply Tethering",
exc.get_dbus_message())
apply_next()
self.tethering_apply.disabled = True
self.tethering_apply.text = "Applying Tethering..."
def on_property_changed(self, name, value):
log.debug("Changed %s: %s=%s", self.path, name, value)
if name == "Powered":
self.powered.state = bool(value)
elif name == "Connected":
self.connected.state = bool(value)
elif name == "Tethering":
state = bool(value)
self.tethering.state = state
self.identifier.disabled = not state
self.passphrase.disabled = not state
elif name == "TetheringIdentifier":
self.identifier.text = str(value)
elif name == "TetheringPassphrase":
self.passphrase.text = str(value)
class ServicesList(object):
"""Provides a Genlist with the known Services.
It will call manager's GetServices() and then keep it updated with
ServicesChanged signal.
Selecting an item will call C{on_selected(path, service_properties)}.
"""
def __init__(self, parent, on_selected=None, on_disclosure=None):
self.services = {}
self.items = {}
self.obj = Genlist(parent)
self.on_selected = on_selected
self.on_disclosure = on_disclosure
self.obj.callback_selected_add(self._item_selected)
self.obj.on_del_add(self._deleted)
9 years ago
self.sig_ch = manager.connect_to_signal(
"ServicesChanged",
self._services_changed
)
self.sig_propch = bus.add_signal_receiver(
self._service_prop_changed,
"PropertyChanged",
"net.connman.Service",
"net.connman",
path_keyword='path'
)
manager.GetServices(
reply_handler=self._get_services_reply,
error_handler=self._get_services_error
)
self.itc = GenlistItemClass(
item_style="default",
text_get_func=self._item_text_get,
content_get_func=self._item_content_get
)
def _deleted(self, obj):
self.sig_ch.remove()
self.obj = None
self.sig_ch = None
self.services.clear()
self.items.clear()
def _items_repopulate(self, paths):
for path in paths:
self.items[path] = self.obj.item_append(self.itc, path)
def _get_services_reply(self, services):
log.debug("Got services: %s", dbus_array_of_dict_to_str(services))
for path, properties in services:
self._service_added(path, properties)
self._items_repopulate(str(path) for path, properties in services)
def _get_services_error(self, exc):
log.critical("Failed to GetServices(): %s", exc)
popup_fatal(self.obj, "Failed to get Services",
exc.get_dbus_message())
def _service_added(self, path, properties):
log.debug("Added %s: %s", path, dbus_dict_to_str(properties))
self.services[path] = properties
def _service_prop_changed(self, name, value, path):
path = str(path)
log.debug("Changed %s: %s=%s", path, name, value)
s = self.services.get(path)
if not s:
return
s[name] = value
it = self.items.get(path)
if not it:
return
it.update()
def _service_changed(self, path, properties):
log.debug("Changed %s: %s", path, dbus_dict_to_str(properties))
d = self.services[path]
for k, v in properties.items():
d[k] = v
def _services_changed(self, changed, removed):
log.debug("Changed: %s, Removed: %s",
dbus_array_of_dict_to_str(changed),
removed)
self.items.clear()
self.obj.clear()
for path in removed:
self._service_removed(path)
for path, properties in changed:
path = str(path)
if path in self.services:
self._service_changed(path, properties)
else:
self._service_added(path, properties)
self._items_repopulate(str(path) for path, properties in changed)
def _service_removed(self, path):
path = str(path)
log.debug("Removed %s", path)
try:
del self.services[path]
except KeyError:
pass
def _item_selected(self, lst, item):
item.selected = False
if not self.on_selected:
return
path = item.data
s = self.services.get(path)
if s:
self.on_selected(path, s)
def _item_disclosure(self, bt, path):
if not self.on_disclosure:
return
s = self.services.get(path)
if s:
self.on_disclosure(path, s)
def _item_text_get(self, obj, part, item_data):
if part != "elm.text":
return None
t = self.services.get(item_data)
if not t:
return "Unknown"
return t.get("Name", item_data[len("/net/connman/service/"):])
def _item_content_get(self, obj, part, item_data):
s = self.services.get(item_data)
if not s:
return None
type = s.get("Type")
state = s.get("State")
error = s.get("Error")
security = s.get("Security")
strength = s.get("Strength")
favorite = s.get("Favorite")
roaming = s.get("Roaming")
auto_connect = s.get("AutoConnect")
connected = (str(state) not in ("idle", "failure"))
if security:
security = [str(x) for x in security]
if "none" in security:
security.remove("none")
if part == "elm.swallow.end":
bx = Box(obj)
bx.horizontal = True
bx.homogeneous = True
bx.padding = (2, 0)
bx.align = (1.0, 0.5)
if connected:
ic = Icon(obj)
ic.standard = "connman-connected"
ic.size_hint_min = ic.size_hint_max = (32, 32)
ic.show()
bx.pack_end(ic)
if security and favorite:
ic = Icon(obj)
ic.standard = "connman-security-favorite"
ic.size_hint_min = ic.size_hint_max = (32, 32)
ic.show()
bx.pack_end(ic)
elif security:
ic = Icon(obj)
ic.standard = "connman-security"
ic.size_hint_min = ic.size_hint_max = (32, 32)
ic.show()
bx.pack_end(ic)
ic = Icon(obj)
ic.standard = "arrow_right"
bt = Button(obj)
bt.content = ic
bt.callback_clicked_add(self._item_disclosure, item_data)
bt.propagate_events = False
bt.show()
bt.size_hint_min = bt.size_hint_max = (32, 32)
bx.pack_end(bt)
return bx
if part != "elm.swallow.icon":
return
ly = Layout(obj)
ly.theme_set("icon", type, "default")
ly.size_hint_min_set(32, 32)
def yesno(val):
return ("no", "yes")[bool(val)]
def ornone(val):
return val or "none"
ly.signal_emit("elm,state," + state, "elm")
ly.signal_emit("elm,error," + ornone(error), "elm")
ly.signal_emit("elm,favorite," + yesno(favorite), "elm")
ly.signal_emit("elm,roaming," + yesno(roaming), "elm")
ly.signal_emit("elm,auto_connect," + yesno(auto_connect), "elm")
ly.signal_emit("elm,connected," + yesno(connected), "elm")
for s in security:
ly.signal_emit("elm,security," + s, "elm")
if security:
ly.signal_emit("elm,security,yes", "elm")
else:
ly.signal_emit("elm,security,none", "elm")
if strength:
ly.edje.message_send(1, strength)
return ly
def service_name_get(self, path):
s = self.services.get(path)
if not s:
return None
n = s.get("Name")
if not n:
return None
return str(n)
class ServiceView(ObjectView):
"""Provides a detailed view of the service given by C{path}.
The C{properties} argument is used to populate the current state,
which will be updated with net.connman.Service.PropertyChanged
signal that it will listen.
User updates will be automatically applied to the server.
"""
bus_interface = "net.connman.Service"
eth_fields = (("Method", "eth_method"),
("Interface", "eth_iface"),
("Address", "eth_addr"),
("MTU", "eth_mtu"),
("Speed", "eth_speed"),
("Duplex", "eth_duplex"),
)
vpn_fields = (("Host", "vpn_host"),
("Domain", "vpn_domain"),
("Name", "vpn_name"),