Skip to content
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
3 changes: 3 additions & 0 deletions framework/python/src/common/mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

"""MQTT client"""
import json
import typing as t
import paho.mqtt.client as mqtt_client
from common import logger
Expand Down Expand Up @@ -55,4 +56,6 @@ def send_message(self, topic: str, message: t.Union[str, dict]) -> None:
message (t.Union[str, dict]): message
"""
self._connect()
if isinstance(message, dict):
message = json.dumps(message)
self._client.publish(topic, str(message))
5 changes: 4 additions & 1 deletion framework/python/src/common/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,9 @@ def detect_network_adapters_change(self) -> dict:
if 'items_removed' in diff:
adapters['adapters_removed'] = diff['items_removed']
# Save new network interfaces to session
LOGGER.debug(f'Network adapters changed {adapters}')
LOGGER.debug(f'Network adapters change detected: {adapters}')
self._ifaces = ifaces_new
return adapters

def get_ifaces(self):
return self._ifaces
18 changes: 15 additions & 3 deletions framework/python/src/common/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from contextlib import asynccontextmanager
import datetime

import logging

from apscheduler.schedulers.asyncio import AsyncIOScheduler
from fastapi import FastAPI
Expand All @@ -24,6 +24,7 @@

# Check adapters period seconds
CHECK_NETWORK_ADAPTERS_PERIOD = 5
INTERNET_CONNECTION_TOPIC = 'events/internet'
NETWORK_ADAPTERS_TOPIC = 'events/adapter'

LOGGER = logger.get_logger('tasks')
Expand All @@ -39,6 +40,8 @@ def __init__(
self._mqtt_client = self._testrun.get_mqtt_client()
local_tz = datetime.datetime.now().astimezone().tzinfo
self._scheduler = AsyncIOScheduler(timezone=local_tz)
# Prevent scheduler warnings
self._scheduler._logger.setLevel(logging.ERROR)

@asynccontextmanager
async def start(self, app: FastAPI): # pylint: disable=unused-argument
Expand All @@ -47,15 +50,24 @@ async def start(self, app: FastAPI): # pylint: disable=unused-argument
Args:
app (FastAPI): app instance
"""
# job that checks for changes in network adapters
# Job that checks for changes in network adapters
self._scheduler.add_job(
func=self._testrun.get_net_orc().network_adapters_checker,
kwargs={
'mgtt_client': self._mqtt_client,
'mqtt_client': self._mqtt_client,
'topic': NETWORK_ADAPTERS_TOPIC
},
trigger='interval',
seconds=CHECK_NETWORK_ADAPTERS_PERIOD,
)
self._scheduler.add_job(
func=self._testrun.get_net_orc().internet_conn_checker,
kwargs={
'mqtt_client': self._mqtt_client,
'topic': INTERNET_CONNECTION_TOPIC
},
trigger='interval',
seconds=CHECK_NETWORK_ADAPTERS_PERIOD,
)
self._scheduler.start()
yield
2 changes: 1 addition & 1 deletion framework/python/src/core/testrun.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def __init__(self,
# Start websockets server
self.start_ws()

# MQTT client
# Init MQTT client
self._mqtt_client = mqtt.MQTT()

if self._no_ui:
Expand Down
11 changes: 9 additions & 2 deletions framework/python/src/net_orc/ip_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""IP Control Module"""

import psutil
import typing as t
from common import logger
Expand Down Expand Up @@ -102,7 +101,7 @@ def get_iface_port_stats(self, iface):

def get_namespaces(self):
result = util.run_command('ip netns list')
#Strip ID's from the namespace results
# Strip ID's from the namespace results
namespaces = re.findall(r'(\S+)(?:\s+\(id: \d+\))?', result[0])
return namespaces

Expand Down Expand Up @@ -241,6 +240,14 @@ def configure_container_interface(self,
return False
return True

def ping_via_gateway(self, host):
"""Ping the host trough the gateway container"""
command = f'docker exec tr-ct-gateway ping -W 1 -c 1 {host}'
output = util.run_command(command)
if '0% packet loss' in output[0]:
return True
return False

@staticmethod
def get_sys_interfaces() -> t.Dict[str, t.Dict[str, str]]:
""" Retrieves all Ethernet network interfaces from the host system
Expand Down
39 changes: 32 additions & 7 deletions framework/python/src/net_orc/network_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import time
import traceback
from docker.types import Mount
from common import logger, util
from common import logger, util, mqtt
from net_orc.listener import Listener
from net_orc.network_event import NetworkEvent
from net_orc.network_validator import NetworkValidator
Expand Down Expand Up @@ -550,10 +550,6 @@ def _start_network_service(self, net_module):
cap_add=['NET_ADMIN'],
name=net_module.container_name,
hostname=net_module.container_name,
# Undetermined version of docker seems to have broken
# DNS configuration (/etc/resolv.conf) Re-add when/if
# this network is utilized and DNS issue is resolved
#network=PRIVATE_DOCKER_NET,
network_mode='none',
privileged=True,
detach=True,
Expand Down Expand Up @@ -789,17 +785,46 @@ def restore_net(self):
def get_session(self):
return self._session

def network_adapters_checker(self, mgtt_client, topic):
def network_adapters_checker(self, mqtt_client: mqtt.MQTT, topic: str):
"""Checks for changes in network adapters
and sends a message to the frontend
"""
try:
adapters = self._session.detect_network_adapters_change()
if adapters:
mgtt_client.send_message(topic, adapters)
mqtt_client.send_message(topic, adapters)
except Exception:
LOGGER.error(traceback.format_exc())

def internet_conn_checker(self, mqtt_client: mqtt.MQTT, topic: str):
"""Checks internet connection and sends a status to frontend"""

# Default message
message = {'connection': False}

# Only check if Testrun is running
if self.get_session().get_status() not in [
'Waiting for Device', 'Monitoring', 'In Progress'
]:
message['connection'] = None

# Only run if single intf mode not used
elif 'single_intf' not in self._session.get_runtime_params():
iface = self._session.get_internet_interface()

# Check that an internet intf has been selected
if iface and iface in self._session.get_ifaces():

# Ping google.com from gateway container
internet_connection = self._ip_ctrl.ping_via_gateway(
'google.com')

if internet_connection:
message['connection'] = True

# Broadcast via MQTT client
mqtt_client.send_message(topic, message)


class NetworkModule:
"""Define all the properties of a Network Module"""
Expand Down
8 changes: 8 additions & 0 deletions modules/ui/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ <h1 class="main-heading">Testrun</h1>
">
<mat-icon>tune</mat-icon>
</button>

<span class="separator"></span>
<app-wifi
[on]="vm.hasInternetConnection"
[disable]="
!isTestrunInProgress(vm.systemStatus) ||
vm.hasInternetConnection === null
"></app-wifi>
<app-shutdown-app [disable]="isTestrunInProgress(vm.systemStatus)">
</app-shutdown-app>
</mat-toolbar>
Expand Down
6 changes: 6 additions & 0 deletions modules/ui/src/app/app.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,9 @@ app-version {
display: flex;
justify-content: center;
}

.separator {
width: 1px;
height: 28px;
background-color: $light-grey;
}
14 changes: 13 additions & 1 deletion modules/ui/src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import { LiveAnnouncer } from '@angular/cdk/a11y';
import { HISTORY } from './mocks/reports.mock';
import { TestRunMqttService } from './services/test-run-mqtt.service';
import { MOCK_ADAPTERS } from './mocks/settings.mock';
import { WifiComponent } from './components/wifi/wifi.component';
import { MatTooltipModule } from '@angular/material/tooltip';

const windowMock = {
Expand Down Expand Up @@ -123,7 +124,10 @@ describe('AppComponent', () => {
'focusFirstElementInContainer',
]);
mockLiveAnnouncer = jasmine.createSpyObj('mockLiveAnnouncer', ['announce']);
mockMqttService = jasmine.createSpyObj(['getNetworkAdapters']);
mockMqttService = jasmine.createSpyObj([
'getNetworkAdapters',
'getInternetConnection',
]);

TestBed.configureTestingModule({
imports: [
Expand All @@ -139,6 +143,7 @@ describe('AppComponent', () => {
CalloutComponent,
MatIconTestingModule,
CertificatesComponent,
WifiComponent,
MatTooltipModule,
],
providers: [
Expand Down Expand Up @@ -441,6 +446,13 @@ describe('AppComponent', () => {
expect(version).toBeTruthy();
});

it('should internet icon', () => {
fixture.detectChanges();
const internet = compiled.querySelector('app-wifi');

expect(internet).toBeTruthy();
});

describe('Callout component visibility', () => {
describe('with no connection settings', () => {
beforeEach(() => {
Expand Down
1 change: 1 addition & 0 deletions modules/ui/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export class AppComponent {
this.appStore.getReports();
this.appStore.getTestModules();
this.appStore.getNetworkAdapters();
this.appStore.getInternetConnection();
this.matIconRegistry.addSvgIcon(
'devices',
this.domSanitizer.bypassSecurityTrustResourceUrl(DEVICES_LOGO_URL)
Expand Down
2 changes: 2 additions & 0 deletions modules/ui/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { ShutdownAppComponent } from './components/shutdown-app/shutdown-app.com
import { WindowProvider } from './providers/window.provider';
import { CertificatesComponent } from './pages/certificates/certificates.component';
import { LOADER_TIMEOUT_CONFIG_TOKEN } from './services/loaderConfig';
import { WifiComponent } from './components/wifi/wifi.component';

import { MqttModule, IMqttServiceOptions } from 'ngx-mqtt';

Expand Down Expand Up @@ -87,6 +88,7 @@ export const MQTT_SERVICE_OPTIONS: IMqttServiceOptions = {
ShutdownAppComponent,
CertificatesComponent,
MqttModule.forRoot(MQTT_SERVICE_OPTIONS),
WifiComponent,
],
providers: [
WindowProvider,
Expand Down
20 changes: 19 additions & 1 deletion modules/ui/src/app/app.store.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,10 @@ describe('AppStore', () => {
mockFocusManagerService = jasmine.createSpyObj([
'focusFirstElementInContainer',
]);
mockMqttService = jasmine.createSpyObj(['getNetworkAdapters']);
mockMqttService = jasmine.createSpyObj([
'getNetworkAdapters',
'getInternetConnection',
]);

TestBed.configureTestingModule({
providers: [
Expand Down Expand Up @@ -162,6 +165,7 @@ describe('AppStore', () => {
isMenuOpen: true,
interfaces: {},
settingMissedError: null,
hasInternetConnection: null,
});
done();
});
Expand Down Expand Up @@ -303,5 +307,19 @@ describe('AppStore', () => {
);
});
});

describe('getInternetConnection', () => {
it('should update store', done => {
mockMqttService.getInternetConnection.and.returnValue(
of({ connection: false })
);
appStore.getInternetConnection();

appStore.viewModel$.pipe(take(1)).subscribe(store => {
expect(store.hasInternetConnection).toEqual(false);
done();
});
});
});
});
});
28 changes: 27 additions & 1 deletion modules/ui/src/app/app.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';
import { tap } from 'rxjs/operators';
import { tap, withLatestFrom } from 'rxjs/operators';
import {
selectError,
selectHasConnectionSettings,
Expand Down Expand Up @@ -55,12 +55,16 @@ export const CONSENT_SHOWN_KEY = 'CONSENT_SHOWN';
export interface AppComponentState {
consentShown: boolean;
isStatusLoaded: boolean;
hasInternetConnection: boolean | null;
systemStatus: TestrunStatus | null;
}
@Injectable()
export class AppStore extends ComponentStore<AppComponentState> {
private consentShown$ = this.select(state => state.consentShown);
private isStatusLoaded$ = this.select(state => state.isStatusLoaded);
private hasInternetConnection$ = this.select(
state => state.hasInternetConnection
);
private hasDevices$ = this.store.select(selectHasDevices);
private hasRiskProfiles$ = this.store.select(selectHasRiskProfiles);
private reports$ = this.store.select(selectReports);
Expand All @@ -85,6 +89,7 @@ export class AppStore extends ComponentStore<AppComponentState> {
isMenuOpen: this.isMenuOpen$,
interfaces: this.interfaces$,
settingMissedError: this.settingMissedError$,
hasInternetConnection: this.hasInternetConnection$,
});

updateConsent = this.updater((state, consentShown: boolean) => ({
Expand All @@ -97,6 +102,13 @@ export class AppStore extends ComponentStore<AppComponentState> {
isStatusLoaded,
}));

updateHasInternetConnection = this.updater(
(state, hasInternetConnection: boolean | null) => ({
...state,
hasInternetConnection,
})
);

setContent = this.effect<void>(trigger$ => {
return trigger$.pipe(
tap(() => {
Expand Down Expand Up @@ -158,6 +170,19 @@ export class AppStore extends ComponentStore<AppComponentState> {
);
});

getInternetConnection = this.effect(trigger$ => {
return trigger$.pipe(
exhaustMap(() => {
return this.testRunMqttService.getInternetConnection().pipe(
withLatestFrom(this.hasInternetConnection$),
tap(([{ connection }]) => {
this.updateHasInternetConnection(connection);
})
);
})
);
});

private notifyAboutTheAdapters(adapters: SystemInterfaces) {
this.notificationService.notify(
`New network adapter(s) ${Object.keys(adapters).join(', ')} has been detected. You can switch to using it in the System settings menu`
Expand Down Expand Up @@ -225,6 +250,7 @@ export class AppStore extends ComponentStore<AppComponentState> {
consentShown: sessionStorage.getItem(CONSENT_SHOWN_KEY) !== null,
isStatusLoaded: false,
systemStatus: null,
hasInternetConnection: null,
});
}
}
Loading