Added target and terminal support in Dashboard
[src/xds/xds-agent.git] / webapp / src / app / @core-xds / services / target.service.ts
1 /**
2 * @license
3 * Copyright (C) 2018 "IoT.bzh"
4 * Author Sebastien Douheret <sebastien@iot.bzh>
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 *   http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
19 import { Injectable, SecurityContext, isDevMode } from '@angular/core';
20 import { Observable } from 'rxjs/Observable';
21 import { Subject } from 'rxjs/Subject';
22 import { BehaviorSubject } from 'rxjs/BehaviorSubject';
23
24 import { XDSAgentService, IXDSTargetConfig, IXDSTargetTerminal } from '../services/xdsagent.service';
25
26 /* FIXME: syntax only compatible with TS>2.4.0
27 export enum TargetTypeEnum {
28     UNSET = '',
29     STANDARD: 'standard',
30 }
31 */
32 export type TargetTypeEnum = '' | 'standard';
33 export const TargetType = {
34   UNSET: <TargetTypeEnum>'',
35   STANDARD: <TargetTypeEnum>'standard',
36 };
37
38 export const TargetTypes = [
39   { value: TargetType.STANDARD, display: 'Standard' },
40 ];
41
42 export const TargetStatus = {
43   ErrorConfig: 'ErrorConfig',
44   Disable: 'Disable',
45   Enable: 'Enable',
46 };
47
48 export type TerminalTypeEnum = '' | 'ssh';
49 export const TerminalType = {
50   UNSET: <TerminalTypeEnum>'',
51   SSH: <TerminalTypeEnum>'ssh',
52 };
53
54 export interface ITarget extends IXDSTargetConfig {
55   isUsable?: boolean;
56 }
57
58 export interface ITerminal extends IXDSTargetTerminal {
59   targetID?: string;
60 }
61
62 export interface ITerminalOutput {
63   termID: string;
64   timestamp: string;
65   stdout: string;
66   stderr: string;
67 }
68
69 export interface ITerminalExit {
70   termID: string;
71   timestamp: string;
72   code: number;
73   error: string;
74 }
75
76 @Injectable()
77 export class TargetService {
78   public targets$: Observable<ITarget[]>;
79   public curTarget$: Observable<ITarget>;
80   public terminalOutput$ = <Subject<ITerminalOutput>>new Subject();
81   public terminalExit$ = <Subject<ITerminalExit>>new Subject();
82
83   private _tgtsList: ITarget[] = [];
84   private tgtsSubject = <BehaviorSubject<ITarget[]>>new BehaviorSubject(this._tgtsList);
85   private _current: ITarget;
86   private curTgtSubject = <BehaviorSubject<ITarget>>new BehaviorSubject(this._current);
87   private curServerID;
88   private termSocket: SocketIOClient.Socket;
89
90   constructor(private xdsSvr: XDSAgentService) {
91     this._current = null;
92     this.targets$ = this.tgtsSubject.asObservable();
93     this.curTarget$ = this.curTgtSubject.asObservable();
94
95     this.xdsSvr.XdsConfig$.subscribe(cfg => {
96       if (!cfg || cfg.servers.length < 1) {
97         return;
98       }
99
100       // FIXME support multiple server
101       this.curServerID = cfg.servers[0].id;
102
103       // Load initial targets list
104       this.xdsSvr.getTargets(this.curServerID).subscribe((targets) => {
105         this._tgtsList = [];
106         targets.forEach(p => {
107           this._addTarget(p, true);
108         });
109
110         // TODO: get previous val from xds-config service / cookie
111         if (this._tgtsList.length > 0) {
112           this._current = this._tgtsList[0];
113           this.curTgtSubject.next(this._current);
114         }
115
116         this.tgtsSubject.next(this._tgtsList);
117       });
118     });
119
120     // Add listener on targets creation, deletion and change events
121     this.xdsSvr.onTargetAdd().subscribe(tgt => this._addTarget(tgt));
122     this.xdsSvr.onTargetDelete().subscribe(tgt => this._delTarget(tgt));
123     this.xdsSvr.onTargetChange().subscribe(tgt => this._updateTarget(tgt));
124
125     // Register events to forward terminal Output and Exit
126     this.xdsSvr.onSocketConnect().subscribe(socket => {
127       this.termSocket = socket;
128
129       // Handle terminal output
130       socket.on('term:output', data => {
131         const termOut = <ITerminalOutput>{
132           termID: data.termID,
133           timestamp: data.timestamp,
134           stdout: atob(data.stdout),
135           stderr: atob(data.stderr),
136         };
137         this.terminalOutput$.next(termOut);
138       });
139
140       // Handle terminal exit event
141       socket.on('term:exit', data => {
142         this.terminalExit$.next(Object.assign({}, <ITerminalExit>data));
143       });
144
145     });
146   }
147
148   setCurrent(p: ITarget): ITarget | undefined {
149     if (!p) {
150       this._current = null;
151       return undefined;
152     }
153     return this.setCurrentById(p.id);
154   }
155
156   setCurrentById(id: string): ITarget | undefined {
157     const p = this._tgtsList.find(item => item.id === id);
158     if (p) {
159       this._current = p;
160       this.curTgtSubject.next(this._current);
161     }
162     return this._current;
163   }
164
165   getCurrent(): ITarget {
166     return this._current;
167   }
168
169   getTargetById(id: string): ITarget | undefined {
170     const t = this._tgtsList.find(item => item.id === id);
171     return t;
172   }
173
174   add(tgt: ITarget): Observable<ITarget> {
175     return this.xdsSvr.addTarget(this.curServerID, tgt);
176   }
177
178   delete(tgt: ITarget): Observable<ITarget> {
179     const idx = this._getTargetIdx(tgt.id);
180     const delTgt = tgt;
181     if (idx === -1) {
182       throw new Error('Invalid target id (id=' + tgt.id + ')');
183     }
184     return this.xdsSvr.deleteTarget(this.curServerID, tgt.id)
185       .map(res => delTgt);
186   }
187
188   setSettings(tgt: ITarget): Observable<ITarget> {
189     return this.xdsSvr.updateTarget(this.curServerID, tgt);
190   }
191
192   terminalOpen(tgtID: string, termID: string, cfg?: IXDSTargetTerminal): Observable<IXDSTargetTerminal> {
193     if (termID === '' || termID === undefined) {
194       // create a new terminal when no termID is set
195       if (cfg === undefined) {
196         cfg = <IXDSTargetTerminal>{
197           name: 'ssh to ' + this.getTargetById(tgtID).name,
198           type: TerminalType.SSH,
199         };
200       }
201       return this.xdsSvr.createTerminalTarget(this.curServerID, tgtID, cfg)
202         .flatMap(res => {
203           return this.xdsSvr.openTerminalTarget(this.curServerID, tgtID, res.id);
204         });
205     } else {
206       return this.xdsSvr.openTerminalTarget(this.curServerID, tgtID, termID);
207     }
208   }
209
210   terminalClose(tgtID, termID: string): Observable<IXDSTargetTerminal> {
211     return this.xdsSvr.closeTerminalTarget(this.curServerID, tgtID, termID);
212   }
213
214   terminalWrite(data: string) {
215     if (this.termSocket) {
216       this.termSocket.emit('term:input', btoa(data));
217     }
218   }
219
220   terminalResize(tgtID, termID: string, cols, rows: number): Observable<IXDSTargetTerminal> {
221     return this.xdsSvr.resizeTerminalTarget(this.curServerID, tgtID, termID, cols, rows);
222   }
223
224   /***  Private functions  ***/
225
226   private _isUsableTarget(p) {
227     return p && (p.status === TargetStatus.Enable);
228   }
229
230   private _getTargetIdx(id: string): number {
231     return this._tgtsList.findIndex((item) => item.id === id);
232   }
233
234   private _addTarget(tgt: ITarget, noNext?: boolean): ITarget {
235
236     tgt.isUsable = this._isUsableTarget(tgt);
237
238     // add new target
239     this._tgtsList.push(tgt);
240
241     // sort target array
242     this._tgtsList.sort((a, b) => {
243       if (a.name < b.name) {
244         return -1;
245       }
246       if (a.name > b.name) {
247         return 1;
248       }
249       return 0;
250     });
251
252     if (!noNext) {
253       this.tgtsSubject.next(this._tgtsList);
254     }
255
256     return tgt;
257   }
258
259   private _delTarget(tgt: ITarget) {
260     const idx = this._tgtsList.findIndex(item => item.id === tgt.id);
261     if (idx === -1) {
262       if (isDevMode) {
263         /* tslint:disable:no-console */
264         console.log('Warning: Try to delete target unknown id: tgt=', tgt);
265       }
266       return;
267     }
268     const delId = this._tgtsList[idx].id;
269     this._tgtsList.splice(idx, 1);
270     if (delId === this._current.id) {
271       this.setCurrent(this._tgtsList[0]);
272     }
273     this.tgtsSubject.next(this._tgtsList);
274   }
275
276   private _updateTarget(tgt: ITarget) {
277     const i = this._getTargetIdx(tgt.id);
278     if (i >= 0) {
279       this._tgtsList[i].status = tgt.status;
280       this._tgtsList[i].isUsable = this._isUsableTarget(tgt);
281       this.tgtsSubject.next(this._tgtsList);
282     }
283   }
284
285 }