from collections import OrderedDict

from django.contrib.auth.models import Permission

from openwisp_controller.migrations import create_default_permissions, get_swapped_model

# Use a pre-defined UUID so the template can be upgraded via migration scripts if needed
TEMPLATE_MONITORING_UUID = '00000000-defa-defa-defa-000000000000'
TEMPLATE_OPENWISP_MONITORING_01 = OrderedDict(
    {
        "path": "/usr/sbin/openwisp-monitoring",
        "mode": "0744",
        "contents": "uuid=$(uci get openwisp.http.uuid)\nkey=$(uci get openwisp.http.key)\nbase_url=$(uci get openwisp.http.url)\nverify_ssl=$(uci get openwisp.http.verify_ssl)\nincluded_interfaces=$(uci get openwisp.monitoring.included_interfaces)\nurl=\"$base_url/api/v1/monitoring/device/$uuid/?key=$key\"\ndata=$(/usr/sbin/netjson-monitoring \"$included_interfaces\")\nif [ \"$verify_ssl\" = 0 ]; then\n    curl_command='curl -k'\nelse\n    curl_command='curl'\nfi\n# send data via POST\n$curl_command -H \"Content-Type: application/json\" \\\n              -X POST \\\n              -d \"$data\" \\\n              -v $url\n",  # noqa
    }
)
TEMPLATE_OPENWISP_MONITORING_02 = OrderedDict(
    {
        "path": "/usr/sbin/legacy-openwisp-monitoring",
        "mode": "0744",
        "contents": "#!/bin/sh\n\n# Exit if openwisp-monitoring package is installed\nopenwisp_monitoring_installed=$(opkg list-installed | grep openwisp-monitoring -c)\nif [ \"$openwisp_monitoring_installed\" == \"1\" ]; then\n    exit 0\nfi\nuuid=$(uci get openwisp.http.uuid)\nkey=$(uci get openwisp.http.key)\nbase_url=$(uci get openwisp.http.url)\nverify_ssl=$(uci get openwisp.http.verify_ssl)\nincluded_interfaces=$(uci get openwisp.monitoring.included_interfaces)\nurl=\"$base_url/api/v1/monitoring/device/$uuid/?key=$key\"\ndata=$(/usr/sbin/legacy-netjson-monitoring \"$included_interfaces\")\nif [ \"$verify_ssl\" = 0 ]; then\n    curl_command='curl -k'\nelse\n    curl_command='curl'\nfi\n# send data via POST\n$curl_command -H \"Content-Type: application/json\" \\\n              -v -d \"$data\" $url\n",  # noqa
    }
)
TEMPLATE_NETJSON_MONITORING_01 = OrderedDict(
    {
        "path": "/usr/sbin/netjson-monitoring",
        "mode": "0744",
        "contents": "#!/usr/bin/env lua\n-- retrieve monitoring information\n-- and return it as NetJSON Output\nio = require('io')\nubus_lib = require('ubus')\ncjson = require('cjson')\nnixio = require('nixio')\nuci = require('uci')\nuci_cursor = uci.cursor()\n\n-- split function\nfunction split(str, pat)\n    local t = {}\n    local fpat = \"(.-)\" .. pat\n    local last_end = 1\n    local s, e, cap = str:find(fpat, 1)\n    while s do\n        if s ~= 1 or cap ~= \"\" then\n            table.insert(t, cap)\n        end\n        last_end = e + 1\n        s, e, cap = str:find(fpat, last_end)\n    end\n    if last_end <= #str then\n        cap = str:sub(last_end)\n        table.insert(t, cap)\n    end\n    return t\nend\n\nlocal function has_value(tab, val)\n    for index, value in ipairs(tab) do\n        if value == val then\n            return true\n        end\n    end\n    return false\nend\n\nlocal function starts_with(str, start)\n    return str:sub(1, #start) == start\nend\n\n-- parse /proc/net/arp\nfunction parse_arp()\n    arp_info = {}\n    for line in io.lines('/proc/net/arp 2> /dev/null') do\n        if line:sub(1, 10) ~= 'IP address' then\n            ip, hw, flags, mac, mask, dev = line:match(\"(%S+)%s+(%S+)%s+(%S+)%s+(%S+)%s+(%S+)%s+(%S+)\")\n            table.insert(arp_info, {\n                ip = ip,\n                mac = mac,\n                interface = dev,\n                state = ''\n            })\n        end\n    end\n    return arp_info\nend\n\nfunction get_ip_neigh_json()\n    arp_info = {}\n    output = io.popen('ip -json neigh 2> /dev/null'):read()\n    if output ~= nil and pcall(function () json_output = cjson.decode(output) end) then\n        json_output = cjson.decode(output)\n        for _, arp_entry in pairs(json_output) do\n            table.insert(arp_info, {\n                ip = arp_entry[\"dst\"],\n                mac = arp_entry[\"lladdr\"],\n                interface = arp_entry[\"dev\"],\n                state = arp_entry[\"state\"][1]\n            })\n        end\n    end\n    return arp_info\nend\n\nfunction get_ip_neigh()\n    arp_info = {}\n    output = io.popen('ip neigh 2> /dev/null')\n    for line in output:lines() do\n        ip, dev, mac, state = line:match(\"(%S+)%s+dev%s+(%S+)%s+lladdr%s+(%S+).*%s(%S+)\")\n        if mac ~= nil then\n            table.insert(arp_info, {\n                ip = ip,\n                mac = mac,\n                interface = dev,\n                state = state\n            })\n        end\n    end\n    return arp_info\nend\n\nfunction get_neighbors()\n    arp_table = get_ip_neigh_json()\n    if next(arp_table) == nil then\n        arp_table = get_ip_neigh()\n    end\n    if next(arp_table) == nil then\n        arp_table = parse_arp()\n    end\n    return arp_table\nend\n\nfunction parse_dhcp_lease_file(path, leases)\n    local f = io.open(path, 'r')\n    if not f then\n        return leases\n    end\n\n    for line in f:lines() do\n        local expiry, mac, ip, name, id = line:match('(%S+)%s+(%S+)%s+(%S+)%s+(%S+)%s+(%S+)')\n        table.insert(leases, {\n            expiry = tonumber(expiry),\n            mac = mac,\n            ip = ip,\n            client_name = name,\n            client_id = id\n        })\n    end\n\n    return leases\nend\n\nfunction get_dhcp_leases()\n    local dhcp_configs = uci_cursor:get_all('dhcp')\n    local leases = {}\n\n    if not dhcp_configs or not next(dhcp_configs) then\n        return nil\n    end\n\n    for name, config in pairs(dhcp_configs) do\n        if config and config['.type'] == 'dnsmasq' and config.leasefile then\n            leases = parse_dhcp_lease_file(config.leasefile, leases)\n        end\n    end\n    return leases\nend\n\nfunction is_table_empty(table_)\n    return not table_ or next(table_) == nil\nend\n\nfunction parse_hostapd_clients(clients)\n  local data = {}\n  for mac, properties in pairs(clients) do\n      properties.mac = mac\n      table.insert(data, properties)\n  end\n  return data\nend\n\nfunction parse_iwinfo_clients(clients)\n  local data = {}\n  for i, p in pairs(clients) do\n      client = {}\n      client.ht = p.rx.ht\n      client.mac = p.mac\n      client.authorized = p.authorized\n      client.vht = p.rx.vht\n      client.wmm = p.wme\n      client.mfp = p.mfp\n      client.auth = p.authenticated\n      client.signal = p.signal\n      client.noise = p.noise\n      table.insert(data, client)\n  end\n  return data\nend\n\n-- takes ubus wireless.status clients output and converts it to NetJSON\nfunction netjson_clients(clients, is_mesh)\n    return (is_mesh and parse_iwinfo_clients(clients) or parse_hostapd_clients(clients))\nend\n\nubus = ubus_lib.connect()\nif not ubus then\n    error('Failed to connect to ubusd')\nend\n\n-- helpers\niwinfo_modes = {\n    ['Master'] = 'access_point',\n    ['Client'] = 'station',\n    ['Mesh Point'] = '802.11s',\n    ['Ad-Hoc'] = 'adhoc'\n}\n\n-- collect system info\nsystem_info = ubus:call('system', 'info', {})\nboard = ubus:call('system', 'board', {})\nloadavg_output = io.popen('cat /proc/loadavg'):read()\nloadavg_output = split(loadavg_output, ' ')\nload_average = {tonumber(loadavg_output[1]), tonumber(loadavg_output[2]), tonumber(loadavg_output[3])}\n\nfunction parse_disk_usage()\n    file = io.popen('df')\n    disk_usage_info = {}\n    for line in file:lines() do\n        if line:sub(1, 10) ~= 'Filesystem' then\n            filesystem, size, used, available, percent, location =\n                line:match('(%S+)%s+(%S+)%s+(%S+)%s+(%S+)%s+(%S+)%s+(%S+)')\n            if filesystem ~= 'tmpfs' and not string.match(filesystem, 'overlayfs') then\n                percent = percent:gsub('%W', '')\n                -- available, size and used are in KiB\n                table.insert(disk_usage_info, {\n                    filesystem = filesystem,\n                    available_bytes = tonumber(available) * 1024,\n                    size_bytes = tonumber(size) * 1024,\n                    used_bytes = tonumber(used) * 1024,\n                    used_percent = tonumber(percent),\n                    mount_point = location\n                })\n            end\n        end\n    end\n    file:close()\n    return disk_usage_info\nend\n\nfunction get_cpus()\n    processors = io.popen('cat /proc/cpuinfo | grep -c processor')\n    cpus = tonumber(processors:read('*a'))\n    processors:close()\n    return cpus\nend\n\nfunction get_vpn_interfaces()\n    -- only openvpn supported for now\n    local items = uci_cursor:get_all('openvpn')\n    local vpn_interfaces = {}\n\n    if is_table_empty(items) then\n        return {}\n    end\n\n    for name, config in pairs(items) do\n        if config and config.dev then\n            vpn_interfaces[config.dev] = true\n        end\n    end\n    return vpn_interfaces\nend\n\n-- init netjson data structure\nnetjson = {\n    type = 'DeviceMonitoring',\n    general = {\n        hostname = board.hostname,\n        local_time = system_info.localtime,\n        uptime = system_info.uptime\n    },\n    resources = {\n        load = load_average,\n        memory = system_info.memory,\n        swap = system_info.swap,\n        cpus = get_cpus(),\n        disk = parse_disk_usage()\n    }\n}\n\ndhcp_leases = get_dhcp_leases()\nif not is_table_empty(dhcp_leases) then\n    netjson.dhcp_leases = dhcp_leases\nend\n\nneighbors = get_neighbors()\nif not is_table_empty(neighbors) then\n    netjson.neighbors = neighbors\nend\n\n-- determine the interfaces to monitor\ntraffic_monitored = arg[1]\ninclude_stats = {}\nif traffic_monitored then\n    traffic_monitored = split(traffic_monitored, ' ')\n    for i, name in pairs(traffic_monitored) do\n        include_stats[name] = true\n    end\nend\n\nfunction is_excluded(name)\n    return name == 'lo'\nend\n\nfunction find_default_gateway(routes)\n    for i = 1, #routes do\n        if routes[i].target == '0.0.0.0' then\n            return routes[i].nexthop\n        end\n    end\n    return nil\nend\n\n-- collect device data\nnetwork_status = ubus:call('network.device', 'status', {})\nwireless_status = ubus:call('network.wireless', 'status', {})\ninterface_data = ubus:call('network.interface', 'dump', {})\nnixio_data = nixio.getifaddrs()\nvpn_interfaces = get_vpn_interfaces()\nwireless_interfaces = {}\ninterfaces = {}\ndns_servers = {}\ndns_search = {}\n\nfunction new_address_array(address, interface, family)\n    proto = interface['proto']\n    if proto == 'dhcpv6' then\n        proto = 'dhcp'\n    end\n    new_address = {\n        address = address['address'],\n        mask = address['mask'],\n        proto = proto,\n        family = family,\n        gateway = find_default_gateway(interface.route)\n    }\n    return new_address\nend\n\nfunction get_interface_info(name, netjson_interface)\n    info = {\n        dns_search = nil,\n        dns_servers = nil\n    }\n    for _, interface in pairs(interface_data['interface']) do\n        if interface['l3_device'] == name then\n            if next(interface['dns-search']) then\n                info.dns_search = interface['dns-search']\n            end\n            if next(interface['dns-server']) then\n                info.dns_servers = interface['dns-server']\n            end\n            if netjson_interface.type == 'bridge' then\n                info.stp = uci_cursor.get('network', interface['interface'], 'stp') == '1'\n            end\n        end\n    end\n    return info\nend\n\nfunction array_concat(source, destination)\n    table.foreach(source, function(key, value)\n        table.insert(destination, value)\n    end)\nend\n\nfunction dict_merge(source, destination)\n    table.foreach(source, function(key, value)\n        destination[key] = value\n    end)\nend\n\n-- collect interface addresses\nfunction get_addresses(name)\n    addresses = {}\n    interface_list = interface_data['interface']\n    addresses_list = {}\n    for _, interface in pairs(interface_list) do\n        if interface['l3_device'] == name then\n            proto = interface['proto']\n            if proto == 'dhcpv6' then\n                proto = 'dhcp'\n            end\n            for _, address in pairs(interface['ipv4-address']) do\n                table.insert(addresses_list, address['address'])\n                new_address = new_address_array(address, interface, 'ipv4')\n                table.insert(addresses, new_address)\n            end\n            for _, address in pairs(interface['ipv6-address']) do\n                table.insert(addresses_list, address['address'])\n                new_address = new_address_array(address, interface, 'ipv6')\n                table.insert(addresses, new_address)\n            end\n        end\n    end\n    for i = 1, #nixio_data do\n        if nixio_data[i].name == name then\n            if not is_excluded(name) then\n                family = nixio_data[i].family\n                addr = nixio_data[i].addr\n                if family == 'inet' then\n                    family = 'ipv4'\n                    -- Since we don't already know this from the dump, we can\n                    -- consider this dynamically assigned, this is the case for\n                    -- example for OpenVPN interfaces, which get their address\n                    -- from the DHCP server embedded in OpenVPN\n                    proto = 'dhcp'\n                elseif family == 'inet6' then\n                    family = 'ipv6'\n                    if starts_with(addr, 'fe80') then\n                        proto = 'static'\n                    else\n                        ula = uci_cursor.get('network', 'globals', 'ula_prefix')\n                        ula_prefix = split(ula, '::')[1]\n                        if starts_with(addr, ula_prefix) then\n                            proto = 'static'\n                        else\n                            proto = 'dhcp'\n                        end\n                    end\n                end\n                if family == 'ipv4' or family == 'ipv6' then\n                    if not has_value(addresses_list, addr) then\n                        table.insert(addresses, {\n                            address = addr,\n                            mask = nixio_data[i].prefix,\n                            proto = proto,\n                            family = family\n                        })\n                    end\n                end\n            end\n        end\n    end\n    return addresses\nend\n\n-- collect relevant wireless interface stats\n-- (traffic and connected clients)\nfor radio_name, radio in pairs(wireless_status) do\n    for i, interface in ipairs(radio.interfaces) do\n        name = interface.ifname\n        local is_mesh = false\n        if name and not is_excluded(name) then\n            iwinfo = ubus:call('iwinfo', 'info', {\n                device = name\n            })\n            netjson_interface = {\n                name = name,\n                type = 'wireless',\n                wireless = {\n                    ssid = iwinfo.ssid,\n                    mode = iwinfo_modes[iwinfo.mode] or iwinfo.mode,\n                    channel = iwinfo.channel,\n                    frequency = iwinfo.frequency,\n                    tx_power = iwinfo.txpower,\n                    signal = iwinfo.signal,\n                    noise = iwinfo.noise,\n                    country = iwinfo.country\n                }\n            }\n            if iwinfo.mode == 'Ad-Hoc' or iwinfo.mode == 'Mesh Point' then\n              clients = ubus:call('iwinfo', 'assoclist', {\n                device = name\n              }).results\n              is_mesh = true\n            else\n              clients = ubus:call('hostapd.' .. name, 'get_clients', {}).clients\n            end\n            if clients and next(clients) ~= nil then\n                netjson_interface.wireless.clients = netjson_clients(clients, is_mesh)\n            end\n            wireless_interfaces[name] = netjson_interface\n        end\n    end\nend\n\nfunction needs_inversion(interface)\n    return interface.type == 'wireless' and interface.wireless.mode == 'access_point'\nend\n\nfunction invert_rx_tx(interface)\n    for k, v in pairs(interface) do\n        if string.sub(k, 0, 3) == \"rx_\" then\n            local tx_key = \"tx_\" .. string.sub(k, 4)\n            local tx_val = interface[tx_key]\n            interface[tx_key] = v\n            interface[k] = tx_val\n        end\n    end\n    return interface\nend\n\n-- collect interface stats\nfor name, interface in pairs(network_status) do\n    -- only collect data from iterfaces which have not been excluded\n    if not is_excluded(name) then\n        netjson_interface = {\n            name = name,\n            type = string.lower(interface.type),\n            up = interface.up,\n            mac = interface.macaddr,\n            txqueuelen = interface.txqueuelen,\n            mtu = interface.mtu,\n            speed = interface.speed,\n            bridge_members = interface['bridge-members'],\n            multicast = interface.multicast\n        }\n        if wireless_interfaces[name] then\n            dict_merge(wireless_interfaces[name], netjson_interface)\n            interface.type = netjson_interface.type\n        end\n        if interface.type == 'Network device' then\n            link_supported = interface['link-supported']\n            if link_supported and next(link_supported) then\n                netjson_interface.type = 'ethernet'\n                netjson_interface.link_supported = link_supported\n            elseif vpn_interfaces[name] then\n                netjson_interface.type = 'virtual'\n            else\n                netjson_interface.type = 'other'\n            end\n        end\n        if include_stats[name] then\n            if needs_inversion(netjson_interface) then\n                --- ensure wifi access point interfaces\n                --- show download and upload values from\n                --- the user's perspective and not from the router perspective\n                interface.statistics = invert_rx_tx(interface.statistics)\n            end\n            netjson_interface.statistics = interface.statistics\n        end\n        addresses = get_addresses(name)\n        if next(addresses) then\n            netjson_interface.addresses = addresses\n        end\n        info = get_interface_info(name, netjson_interface)\n        if info.stp ~= nil then\n            netjson_interface.stp = info.stp\n        end\n        table.insert(interfaces, netjson_interface)\n        -- DNS info is independent from interface\n        if info.dns_servers then\n            array_concat(info.dns_servers, dns_servers)\n        end\n        if info.dns_search then\n            array_concat(info.dns_search, dns_search)\n        end\n    end\nend\n\nif next(interfaces) ~= nil then\n    netjson.interfaces = interfaces\nend\nif next(dns_servers) ~= nil then\n    netjson.dns_servers = dns_servers\nend\nif next(dns_search) ~= nil then\n    netjson.dns_search = dns_search\nend\n\nprint(cjson.encode(netjson))\n",  # noqa
    }
)
TEMPLATE_NETJSON_MONITORING_02 = OrderedDict(
    {
        "path": "/usr/sbin/legacy-netjson-monitoring",
        "mode": "0744",
        "contents": "#!/usr/bin/env lua\n-- retrieve monitoring information\n-- and return it as NetJSON Output\nio = require('io')\nubus_lib = require('ubus')\ncjson = require('cjson')\nnixio = require('nixio')\nuci = require('uci')\nuci_cursor = uci.cursor()\n\n-- split function\nfunction split(str, pat)\n    local t = {}\n    local fpat = \"(.-)\" .. pat\n    local last_end = 1\n    local s, e, cap = str:find(fpat, 1)\n    while s do\n        if s ~= 1 or cap ~= \"\" then\n            table.insert(t, cap)\n        end\n        last_end = e + 1\n        s, e, cap = str:find(fpat, last_end)\n    end\n    if last_end <= #str then\n        cap = str:sub(last_end)\n        table.insert(t, cap)\n    end\n    return t\nend\n\nlocal function has_value(tab, val)\n    for index, value in ipairs(tab) do\n        if value == val then\n            return true\n        end\n    end\n    return false\nend\n\nlocal function starts_with(str, start)\n    return str:sub(1, #start) == start\nend\n\n-- parse /proc/net/arp\nfunction parse_arp()\n    arp_info = {}\n    for line in io.lines('/proc/net/arp 2> /dev/null') do\n        if line:sub(1, 10) ~= 'IP address' then\n            ip, hw, flags, mac, mask, dev = line:match(\"(%S+)%s+(%S+)%s+(%S+)%s+(%S+)%s+(%S+)%s+(%S+)\")\n            table.insert(arp_info, {\n                ip = ip,\n                mac = mac,\n                interface = dev,\n                state = ''\n            })\n        end\n    end\n    return arp_info\nend\n\nfunction get_ip_neigh_json()\n    arp_info = {}\n    output = io.popen('ip -json neigh 2> /dev/null'):read()\n    if output ~= nil and pcall(function () json_output = cjson.decode(output) end) then\n        json_output = cjson.decode(output)\n        for _, arp_entry in pairs(json_output) do\n            table.insert(arp_info, {\n                ip = arp_entry[\"dst\"],\n                mac = arp_entry[\"lladdr\"],\n                interface = arp_entry[\"dev\"],\n                state = arp_entry[\"state\"][1]\n            })\n        end\n    end\n    return arp_info\nend\n\nfunction get_ip_neigh()\n    arp_info = {}\n    output = io.popen('ip neigh 2> /dev/null')\n    for line in output:lines() do\n        ip, dev, mac, state = line:match(\"(%S+)%s+dev%s+(%S+)%s+lladdr%s+(%S+).*%s(%S+)\")\n        if mac ~= nil then\n            table.insert(arp_info, {\n                ip = ip,\n                mac = mac,\n                interface = dev,\n                state = state\n            })\n        end\n    end\n    return arp_info\nend\n\nfunction get_neighbors()\n    arp_table = get_ip_neigh_json()\n    if next(arp_table) == nil then\n        arp_table = get_ip_neigh()\n    end\n    if next(arp_table) == nil then\n        arp_table = parse_arp()\n    end\n    return arp_table\nend\n\nfunction parse_dhcp_lease_file(path, leases)\n    local f = io.open(path, 'r')\n    if not f then\n        return leases\n    end\n\n    for line in f:lines() do\n        local expiry, mac, ip, name, id = line:match('(%S+)%s+(%S+)%s+(%S+)%s+(%S+)%s+(%S+)')\n        table.insert(leases, {\n            expiry = tonumber(expiry),\n            mac = mac,\n            ip = ip,\n            client_name = name,\n            client_id = id\n        })\n    end\n\n    return leases\nend\n\nfunction get_dhcp_leases()\n    local dhcp_configs = uci_cursor:get_all('dhcp')\n    local leases = {}\n\n    if not dhcp_configs or not next(dhcp_configs) then\n        return nil\n    end\n\n    for name, config in pairs(dhcp_configs) do\n        if config and config['.type'] == 'dnsmasq' and config.leasefile then\n            leases = parse_dhcp_lease_file(config.leasefile, leases)\n        end\n    end\n    return leases\nend\n\nfunction is_table_empty(table_)\n    return not table_ or next(table_) == nil\nend\n\nfunction parse_hostapd_clients(clients)\n  local data = {}\n  for mac, properties in pairs(clients) do\n      properties.mac = mac\n      table.insert(data, properties)\n  end\n  return data\nend\n\nfunction parse_iwinfo_clients(clients)\n  local data = {}\n  for i, p in pairs(clients) do\n      client = {}\n      client.ht = p.rx.ht\n      client.mac = p.mac\n      client.authorized = p.authorized\n      client.vht = p.rx.vht\n      client.wmm = p.wme\n      client.mfp = p.mfp\n      client.auth = p.authenticated\n      client.signal = p.signal\n      client.noise = p.noise\n      table.insert(data, client)\n  end\n  return data\nend\n\n-- takes ubus wireless.status clients output and converts it to NetJSON\nfunction netjson_clients(clients, is_mesh)\n    return (is_mesh and parse_iwinfo_clients(clients) or parse_hostapd_clients(clients))\nend\n\nubus = ubus_lib.connect()\nif not ubus then\n    error('Failed to connect to ubusd')\nend\n\n-- helpers\niwinfo_modes = {\n    ['Master'] = 'access_point',\n    ['Client'] = 'station',\n    ['Mesh Point'] = '802.11s',\n    ['Ad-Hoc'] = 'adhoc'\n}\n\n-- collect system info\nsystem_info = ubus:call('system', 'info', {})\nboard = ubus:call('system', 'board', {})\nloadavg_output = io.popen('cat /proc/loadavg'):read()\nloadavg_output = split(loadavg_output, ' ')\nload_average = {tonumber(loadavg_output[1]), tonumber(loadavg_output[2]), tonumber(loadavg_output[3])}\n\nfunction parse_disk_usage()\n    file = io.popen('df')\n    disk_usage_info = {}\n    for line in file:lines() do\n        if line:sub(1, 10) ~= 'Filesystem' then\n            filesystem, size, used, available, percent, location =\n                line:match('(%S+)%s+(%S+)%s+(%S+)%s+(%S+)%s+(%S+)%s+(%S+)')\n            if filesystem ~= 'tmpfs' and not string.match(filesystem, 'overlayfs') then\n                percent = percent:gsub('%W', '')\n                -- available, size and used are in KiB\n                table.insert(disk_usage_info, {\n                    filesystem = filesystem,\n                    available_bytes = tonumber(available) * 1024,\n                    size_bytes = tonumber(size) * 1024,\n                    used_bytes = tonumber(used) * 1024,\n                    used_percent = tonumber(percent),\n                    mount_point = location\n                })\n            end\n        end\n    end\n    file:close()\n    return disk_usage_info\nend\n\nfunction get_cpus()\n    processors = io.popen('cat /proc/cpuinfo | grep -c processor')\n    cpus = tonumber(processors:read('*a'))\n    processors:close()\n    return cpus\nend\n\nfunction get_vpn_interfaces()\n    -- only openvpn supported for now\n    local items = uci_cursor:get_all('openvpn')\n    local vpn_interfaces = {}\n\n    if is_table_empty(items) then\n        return {}\n    end\n\n    for name, config in pairs(items) do\n        if config and config.dev then\n            vpn_interfaces[config.dev] = true\n        end\n    end\n    return vpn_interfaces\nend\n\n-- init netjson data structure\nnetjson = {\n    type = 'DeviceMonitoring',\n    general = {\n        hostname = board.hostname,\n        local_time = system_info.localtime,\n        uptime = system_info.uptime\n    },\n    resources = {\n        load = load_average,\n        memory = system_info.memory,\n        swap = system_info.swap,\n        cpus = get_cpus(),\n        disk = parse_disk_usage()\n    }\n}\n\ndhcp_leases = get_dhcp_leases()\nif not is_table_empty(dhcp_leases) then\n    netjson.dhcp_leases = dhcp_leases\nend\n\nneighbors = get_neighbors()\nif not is_table_empty(neighbors) then\n    netjson.neighbors = neighbors\nend\n\n-- determine the interfaces to monitor\ntraffic_monitored = arg[1]\ninclude_stats = {}\nif traffic_monitored and traffic_monitored ~= '*' then\n    traffic_monitored = split(traffic_monitored, ' ')\n    for i, name in pairs(traffic_monitored) do\n        include_stats[name] = true\n    end\nend\n\nfunction is_excluded(name)\n    return name == 'lo'\nend\n\nfunction find_default_gateway(routes)\n    for i = 1, #routes do\n        if routes[i].target == '0.0.0.0' then\n            return routes[i].nexthop\n        end\n    end\n    return nil\nend\n\n-- collect device data\nnetwork_status = ubus:call('network.device', 'status', {})\nwireless_status = ubus:call('network.wireless', 'status', {})\ninterface_data = ubus:call('network.interface', 'dump', {})\nnixio_data = nixio.getifaddrs()\nvpn_interfaces = get_vpn_interfaces()\nwireless_interfaces = {}\ninterfaces = {}\ndns_servers = {}\ndns_search = {}\n\nfunction new_address_array(address, interface, family)\n    proto = interface['proto']\n    if proto == 'dhcpv6' then\n        proto = 'dhcp'\n    end\n    new_address = {\n        address = address['address'],\n        mask = address['mask'],\n        proto = proto,\n        family = family,\n        gateway = find_default_gateway(interface.route)\n    }\n    return new_address\nend\n\nspecialized_interfaces = {\n    modemmanager = function(name, interface)\n        local modem = uci_cursor.get('network', interface['interface'], 'device')\n        local info = {}\n\n        local general = io.popen('mmcli --output-json -m '..modem):read(\"*a\")\n        if general and pcall(function () general = cjson.decode(general) end) then\n            general = general.modem\n\n            if not is_table_empty(general['3gpp']) then\n                info.imei = general['3gpp'].imei\n                info.operator_name = general['3gpp']['operator-name']\n                info.operator_code = general['3gpp']['operator-code']\n            end\n\n            if not is_table_empty(general.generic) then\n                info.manufacturer = general.generic.manufacturer\n                info.model = general.generic.model\n                info.connection_status = general.generic.state\n                info.power_status = general.generic['power-state']\n            end\n        end\n\n        local signal = io.popen('mmcli --output-json -m '..modem..' --signal-get'):read()\n        if signal and pcall(function () signal = cjson.decode(signal) end) then\n            -- only send data if not empty to avoid generating too much traffic\n            if not is_table_empty(signal.modem) and not is_table_empty(signal.modem.signal) then\n                -- omit refresh rate\n                signal.modem.signal.refresh = nil\n                info.signal = {}\n                -- collect section and values only if not empty\n                for section_key, section_values in pairs(signal.modem.signal) do\n                    for key, value in pairs(section_values) do\n                        if value ~= '--' then\n                            if is_table_empty(info.signal[section_key]) then\n                                info.signal[section_key] = {}\n                            end\n                            info.signal[section_key][key] = tonumber(value)\n                        end\n                    end\n                end\n            end\n        end\n\n        return {type='modem-manager', mobile=info}\n    end\n}\n\nfunction get_interface_info(name, netjson_interface)\n    info = {\n        dns_search = nil,\n        dns_servers = nil\n    }\n    for _, interface in pairs(interface_data['interface']) do\n        if interface['l3_device'] == name then\n            if next(interface['dns-search']) then\n                info.dns_search = interface['dns-search']\n            end\n            if next(interface['dns-server']) then\n                info.dns_servers = interface['dns-server']\n            end\n            if netjson_interface.type == 'bridge' then\n                info.stp = uci_cursor.get('network', interface['interface'], 'stp') == '1'\n            end\n            -- collect specialized info if available\n            local specialized_info = specialized_interfaces[interface.proto]\n            if specialized_info then\n                info.specialized = specialized_info(name, interface)\n            end\n        end\n    end\n    return info\nend\n\nfunction array_concat(source, destination)\n    table.foreach(source, function(key, value)\n        table.insert(destination, value)\n    end)\nend\n\nfunction dict_merge(source, destination)\n    table.foreach(source, function(key, value)\n        destination[key] = value\n    end)\nend\n\n-- collect interface addresses\nfunction get_addresses(name)\n    addresses = {}\n    interface_list = interface_data['interface']\n    addresses_list = {}\n    for _, interface in pairs(interface_list) do\n        if interface['l3_device'] == name then\n            proto = interface['proto']\n            if proto == 'dhcpv6' then\n                proto = 'dhcp'\n            end\n            for _, address in pairs(interface['ipv4-address']) do\n                table.insert(addresses_list, address['address'])\n                new_address = new_address_array(address, interface, 'ipv4')\n                table.insert(addresses, new_address)\n            end\n            for _, address in pairs(interface['ipv6-address']) do\n                table.insert(addresses_list, address['address'])\n                new_address = new_address_array(address, interface, 'ipv6')\n                table.insert(addresses, new_address)\n            end\n        end\n    end\n    for i = 1, #nixio_data do\n        if nixio_data[i].name == name then\n            if not is_excluded(name) then\n                family = nixio_data[i].family\n                addr = nixio_data[i].addr\n                if family == 'inet' then\n                    family = 'ipv4'\n                    -- Since we don't already know this from the dump, we can\n                    -- consider this dynamically assigned, this is the case for\n                    -- example for OpenVPN interfaces, which get their address\n                    -- from the DHCP server embedded in OpenVPN\n                    proto = 'dhcp'\n                elseif family == 'inet6' then\n                    family = 'ipv6'\n                    if starts_with(addr, 'fe80') then\n                        proto = 'static'\n                    else\n                        ula = uci_cursor.get('network', 'globals', 'ula_prefix')\n                        ula_prefix = split(ula, '::')[1]\n                        if starts_with(addr, ula_prefix) then\n                            proto = 'static'\n                        else\n                            proto = 'dhcp'\n                        end\n                    end\n                end\n                if family == 'ipv4' or family == 'ipv6' then\n                    if not has_value(addresses_list, addr) then\n                        table.insert(addresses, {\n                            address = addr,\n                            mask = nixio_data[i].prefix,\n                            proto = proto,\n                            family = family\n                        })\n                    end\n                end\n            end\n        end\n    end\n    return addresses\nend\n\n-- collect relevant wireless interface stats\n-- (traffic and connected clients)\nfor radio_name, radio in pairs(wireless_status) do\n    for i, interface in ipairs(radio.interfaces) do\n        name = interface.ifname\n        local is_mesh = false\n        if name and not is_excluded(name) then\n            iwinfo = ubus:call('iwinfo', 'info', {\n                device = name\n            })\n            netjson_interface = {\n                name = name,\n                type = 'wireless',\n                wireless = {\n                    ssid = iwinfo.ssid,\n                    mode = iwinfo_modes[iwinfo.mode] or iwinfo.mode,\n                    channel = iwinfo.channel,\n                    frequency = iwinfo.frequency,\n                    tx_power = iwinfo.txpower,\n                    signal = iwinfo.signal,\n                    noise = iwinfo.noise,\n                    country = iwinfo.country\n                }\n            }\n            if iwinfo.mode == 'Ad-Hoc' or iwinfo.mode == 'Mesh Point' then\n                clients = ubus:call('iwinfo', 'assoclist', {\n                    device = name\n                }).results\n                is_mesh = true\n            else\n              hostapd_output = ubus:call('hostapd.' .. name, 'get_clients', {})\n              if hostapd_output then\n                  clients = hostapd_output.clients\n              end\n            end\n            if clients and next(clients) ~= nil then\n                netjson_interface.wireless.clients = netjson_clients(clients, is_mesh)\n            end\n            wireless_interfaces[name] = netjson_interface\n        end\n    end\nend\n\nfunction needs_inversion(interface)\n    return interface.type == 'wireless' and interface.wireless.mode == 'access_point'\nend\n\nfunction invert_rx_tx(interface)\n    for k, v in pairs(interface) do\n        if string.sub(k, 0, 3) == \"rx_\" then\n            local tx_key = \"tx_\" .. string.sub(k, 4)\n            local tx_val = interface[tx_key]\n            interface[tx_key] = v\n            interface[k] = tx_val\n        end\n    end\n    return interface\nend\n\n-- collect interface stats\nfor name, interface in pairs(network_status) do\n    -- only collect data from iterfaces which have not been excluded\n    if not is_excluded(name) then\n        netjson_interface = {\n            name = name,\n            type = string.lower(interface.type),\n            up = interface.up,\n            mac = interface.macaddr,\n            txqueuelen = interface.txqueuelen,\n            mtu = interface.mtu,\n            speed = interface.speed,\n            bridge_members = interface['bridge-members'],\n            multicast = interface.multicast\n        }\n        if wireless_interfaces[name] then\n            dict_merge(wireless_interfaces[name], netjson_interface)\n            interface.type = netjson_interface.type\n        end\n        if interface.type == 'Network device' then\n            link_supported = interface['link-supported']\n            if link_supported and next(link_supported) then\n                netjson_interface.type = 'ethernet'\n                netjson_interface.link_supported = link_supported\n            elseif vpn_interfaces[name] then\n                netjson_interface.type = 'virtual'\n            else\n                netjson_interface.type = 'other'\n            end\n        end\n        if include_stats[name] or traffic_monitored == '*' then\n            if needs_inversion(netjson_interface) then\n                --- ensure wifi access point interfaces\n                --- show download and upload values from\n                --- the user's perspective and not from the router perspective\n                interface.statistics = invert_rx_tx(interface.statistics)\n            end\n            netjson_interface.statistics = interface.statistics\n        end\n        addresses = get_addresses(name)\n        if next(addresses) then\n            netjson_interface.addresses = addresses\n        end\n        info = get_interface_info(name, netjson_interface)\n        if info.stp ~= nil then\n            netjson_interface.stp = info.stp\n        end\n        if info.specialized then\n            for key, value in pairs(info.specialized) do\n                netjson_interface[key] = value\n            end\n        end\n        table.insert(interfaces, netjson_interface)\n        -- DNS info is independent from interface\n        if info.dns_servers then\n            array_concat(info.dns_servers, dns_servers)\n        end\n        if info.dns_search then\n            array_concat(info.dns_search, dns_search)\n        end\n    end\nend\n\nif next(interfaces) ~= nil then\n    netjson.interfaces = interfaces\nend\nif next(dns_servers) ~= nil then\n    netjson.dns_servers = dns_servers\nend\nif next(dns_search) ~= nil then\n    netjson.dns_search = dns_search\nend\n\nprint(cjson.encode(netjson))",  # noqa
    }
)
TEMPLATE_CRONTAB_MONITORING_01 = OrderedDict(
    {
        "path": "/etc/crontabs/root",
        "mode": "0644",
        "contents": "*/5 * * * * /usr/sbin/openwisp-monitoring\n",
    }
)
TEMPLATE_CRONTAB_MONITORING_02 = OrderedDict(
    {
        "path": "/etc/crontabs/root",
        "mode": "0644",
        "contents": "*/5 * * * * /usr/sbin/legacy-openwisp-monitoring\n",
    }
)
TEMPLATE_RC_LOCAL_01 = OrderedDict(
    {
        "path": "/etc/rc.local",
        "mode": "0644",
        "contents": "# Put your custom commands here that should be executed once\n# the system init finished. By default this file does nothing.\n\n/usr/sbin/openwisp-monitoring\ntouch /etc/crontabs/root\n/etc/init.d/cron start\n\nexit 0\n",  # noqa
    }
)
TEMPLATE_RC_LOCAL_02 = OrderedDict(
    {
        "path": "/etc/rc.local",
        "mode": "0644",
        "contents": "# Put your custom commands here that should be executed once\n# the system init finished. By default this file does nothing.\n\n/usr/sbin/legacy-openwisp-monitoring\ntouch /etc/crontabs/root\n/etc/init.d/cron start\n\nexit 0\n",  # noqa
    }
)
TEMPLATE_UPDATE_OPENWISP_PACKAGES_01 = OrderedDict(
    {
        "path": "/usr/sbin/update-openwisp-packages",
        "mode": "0744",
        "contents": "#!/bin/sh\nreboot=false\n\n# updates opkg lists only if necessary\nopkg_update(){\n    (\n        test -d /tmp/opkg-lists/ && \\\n        test -f /tmp/opkg-lists/openwrt_base && \\\n        test -f /tmp/opkg-lists/openwrt_packages && \\\n        test -f /tmp/opkg-lists/openwrt_core\n    ) || opkg update;\n}\n\n# installs libubus-lua if necessary\nlibubus_lua_installed=$(opkg list-installed | grep libubus-lua -c)\nif [ \"$libubus_lua_installed\" == \"0\" ]; then\n    opkg_update\n    opkg install libubus-lua\nfi\n\n# installs lua-cjson if necessary\nluacjson_installed=$(opkg list-installed | grep lua-cjson -c)\nif [ \"$luacjson_installed\" == \"0\" ]; then\n    opkg_update\n    opkg install lua-cjson\nfi\n\n# installs rpcd-mod-iwinfo if necessary\nrpcd_mod_iwinfo_installed=$(opkg list-installed | grep rpcd-mod-iwinfo -c)\nif [ \"$rpcd_mod_iwinfo_installed\" == \"0\" ]; then\n    opkg_update\n    opkg install rpcd-mod-iwinfo\n    reboot=true \nfi\n\n# upgrades openwisp-config if necessary\nopenwisp_config_version=$(openwisp_config --version)\nif [ \"$openwisp_config_version\" != \"openwisp-config 0.5.0\" ]; then\n    # backup config just in case...\n    cp /etc/config/openwisp /etc/config/openwisp-backup\n    opkg_update\n    opkg install http://downloads.openwisp.io/openwisp-config/2021-01-07-162007/openwisp-config-mbedtls_0.5.0-1_all.ipk\n    # restore backup\n    mv /etc/config/openwisp-backup /etc/config/openwisp\n    # remove default conf\n    rm /etc/config/openwisp-opkg\n    /etc/init.d/openwisp_config restart\nfi\n\n# reboots if rpcd-mod-iwinfo has been installed\nif [ \"$reboot\" == \"true\" ]; then\n    sleep 5\n    reboot && exit\nfi\n",  # noqa
    }
)
TEMPLATE_UPDATE_OPENWISP_PACKAGES_02 = OrderedDict(
    {
        "path": "/usr/sbin/legacy-update-openwisp-package",
        "mode": "0744",
        "contents": "#!/bin/sh\nreboot=false\n\n# updates opkg lists only if necessary\nopkg_update(){\n    (\n        test -d /tmp/opkg-lists/ && \\\n        test -f /tmp/opkg-lists/openwrt_base && \\\n        test -f /tmp/opkg-lists/openwrt_packages && \\\n        test -f /tmp/opkg-lists/openwrt_core\n    ) || opkg update;\n}\n\n# installs libubus-lua if necessary\nlibubus_lua_installed=$(opkg list-installed | grep libubus-lua -c)\nif [ \"$libubus_lua_installed\" == \"0\" ]; then\n    opkg_update\n    opkg install libubus-lua\nfi\n\n# installs lua-cjson if necessary\nluacjson_installed=$(opkg list-installed | grep lua-cjson -c)\nif [ \"$luacjson_installed\" == \"0\" ]; then\n    opkg_update\n    opkg install lua-cjson\nfi\n\n# installs rpcd-mod-iwinfo if necessary\nrpcd_mod_iwinfo_installed=$(opkg list-installed | grep rpcd-mod-iwinfo -c)\nif [ \"$rpcd_mod_iwinfo_installed\" == \"0\" ]; then\n    opkg_update\n    opkg install rpcd-mod-iwinfo\n    reboot=true \nfi\n\n# reboots if rpcd-mod-iwinfo has been installed\nif [ \"$reboot\" == \"true\" ]; then\n    sleep 5\n    reboot && exit\nfi\n",  # noqa
    }
)
TEMPLATE_POST_RELOAD_HOOK_01 = OrderedDict(
    {
        "path": "/etc/openwisp/post-reload-hook",
        "mode": "0744",
        "contents": "#!/bin/sh\ntouch /etc/crontabs/root\n/etc/init.d/cron start\n/usr/sbin/update-openwisp-packages\n/usr/sbin/openwisp-monitoring\n",  # noqa
    }
)
TEMPLATE_POST_RELOAD_HOOK_02 = OrderedDict(
    {
        "path": "/etc/openwisp/post-reload-hook",
        "mode": "0744",
        "contents": "#!/bin/sh\ntouch /etc/crontabs/root\n/etc/init.d/cron start\n/usr/sbin/legacy-update-openwisp-packages\n/usr/sbin/legacy-openwisp-monitoring\n",  # noqa
    }
)


def assign_permissions_to_groups(apps, schema_editor):
    create_default_permissions(apps, schema_editor)
    operators_read_only_admins_manage = [
        'devicedata',
        'devicemonitoring',
        'wificlient',
        'wifisession',
    ]
    manage_operations = ['add', 'change', 'delete']
    Group = get_swapped_model(apps, 'openwisp_users', 'Group')

    try:
        admin = Group.objects.get(name='Administrator')
        operator = Group.objects.get(name='Operator')
    # consider failures custom cases
    # that do not have to be dealt with
    except Group.DoesNotExist:
        return

    for model_name in operators_read_only_admins_manage:
        try:
            permission = Permission.objects.get(codename='view_{}'.format(model_name))
            operator.permissions.add(permission.pk)
        except Permission.DoesNotExist:
            pass
        for operation in manage_operations:
            admin.permissions.add(
                Permission.objects.get(
                    codename='{}_{}'.format(operation, model_name)
                ).pk
            )
