diff --git a/plinth/modules/network/templates/connections_type_select.html b/plinth/modules/networks/templates/connections_type_select.html
similarity index 100%
rename from plinth/modules/network/templates/connections_type_select.html
rename to plinth/modules/networks/templates/connections_type_select.html
diff --git a/plinth/modules/network/urls.py b/plinth/modules/networks/urls.py
similarity index 64%
rename from plinth/modules/network/urls.py
rename to plinth/modules/networks/urls.py
index 89d92ef54..410fd8777 100644
--- a/plinth/modules/network/urls.py
+++ b/plinth/modules/networks/urls.py
@@ -23,17 +23,17 @@ from django.conf.urls import patterns, url
urlpatterns = patterns(
- 'plinth.modules.network.network',
- url(r'^sys/network/$', 'index', name='index'),
- url(r'^sys/network/(?P[\w.@+-]+)/edit/$',
+ 'plinth.modules.networks.networks',
+ url(r'^sys/networks/$', 'index', name='index'),
+ url(r'^sys/networks/(?P[\w.@+-]+)/edit/$',
'edit', name='edit'),
- url(r'^sys/network/(?P[\w.@+-]+)/activate/$',
+ url(r'^sys/networks/(?P[\w.@+-]+)/activate/$',
'activate', name='activate'),
- url(r'^sys/network/(?P[\w.@+-]+)/deactivate/$',
+ url(r'^sys/networks/(?P[\w.@+-]+)/deactivate/$',
'deactivate', name='deactivate'),
- url(r'^sys/network/add/$', 'add', name='add'),
- url(r'^sys/network/add/ethernet/$', 'add_ethernet', name='add_ethernet'),
- url(r'^sys/network/add/wifi/$', 'add_wifi', name='add_wifi'),
- url(r'^sys/network/(?P[\w.@+-]+)/delete/$',
+ url(r'^sys/networks/add/$', 'add', name='add'),
+ url(r'^sys/networks/add/ethernet/$', 'add_ethernet', name='add_ethernet'),
+ url(r'^sys/networks/add/wifi/$', 'add_wifi', name='add_wifi'),
+ url(r'^sys/networks/(?P[\w.@+-]+)/delete/$',
'delete', name='delete'),
)
diff --git a/plinth/network.py b/plinth/network.py
new file mode 100644
index 000000000..ba895d247
--- /dev/null
+++ b/plinth/network.py
@@ -0,0 +1,237 @@
+#
+# This file is part of Plinth.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+
+"""
+Helper functions for working with network manager.
+"""
+
+from gettext import gettext as _
+import NetworkManager
+import uuid
+import urllib
+
+
+CONNECTION_TYPE_NAMES = {
+ '802-3-ethernet': 'Ethernet',
+ '802-11-wireless': 'Wi-Fi',
+}
+
+
+class ConnectionNotFound(Exception):
+ def __init__(self, msg):
+ self.msg = msg
+ def __str__(self):
+ return self.msg
+
+
+class DeviceNotFound(Exception):
+ def __init__(self, msg):
+ self.msg = msg
+ def __str__(self):
+ return self.msg
+
+
+def get_connection_list():
+ """Get a list of active and available connections."""
+ connections = []
+ active = []
+
+ for conn in NetworkManager.NetworkManager.ActiveConnections:
+ try:
+ settings = conn.Connection.GetSettings()['connection']
+ except DBusException:
+ # DBusException can be thrown here if the connection list is loaded
+ # quickly after a connection is deactivated.
+ continue
+ active.append(settings['id'])
+
+ for conn in NetworkManager.Settings.ListConnections():
+ settings = conn.GetSettings()['connection']
+ # Display a friendly type name if known.
+ conn_type = CONNECTION_TYPE_NAMES.get(settings['type'],
+ settings['type'])
+ connections.append({
+ 'name': settings['id'],
+ 'id': urllib.parse.quote_plus(settings['id']),
+ 'type': conn_type,
+ 'is_active': settings['id'] in active,
+ })
+ connections.sort(key=lambda x: x['is_active'], reverse=True)
+ return connections
+
+
+def get_connection(name):
+ """Returns connection with id matching name.
+ Returns None if not found.
+ """
+ connections = NetworkManager.Settings.ListConnections()
+ connections = dict([(x.GetSettings()['connection']['id'], x)
+ for x in connections])
+ return connections.get(name)
+
+
+def get_active_connection(name):
+ """Returns active connection with id matching name.
+ Returns None if not found.
+ """
+ connections = NetworkManager.NetworkManager.ActiveConnections
+ connections = dict([(x.Connection.GetSettings()['connection']['id'], x)
+ for x in connections])
+ return connections.get(name)
+
+
+def edit_ethernet_connection(conn, name, ipv4_method, ipv4_address):
+ settings = conn.GetSettings()
+
+ new_settings = {
+ 'connection': {
+ 'id': name,
+ 'type': settings['connection']['type'],
+ 'uuid': settings['connection']['uuid'],
+ },
+ '802-3-ethernet': {},
+ 'ipv4': {'method': ipv4_method},
+ }
+ if ipv4_method == 'manual' and ipv4_address:
+ new_settings['ipv4']['addresses'] = [
+ (ipv4_address,
+ 24, # CIDR prefix length
+ '0.0.0.0')] # gateway
+
+ conn.Update(new_settings)
+
+
+def edit_wifi_connection(conn, name, ssid, ipv4_method, ipv4_address):
+ settings = conn.GetSettings()
+
+ new_settings = {
+ 'connection': {
+ 'id': name,
+ 'type': settings['connection']['type'],
+ 'uuid': settings['connection']['uuid'],
+ },
+ '802-11-wireless': {
+ 'ssid': ssid,
+ },
+ 'ipv4': {'method': ipv4_method},
+ }
+ if ipv4_method == 'manual' and ipv4_address:
+ new_settings['ipv4']['addresses'] = [
+ (ipv4_address,
+ 24, # CIDR prefix length
+ '0.0.0.0')] # gateway
+
+ conn.Update(new_settings)
+
+
+def activate_connection(name):
+ # Find the connection
+ conn = get_connection(name)
+ if not conn:
+ raise ConnectionNotFound(
+ _('Failed to activate connection %s: '
+ 'Connection not found.') % name)
+
+ # Find a suitable device
+ ctype = conn.GetSettings()['connection']['type']
+ if ctype == 'vpn':
+ for dev in NetworkManager.NetworkManager.GetDevices():
+ if (dev.State == NetworkManager.NM_DEVICE_STATE_ACTIVATED
+ and dev.Managed):
+ break
+ else:
+ raise DeviceNotFound(
+ _('Failed to activate connection %s: '
+ 'No suitable device is available.') % name)
+ else:
+ dtype = {
+ '802-11-wireless': NetworkManager.NM_DEVICE_TYPE_WIFI,
+ '802-3-ethernet': NetworkManager.NM_DEVICE_TYPE_ETHERNET,
+ 'gsm': NetworkManager.NM_DEVICE_TYPE_MODEM,
+ }.get(ctype, ctype)
+
+ for dev in NetworkManager.NetworkManager.GetDevices():
+ if (dev.DeviceType == dtype
+ and dev.State == NetworkManager.NM_DEVICE_STATE_DISCONNECTED):
+ break
+ else:
+ raise DeviceNotFound(
+ _('Failed to activate connection %s: '
+ 'No suitable device is available.') % name)
+
+ NetworkManager.NetworkManager.ActivateConnection(conn, dev, "/")
+
+
+def deactivate_connection(name):
+ active = get_active_connection(name)
+ if active:
+ NetworkManager.NetworkManager.DeactivateConnection(active)
+ else:
+ raise ConnectionNotFound(
+ _('Failed to deactivate connection %s: '
+ 'Connection not found.') % name)
+
+
+def add_ethernet_connection(name, ipv4_method, ipv4_address):
+ conn = {
+ 'connection': {
+ 'id': name,
+ 'type': '802-3-ethernet',
+ 'uuid': str(uuid.uuid4()),
+ },
+ '802-3-ethernet': {},
+ 'ipv4': {'method': ipv4_method},
+ }
+
+ if ipv4_method == 'manual' and ipv4_address:
+ conn['ipv4']['addresses'] = [
+ (ipv4_address,
+ 24, # CIDR prefix length
+ '0.0.0.0')] # gateway
+
+ NetworkManager.Settings.AddConnection(conn)
+
+
+def add_wifi_connection(name, ssid, ipv4_method, ipv4_address):
+ conn = {
+ 'connection': {
+ 'id': name,
+ 'type': '802-11-wireless',
+ 'uuid': str(uuid.uuid4()),
+ },
+ '802-11-wireless': {
+ 'ssid': ssid,
+ },
+ 'ipv4': {'method': ipv4_method},
+ }
+
+ if ipv4_method == 'manual' and ipv4_address:
+ conn['ipv4']['addresses'] = [
+ (ipv4_address,
+ 24, # CIDR prefix length
+ '0.0.0.0')] # gateway
+
+ NetworkManager.Settings.AddConnection(conn)
+
+
+def delete_connection(name):
+ conn = get_connection(name)
+ if not conn:
+ raise ConnectionNotFound(
+ _('Failed to delete connection %s: '
+ 'Connection not found.') % name)
+ conn.Delete()
diff --git a/plinth/tests/test_network.py b/plinth/tests/test_network.py
new file mode 100644
index 000000000..401087627
--- /dev/null
+++ b/plinth/tests/test_network.py
@@ -0,0 +1,72 @@
+#!/usr/bin/python3
+#
+# This file is part of Plinth.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+
+import unittest
+
+from plinth import network
+
+
+class TestNetwork(unittest.TestCase):
+ """Verify that the network module performs as expected."""
+
+ @classmethod
+ def setUpClass(cls):
+ network.add_ethernet_connection(
+ 'plinth_test_eth', 'auto', '')
+ network.add_wifi_connection(
+ 'plinth_test_wifi', 'plinthtestwifi', 'auto', '')
+
+ @classmethod
+ def tearDownClass(cls):
+ network.delete_connection('plinth_test_eth')
+ network.delete_connection('plinth_test_wifi')
+
+ def test_get_connection_list(self):
+ """Check that we can get a list of available connections."""
+ connections = network.get_connection_list()
+ self.assertTrue('plinth_test_eth' in [x['name'] for x in connections])
+ self.assertTrue('plinth_test_wifi' in [x['name'] for x in connections])
+
+ def test_get_connection(self):
+ """Check that we can get a connection by name."""
+ conn = network.get_connection('plinth_test_eth')
+ self.assertEqual(
+ conn.GetSettings()['connection']['id'], 'plinth_test_eth')
+
+ conn = network.get_connection('plinth_test_wifi')
+ self.assertEqual(
+ conn.GetSettings()['connection']['id'], 'plinth_test_wifi')
+
+ def test_edit_ethernet_connection(self):
+ """Check that we can update an ethernet connection."""
+ conn = network.get_connection('plinth_test_eth')
+ network.edit_ethernet_connection(
+ conn, 'plinth_test_eth', 'manual', '169.254.0.1')
+ conn = network.get_connection('plinth_test_eth')
+ self.assertEqual(conn.GetSettings()['ipv4']['method'], 'manual')
+
+ def test_edit_wifi_connection(self):
+ """Check that we can update a wifi connection."""
+ conn = network.get_connection('plinth_test_wifi')
+ network.edit_wifi_connection(
+ conn, 'plinth_test_wifi', 'plinthtestwifi2', 'auto', '')
+ conn = network.get_connection('plinth_test_wifi')
+ self.assertEqual(conn.GetSettings()['802-11-wireless']['ssid'], 'plinthtestwifi2')
+
+if __name__ == "__main__":
+ unittest.main()