Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(monitoring): show ip addresses of wan ifaces #433

Merged
merged 3 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/controller/users/UsersTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { type ControllerAccount } from '@/stores/controller/accounts'
import { useLoginStore } from '@/stores/controller/controllerLogin'
import { ref } from 'vue'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { faCheck, faCircleCheck, faCircleXmark } from '@fortawesome/free-solid-svg-icons'
import { faCircleCheck, faCircleXmark } from '@fortawesome/free-solid-svg-icons'

const props = defineProps<{
users: ControllerAccount[]
Expand Down
128 changes: 105 additions & 23 deletions src/components/standalone/monitoring/ConnectivityMonitor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,16 @@ import InterfaceTrafficCard from './connectivity/InterfaceTrafficCard.vue'
import { isEmpty } from 'lodash-es'
import type { Policy } from '@/composables/useMwan'
import WanConnectionsCard from './connectivity/WanConnectionsCard.vue'
import { useNetworkDevices } from '@/composables/useNetworkDevices'
import { getIpv4Addresses, getIpv6Addresses, getName, isDeviceUp } from '@/lib/standalone/network'
import { useUciNetworkConfig } from '@/composables/useUciNetworkConfig'

export type Wan = {
iface: string
device: string
status?: string
ip4Addresses?: string[]
ip6Addresses?: string[]
}

export type WanEvent = {
Expand All @@ -32,10 +37,20 @@ export type WanEvent = {
}

const { t } = useI18n()
const { allDevices, listDevices, loadingListDevices, errorListDevices, errorListDevicesDetails } =
useNetworkDevices()
const {
networkConfig,
getNetworkConfig,
loadingNetworkConfig,
errorNetworkConfig,
errorNetworkConfigDetails
} = useUciNetworkConfig()

const wans = ref<Wan[]>([])
const mwanEvents = ref<Record<string, any[]>>({})
const mwanPolicies = ref<Policy[]>([])

let loading = ref({
listWans: false,
getMwanReport: false,
Expand All @@ -51,43 +66,75 @@ let error = ref({
getMwanPoliciesDetails: ''
})

const loadingData = computed(() => {
return (
loading.value.listWans ||
loading.value.getMwanReport ||
loading.value.getMwanPolicies ||
loadingNetworkConfig.value ||
loadingListDevices.value
)
})

const wanConnections = computed(() => {
const wanData = []
const wanData: Wan[] = []

for (const wan of wans.value) {
// get wan status from policy data
let statusFound = false

for (const policy of mwanPolicies.value) {
if (statusFound) {
break
}

for (const policyMembers of Object.values(policy.members)) {
if (mwanPolicies.value.length > 0) {
// multiwan configured
for (const policy of mwanPolicies.value) {
if (statusFound) {
break
}

for (const policyMember of policyMembers) {
if (policyMember.interface == wan.iface) {
wanData.push({
...wan,
status: policyMember.status
})
statusFound = true
for (const policyMembers of Object.values(policy.members)) {
if (statusFound) {
break
}

for (const policyMember of policyMembers) {
if (policyMember.interface == wan.iface) {
const devFound = allDevices.value.find((dev) => getName(dev) === wan.device)

wanData.push({
...wan,
status: policyMember.status,
ip4Addresses: devFound ? getIpv4Addresses(devFound, networkConfig.value) : [],
ip6Addresses: devFound ? getIpv6Addresses(devFound, networkConfig.value) : []
})
statusFound = true
break
}
}
}
}
} else {
// multiwan not configured

const devFound = allDevices.value.find((dev) => getName(dev) === wan.device)

if (devFound) {
wanData.push({
...wan,
status: isDeviceUp(devFound, allDevices.value) ? 'online' : 'offline',
ip4Addresses: getIpv4Addresses(devFound, networkConfig.value),
ip6Addresses: getIpv6Addresses(devFound, networkConfig.value)
})
}
}
}
return wanData
return wanData.sort(sortByProperty('iface'))
})

onMounted(() => {
listWans()
getMwanReport()
getMwanPolicies()
listDevices()
getNetworkConfig()
})

async function listWans() {
Expand Down Expand Up @@ -196,25 +243,60 @@ async function getMwanPolicies() {
{{ error.getMwanPoliciesDetails }}
</template>
</NeInlineNotification>
<div class="grid grid-cols-1 gap-x-6 gap-y-6 xl:grid-cols-2">
<!-- listDevices error notification -->
<NeInlineNotification
v-if="errorListDevices"
kind="error"
:title="t('error.cannot_load_network_devices')"
:description="errorListDevices"
:closeAriaLabel="t('common.close')"
class="mb-4"
>
<template v-if="errorListDevicesDetails" #details>
{{ errorListDevicesDetails }}
</template>
</NeInlineNotification>
<!-- uci network config error notification -->
<NeInlineNotification
v-if="errorNetworkConfig"
kind="error"
:title="t('error.cannot_load_network_config')"
:description="errorNetworkConfig"
:closeAriaLabel="t('common.close')"
class="mb-4"
>
<template v-if="errorNetworkConfigDetails" #details>
{{ errorNetworkConfigDetails }}
</template>
</NeInlineNotification>
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-12">
<!-- skeleton -->
<template v-if="loading.listWans || loading.getMwanReport || loading.getMwanPolicies">
<template v-if="loadingData">
<NeCard
loading
:skeletonLines="7"
class="sm:col-span-12 3xl:col-span-8 6xl:col-span-4 7xl:col-span-3"
></NeCard>
<NeCard
v-for="index in 4"
:key="index"
loading
:skeletonLines="5"
class="col-span-1"
:skeletonLines="7"
class="sm:col-span-12 xl:col-span-6 3xl:col-span-4 7xl:col-span-3"
></NeCard>
</template>
<template v-else>
<!-- connections -->
<WanConnectionsCard v-if="wanConnections.length" :wanConnections="wanConnections" />
<WanConnectionsCard
v-if="wanConnections.length"
:wanConnections="wanConnections"
class="sm:col-span-12 3xl:col-span-8 6xl:col-span-4 7xl:col-span-3"
/>
<!-- wan events -->
<NeCard
v-if="isEmpty(mwanEvents)"
:title="t('standalone.real_time_monitor.wan_events')"
class="col-span-1"
class="sm:col-span-12 xl:col-span-6 3xl:col-span-4 7xl:col-span-3"
>
<NeEmptyState
:title="t('standalone.real_time_monitor.no_events_message')"
Expand All @@ -228,15 +310,15 @@ async function getMwanPolicies() {
:key="wanName"
:wan="wanName"
:wanEvents="events"
class="col-span-1"
class="sm:col-span-12 xl:col-span-6 3xl:col-span-4 7xl:col-span-3"
/>
<!-- wans traffic -->
<InterfaceTrafficCard
v-for="wan in wans"
:key="wan.device"
:iface="wan.iface"
:device="wan.device"
class="col-span-1"
class="sm:col-span-12 xl:col-span-6 3xl:col-span-4 7xl:col-span-3"
/>
</template>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const { currentPage, paginatedItems } = useItemPagination(() => props.wanConnect
<NeTableHeadCell>{{ t('standalone.real_time_monitor.interface') }}</NeTableHeadCell>
<NeTableHeadCell>{{ t('standalone.real_time_monitor.device') }}</NeTableHeadCell>
<NeTableHeadCell>{{ t('common.status') }}</NeTableHeadCell>
<NeTableHeadCell>{{ t('common.ip_address') }}</NeTableHeadCell>
</NeTableHead>
<NeTableBody>
<NeTableRow v-for="(item, index) in paginatedItems" :key="index">
Expand Down Expand Up @@ -66,6 +67,12 @@ const { currentPage, paginatedItems } = useItemPagination(() => props.wanConnect
}}
</div>
</NeTableCell>
<NeTableCell :data-label="t('common.ip_address')">
<span v-if="item.ip4Addresses.length || item.ip6Addresses.length">
{{ (item.ip4Addresses || []).concat(item.ip6Addresses || []).join(', ') }}
</span>
<span v-else>-</span>
</NeTableCell>
</NeTableRow>
</NeTableBody>
<template #paginator>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import { useI18n } from 'vue-i18n'
import type { ServerTunnel, ClientTunnel } from './TunnelManager.vue'
import NeMultiTextInput from '../NeMultiTextInput.vue'
import { ubusCall, ValidationError } from '@/lib/standalone/ubus'
import { AxiosError } from 'axios'

type TunnelDefaults = {
secret: string
Expand Down
81 changes: 81 additions & 0 deletions src/composables/useNetworkDevices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (C) 2024 Nethesis S.r.l.
// SPDX-License-Identifier: GPL-3.0-or-later

import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { ubusCall } from '@/lib/standalone/ubus'
import { getAxiosErrorMessage } from '@nethesis/vue-components'
import {
getName,
type DeviceOrIface,
type ZoneWithDeviceNames,
type ZoneWithDevices
} from '@/lib/standalone/network'
import { zonesSorting } from '@/stores/standalone/firewall'

/**
* Composable that handles the network devices retrieved from the ns.devices list-devices API
*/
export function useNetworkDevices() {
const { t } = useI18n()

const allDevices = ref<DeviceOrIface[]>([])
const devicesByZone = ref<ZoneWithDeviceNames[]>([])
const loadingListDevices = ref(false)
const errorListDevices = ref('')
const errorListDevicesDetails = ref('')

const sortedZonesAndDevices = computed(() => {
const zones: ZoneWithDevices[] = []
devicesByZone.value.forEach((z: ZoneWithDeviceNames) => {
const deviceList: DeviceOrIface[] = []
z.devices.forEach((devName: string) => {
const devFound = allDevices.value.find((dev) => getName(dev) === devName)

if (devFound) {
deviceList.push(devFound)
}
})
const zone = { name: z.name, devices: deviceList }
zones.push(zone)
})

return zones.sort(zonesSorting)
})

async function listDevices() {
loadingListDevices.value = true
allDevices.value = []
devicesByZone.value = []
errorListDevices.value = ''
errorListDevicesDetails.value = ''

try {
const res = await ubusCall('ns.devices', 'list-devices')
const bond_devices = res.data.all_devices
.filter((device: DeviceOrIface) => device.name?.startsWith('bond-'))
.map((device: DeviceOrIface) => device.name?.slice(5))

allDevices.value = res.data.all_devices.filter(
(device: DeviceOrIface) => !bond_devices.includes(device.name ?? device['.name'])
)
devicesByZone.value = res.data.devices_by_zone
} catch (err: any) {
console.error(err)
errorListDevices.value = t(getAxiosErrorMessage(err))
errorListDevicesDetails.value = err.toString()
} finally {
loadingListDevices.value = false
}
}

return {
allDevices,
devicesByZone,
sortedZonesAndDevices,
listDevices,
loadingListDevices,
errorListDevices,
errorListDevicesDetails
}
}
Loading