blob: 4f2a146ad95cfc15dc0abb707770ee7da441199a [file] [log] [blame]
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { catchError, tap, map } from 'rxjs/operators';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/do';
import { TreeService } from './tree.service';
import { Device } from '../inventory/device';
import { Session, Node, NodeSchema } from './session';
/**
* Service to control NETCONF sessions.
*
* The class maintain list of sessions by using localStorage.
*/
@Injectable()
export class SessionsService {
/** List of opened NETCONF sessions. */
public sessions: Session[];
/** Identifier of the currently active session. */
public activeSession: string;
/**
* Initiate internal data.
* @param http Handler to communicate with the backend.
* @param treeService Handler to control data tree.
*/
constructor(private http: HttpClient, private treeService: TreeService) {
this.activeSession = localStorage.getItem('activeSession');
if (!this.activeSession) {
this.activeSession = "";
}
this.checkSessions();
}
/**
* Store the sessions list into localStorage for later use / reload. Does
* not store activeSession identifier, which is handled separately because
* of its possible more frequent change.
*/
storeSessions(): void {
localStorage.setItem('sessions', JSON.stringify(this.sessions));
}
/**
* Load the sessions list from localStorage.
*/
loadSessions(): void {
this.sessions = JSON.parse(localStorage.getItem('sessions'));
if (!this.sessions) {
this.sessions = [];
}
for (let session of this.sessions) {
/* fix links in modifications data to link the currently reloaded objects */
for (let mod in session.modifications) {
if ('data' in session.modifications[mod]) {
session.modifications[mod]['data'] = this.treeService.pathNode(session, mod);
}
}
}
}
/**
* Get Session according to the specified key. If no key is specified, the
* current activeSession is returned.
* @param key Identifier of the session.
* @returns The session corresponding to the given key or null in case no
* such session exists.
*/
getSession(key: string = this.activeSession): Session {
if (key) {
for (let i = this.sessions.length; i > 0; i--) {
if (this.sessions[i - 1].key == key) {
return this.sessions[i - 1];
}
}
}
return null;
}
/**
* Change the current activeSession.
* @param key Identifier of the session supposed to became activeSession
* @returns The new activeSession object.
*/
changeActiveSession(key: string): Session {
let result = this.getSession(key);
if (result) {
this.activeSession = key;
localStorage.setItem('activeSession', this.activeSession);
}
return result;
}
/**
* Check with backend that session on the specific index in the sessions
* list is still alive.
* @param i Index of the session to check in the sessions list.
*/
private checkSessionIndex(i: number): void {
this.alive(this.sessions[i].key).subscribe(resp => {
if (!resp['success']) {
if (this.activeSession && this.sessions[i].key == this.activeSession) {
/* active session is not alive - select new active session
* as the one on the left from the current one, if there
* is no one, choose the one on the right */
if (i > 0) {
this.activeSession = this.sessions[i - 1].key;
} else if (i + 1 < this.sessions.length) {
this.activeSession = this.sessions[i + 1].key;
} else {
this.activeSession = "";
}
localStorage.setItem('activeSession', this.activeSession);
}
this.sessions.splice(i, 1);
this.storeSessions();
}
});
}
/**
* Check with backend that the session with the given key is still alive.
* @param key Identifier of the session to check.
*/
private checkSession(key: string): void {
for (let i in this.sessions) {
if (this.sessions[i].key == key) {
this.checkSessionIndex(Number(i));
break;
}
}
}
/**
* Check all the sessions if they are still alive. If not, the session is
* removed from the list and if it was activeSession, the new one is
* selected.
*/
checkSessions(): void {
this.loadSessions();
/* verify that the sessions are still active */
for (let i = this.sessions.length; i > 0; i--) {
this.checkSessionIndex(i - 1);
}
}
/**
* Hide a data subtree from view. Hiding is done via set of filters taken
* into account in tree-node template.
* @param activeSession Session to work with.
* @param node Root of the subtree to hide, this node is the last visible node.
*/
collapse(activeSession: Session, node: Node = null ): void {
if (node) {
for (let i = activeSession.treeFilters.length; i > 0; i--) {
if (activeSession.treeFilters[Number(i) - 1].startsWith(node['path'])) {
activeSession.treeFilters.splice(Number(i) - 1, 1);
}
}
activeSession.treeFilters.push(node['path']);
activeSession.dataPresence = 'mixed';
} else {
activeSession.treeFilters = [];
if (activeSession.data) {
for (let root of activeSession.data['children']) {
if ('subtreeRoot' in root) {
continue;
}
activeSession.treeFilters.push(root['path']);
}
}
activeSession.dataPresence = 'root';
}
this.treeService.updateHiddenFlags( activeSession );
this.storeSessions();
}
/**
* Show currently not visited data subtree.
* There are 2 situations why a subtree is not visible. a) It was previously
* collapsed and now it is filtered out via filters. b) It was not loaded
* yet - at the beginning, only the data roots are loaded from backend and
* it is up to user to select subtrees to work with. At that moment the
* complete subtree is loaded from backend even in case the user expanded
* only one level of children (so in such a case standard collapse filters
* are set).
* @param activeSession Session to work with.
* @param node Node to expand, null in case of root node.
* @param all Flag if all levels of children should be expanded or just one.
*/
expand(activeSession: Session, node: Node = null, all: boolean = true): void {
if (!node) {
/* root */
let backup = activeSession.data;
activeSession.data = null;
delete backup['children'];
activeSession.loading = true;
this.rpcGetSubtree(activeSession.key, true).subscribe((result: object) => {
if (result['success']) {
for (let iter of result['data']) {
this.treeService.setDirty( activeSession, iter );
}
activeSession.data = backup;
activeSession.data['children'] = result['data'];
activeSession.loading = false;
activeSession.dataPresence = 'all';
activeSession.treeFilters = [];
this.storeSessions();
}
});
} else if ('subtreeRoot' in node) {
node['loading'] = true;
this.rpcGetSubtree(activeSession.key, true, node['path']).subscribe(result => {
delete node['loading'];
if (result['success']) {
for (let iter of result['data']['children']){
this.treeService.setDirty(activeSession, iter);
if (!all) {
activeSession.treeFilters.push(iter['path']);
}
}
node['children'] = result['data']['children'];
this.treeService.updateHiddenFlags(activeSession);
delete node['subtreeRoot'];
activeSession.dataPresence = 'all';
//console.log(activeSession);
for (let root of activeSession.data['children']) {
if ('subtreeRoot' in root) {
activeSession.dataPresence = 'mixed';
break;
}
}
this.storeSessions();
}
} );
} else {
let index = activeSession.treeFilters.indexOf(node['path']);
if (index != -1) {
activeSession.treeFilters.splice(index, 1);
} else {
for (let i = activeSession.treeFilters.length; i > 0; i--) {
if (activeSession.treeFilters[Number(i) - 1].startsWith(node['path'])) {
activeSession.treeFilters.splice(Number(i) - 1, 1);
}
}
}
if (!all && ('children' in node)) {
for (let child of node['children']) {
activeSession.treeFilters.push(child['path'])
}
}
this.treeService.updateHiddenFlags(activeSession);
this.storeSessions();
}
}
/**
* Backend request to check validity of the value for the specified node.
*
* Accesses backend REST API GET:/netopeer/session/schema/checkvalue
*
* TODO check keys, uniques
*
* @param sessionKey Session identifier.
* @param path Schema path of the node to check
* @param value Value of the node to be checked
* @returns Observable
*/
checkValue(sessionKey: string, path: string, value: string): Observable<string[]> {
let params = new HttpParams()
.set('key', sessionKey)
.set('path', path)
.set('value', value);
//let options = new RequestOptions();
return this.http.get<string[]>('/netopeer/session/schema/checkvalue', { params: params })
.pipe(
catchError(err => Observable.throw(err))
);
}
/**
* Filter given schemas list by the information about the node's children.
* Only the schemas which can be instantiated as child of node are kept
* in the list.
*
* TODO check max-instances
*
* @param parent Parent node to fit the children schemas list.
* @param schemas Schemas list to be reduced.
*/
private filterSchemas(parent: Node, schemas: NodeSchema[]): void {
for (let index = schemas.length - 1; index >= 0; index--) {
if (schemas[index]['type'] & 0x18) {
/* schema nodes that can be instantiated multiple times
* - lists and leaf-lists */
continue;
}
if (!schemas[index]['config']) {
/* read-only nodes cannot be instantiated */
schemas.splice(index, 1);
continue
}
/* try to find existing instance */
let children;
if (('children' in parent) && 'newChildren' in parent) {
children = parent['children'].concat(parent['newChildren']);
} else if ('children' in parent) {
children = parent['children'];
} else {
children = parent['newChildren'];
}
for (let item of children) {
if (parent['deleted']) {
continue;
}
if (schemas[index]['name'] == item['info']['name'] && schemas[index]['module'] == item['info']['module']) {
/* node is already instantiated */
schemas.splice(index, 1);
break;
}
}
}
}
/**
* Backend request to get list of children schemas for the given node. The
* list is further filtered to remove nodes that cannot be created in the
* given node (e.g. because instance is already present).
*
* Accesses backend REST API GET:/netopeer/session/schema
*
* @param sessionKey Session identifier.
* @param node Node, whose children schema should be obtained.
* @returns Observable
*/
childrenSchemas(sessionKey: string, node: Node): Observable<NodeSchema[]> {
let params = new HttpParams()
.set('key', sessionKey)
.set('path', node['info']['path'])
.set('relative', 'children');
return this.http.get<object>('/netopeer/session/schema', { params: params })
.pipe(
map((resp : NodeSchema[]) => resp['data']),
tap((resp: NodeSchema[]) => this.filterSchemas(node, resp))
);
/*
* map((resp: Response) => {
let result = resp.json();
//console.log(result)
if (result['success']) {
this.filterSchemas(node, result['data']);
return result['data'];
} else {
return [];
}
}).toPromise();
* */
}
/**
* Backend request to get list of values for the specific schema node.
*
* TODO in case of leaf-list check values of siblings
*
* Accesses backend REST API GET:/netopeer/session/schema/values
*
* @param sessionKey Session identifier.
* @param node Node, whose possible values should be obtained.
* @returns Observable
*/
schemaValues(sessionKey: string, node: Node): Observable<object> {
let params = new HttpParams()
.set('key', sessionKey)
.set('path', node['info']['path']);
return this.http.get<object>('/netopeer/session/schema/values', { params: params });
}
/**
* Backend request to check if the session is still alive.
*
* Accesses backend REST API GET:/netopeer/session/alive
*
* @param sessionKey Session identifier.
* @returns Observable<string[]>
*/
alive(sessionKey: string): Observable<string[]> {
let params = new HttpParams()
.set('key', sessionKey);
return this.http.get<string[]>('/netopeer/session/alive', { params: params });
}
/**
* Backend request to get the list of NETCONF capabilities of the session.
*
* Accesses backend REST API GET:/netopeer/session/capabilities
*
* @param sessionKey Session identifier.
* @returns Observable
*/
getCpblts(sessionKey: string): Observable<object> {
let params = new HttpParams()
.set('key', sessionKey);
return this.http.get<object>('/netopeer/session/capabilities', { params: params })
.pipe(
catchError((err: any) => Observable.throw(err))
);
}
/**
* Backend request to get running data.
*
* Accesses backend REST API GET:/netopeer/session/rpcGet
*
* @param sessionKey Session identifier.
* @param all Flag to get whole subtree or only one level of children
* @param path Optional path to get the selected subtree of data.
* @returns Observable
*/
rpcGetSubtree(sessionKey: string, all: boolean, path: string = ""): Observable<object> { // <string[]>
let params = new HttpParams()
.set('key', sessionKey)
.set('recursive', all.toString());
if (path !== "") {
params = params.append('path', path);
}
return this.http.get<object>('/netopeer/session/rpcGet', { params: params })
.pipe(
map( (response: object) => {
if( !response['success'] ) {
this.checkSession( sessionKey );
}
return response;
}),
catchError((err: any) => Observable.throw(err))
);
}
/**
* Backend request to get complete running data. The returned data are
* connected with the provided session.
*
* Accesses backend REST API GET:/netopeer/session/rpcGet via rpcGetSubtree()
*
* @param session Session to work with.
* @param all Flag to get whole subtree or only one level of children
*/
rpcGet(session: Session, all: boolean): void {
session.loading = true;
delete session.data;
this.rpcGetSubtree( session.key, all ).subscribe( result => {
if ( result['success'] ) {
for ( let iter of result['data'] ) {
this.treeService.setDirty( session, iter );
}
session.data = {};
session.data['path'] = '/';
session.data['info'] = {};
session.data['info']['config'] = true;
session.data['info']['path'] = '/';
session.data['children'] = result['data'];
}
session.loading = false;
this.storeSessions();
} );
}
/**
* Backend request to apply configuration changes.
*
* Accesses backend REST API POST:/netopeer/session/commit
*
* @param session Session to work with.
* @returns Backend's response as json in Promise.
*/
commit(session: Session): Observable<any> {
return this.http.post('/netopeer/session/commit', {'key': session.key, 'modifications': session.modifications})
.pipe(
catchError((err: any) => Observable.throw(err))
);
}
/**
* Backend request to close NETCONF session. Internally handles maintenance
* of the sessions list and selects activeSession if necessary.
*
* Accesses backend REST API DELETE:/netopeer/session
*
* @param key Session identifier.
* @returns Backend's response as JSON in Observable.
*/
close(key: string) {
let params = new HttpParams()
.set('key', key);
// We need to use generic request, HttpClient.delete does not support sending params.
return this.http.request('DELETE', '/netopeer/session', { params: params })
.pipe(
tap(resp => {
if (resp['success']) {
let index = this.sessions.findIndex((s: Session) => s.key == key);
this.sessions.splice(index, 1);
if (key == this.activeSession) {
if (index > 0) {
this.activeSession = this.sessions[index - 1].key;
} else if (this.sessions.length) {
this.activeSession = this.sessions[0].key;
} else {
this.activeSession = ""
}
}
}
}),
catchError((err: any) => Observable.throw(err))
)
}
/**
* Backend request to create NETCONF session to the specified device.
* Internally handles maintenance of the sessions list and activeSession value.
*
* Accesses backend REST API POST:/netopeer/session
*
* @param dev NETCONF device to connect to.
*/
connect(dev: Device) {
let body = {};
if (dev.id) {
body = {'id': dev.id};
} else {
body = {'name': dev.name, 'hostname': dev.hostname, 'port': dev.port, 'username': dev.username, 'password': dev.password};
//options = new RequestOptions({body: JSON.stringify({'device': {'name': dev.name, 'hostname': dev.hostname, 'port': dev.port, 'username': dev.username, 'password': dev.password}})});
}
return this.http.post('/netopeer/session', body)
.pipe(
tap(resp => {
if (resp['success']) {
this.sessions.push(new Session(resp['session-key'], dev));
this.activeSession = resp['session-key'];
this.storeSessions();
localStorage.setItem('activeSession', this.activeSession);
}
}),
catchError((err: any) => Observable.throw(err))
);
}
}