blob: 1b582dd1b3a415f3d74d6380eb69a7604be11562 [file] [log] [blame]
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpResponse, HttpParams } from '@angular/common/http';
import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { merge } from 'rxjs/index';
import { map, mergeAll, first, catchError, reduce } from 'rxjs/operators';
import { CDN } from '../models/cdn';
import { DataPoint, DataSet, DataSetWithSummary, TPSData } from '../models/data';
import { DeliveryService } from '../models/deliveryservice';
import { InvalidationJob } from '../models/invalidation';
import { Type } from '../models/type';
import { Role, User, Capability } from '../models/user';
function constructDataSetFromResponse (r: any): DataSetWithSummary {
if (!r.series || !r.series.name) {
console.debug(r);
throw new Error('No series data for response!');
}
const data = new Array<DataPoint>();
if (r.series.values !== null && r.series.values !== undefined) {
for (const v of r.series.values) {
if (v[1] === null) {
continue;
}
data.push({t: new Date(v[0]), y: v[1].toFixed(3)} as DataPoint);
}
}
let min: number;
let max: number;
let fifth: number;
let nfifth: number;
let neight: number;
let mean: number;
if (r.summary) {
min = r.summary.min;
max = r.summary.max;
fifth = r.summary.fifthPercentile;
nfifth = r.summary.ninetyFifthPercentile;
neight = r.summary.ninetyEightPercentile;
mean = r.summary.mean;
} else {
min = null;
max = null;
fifth = null;
nfifth = null;
neight = null;
mean = null;
}
return {
dataSet: {label: r.series.name.split('.')[0], data: data} as DataSet,
min: min,
max: max,
fifthPercentile: fifth,
ninetyFifthPercentile: nfifth,
ninetyEighthPercentile: neight,
mean: mean
} as DataSetWithSummary;
}
/**
* The APIService provides access to the Traffic Ops API. Its methods should be kept API-version
* agnostic (from the caller's perspective), and always return `Observable`s.
*/
@Injectable({ providedIn: 'root' })
export class APIService {
/**
* The current API version to use
* @todo Get this from the environment
*/
public API_VERSION = '1.4';
private deliveryServiceTypes: Array<Type>;
// private cookies: string;
constructor (private readonly http: HttpClient) {
}
private delete (path: string, data?: any): Observable<HttpResponse<any>> {
return this.do('delete', path, data);
}
private get (path: string, data?: any): Observable<HttpResponse<any>> {
return this.do('get', path, data);
}
private head (path: string, data?: any): Observable<HttpResponse<any>> {
return this.do('head', path, data);
}
private options (path: string, data?: any): Observable<HttpResponse<any>> {
return this.do('options', path, data);
}
private patch (path: string, data?: any): Observable<HttpResponse<any>> {
return this.do('patch', path, data);
}
private post (path: string, data?: any): Observable<HttpResponse<any>> {
return this.do('post', path, data);
}
private push (path: string, data?: any): Observable<HttpResponse<any>> {
return this.do('push', path, data);
}
private do (method: string, path: string, data?: Object): Observable<HttpResponse<any>> {
/* tslint:disable */
const options = {headers: new HttpHeaders({'Content-Type': 'application/json'}),
observe: 'response' as 'response',
responseType: 'json' as 'json',
body: data};
/* tslint:enable */
return this.http.request(method, path, options).pipe(map((response) => {
// TODO pass alerts to the alert service
// (TODO create the alert service)
return response as HttpResponse<any>;
}));
}
/**
* Performs authentication with the Traffic Ops server.
* @param u The username to be used for authentication
* @param p The password of user `u`
* @returns An observable that will emit the entire HTTP response
*/
public login (u: string, p: string): Observable<HttpResponse<any>> {
const path = '/api/' + this.API_VERSION + '/user/login';
return this.post(path, {u, p});
}
/**
* Fetches the current user from Traffic Ops
* @returns An observable that will emit a `User` object representing the current user.
*/
public getCurrentUser (): Observable<User> {
const path = '/api/' + this.API_VERSION + '/user/current';
return this.get(path).pipe(map(
r => {
return r.body.response as User;
}
));
}
/**
* Gets a list of all visible Delivery Services
* @param A unique identifier for a Delivery Service - either a numeric id or an "xml_id"
* @throws TypeError if ``id`` is not a proper type
* @returns An observable that will emit an array of `DeliveryService` objects.
*/
public getDeliveryServices (id?: string | number): Observable<DeliveryService[] | DeliveryService> {
let path = '/api/' + this.API_VERSION + '/deliveryservices';
if (id) {
if (typeof(id) === 'string') {
path += '?xml_id=' + encodeURIComponent(id);
} else if (typeof(id) === 'number') {
path += '?id=' + String(id);
} else {
throw new TypeError("'id' must be a string or a number! (got: '" + typeof(id) + "')");
}
return this.get(path).pipe(map(
r => {
return r.body.response[0] as DeliveryService;
}
));
}
return this.get(path).pipe(map(
r => {
return r.body.response as DeliveryService[];
}
));
}
/**
* Creates a new Delivery Service
* @param ds The new Delivery Service object
* @returns An Observable that will emit a boolean value indicating the success of the operation
*/
public createDeliveryService (ds: DeliveryService): Observable<boolean> {
const path = '/api/' + this.API_VERSION + '/deliveryservices';
return this.post(path, ds).pipe(map(
r => {
return true;
},
e => {
return false;
}
));
}
/**
* Retrieves capacity statistics for the Delivery Service identified by a given, unique,
* integral value.
* @param d Either a {@link DeliveryService} or an integral, unique identifier of a Delivery Service
* @returrns An Observable that emits an object that hopefully has the right keys to represent capacity.
* @throws If `d` is a {@link DeliveryService} that has no (valid) id
*/
public getDSCapacity (d: number | DeliveryService): Observable<any> {
let id: number;
if (typeof d === 'number'){
id = d;
} else {
d = d as DeliveryService;
if (!d.id || d.id < 0) {
throw new Error("Delivery Service id must be defined!");
}
id = d.id;
}
const path = '/api/' + this.API_VERSION + '/deliveryservices/' + String(id) + '/capacity';
return this.get(path).pipe(map(
r => {
return r.body.response;
}
));
}
/**
* Retrieves the Cache Group health of a Delivery Service identified by a given, unique,
* integral value.
* @param d The integral, unique identifier of a Delivery Service
* @returns An Observable that emits a response from the health endpoint
*/
public getDSHealth (d: number): Observable<any> {
const path = '/api/' + this.API_VERSION + '/deliveryservices/' + String(d) + '/health';
return this.get(path).pipe(map(
r => {
return r.body.response;
}
));
}
/**
* Retrieves Delivery Service throughput statistics for a given time period, averaged over a given
* interval.
* @param d The `xml_id` of a Delivery Service
* @param start A date/time from which to start data collection
* @param end A date/time at which to end data collection
* @param interval A unit-suffixed interval over which data will be "binned"
* @param useMids Collect data regarding Mid-tier cache servers rather than Edge-tier cache servers
* @param dataOnly Only returns the data series, not any supplementing meta info found in the API response
* @returns An Observable that will emit an Array of datapoint Arrays (length 2 containing a date string and data value)
*//* tslint:disable */
public getDSKBPS (d: string,
start: Date,
end: Date,
interval: string,
useMids: boolean,
dataOnly?: boolean): Observable<any | Array<DataPoint>> {
/* tslint:enable */
let path = '/api/' + this.API_VERSION + '/deliveryservice_stats?metricType=kbps';
path += '&interval=' + interval;
path += '&deliveryServiceName=' + d;
path += '&startDate=' + start.toISOString();
path += '&endDate=' + end.toISOString();
path += '&serverType=' + (useMids ? 'mid' : 'edge');
return this.get(path).pipe(map(
r => {
if (r && r.body && r.body.response) {
const resp = r.body.response;
if (dataOnly) {
if (resp.hasOwnProperty('series') && (resp.series.hasOwnProperty('values'))) {
return resp.series.values.filter(d => d[1] !== null).map(
d => ({
t: new Date(d[0]),
y: d[1].toFixed(3)
} as DataPoint)) as Array<DataPoint>;
}
throw new Error("No data series found! Path was '" + path + "'");
}
return r.body.response;
}
return null;
}
));
}
/**
* Gets total TPS data for a Delivery Service. To get TPS data broken down by HTTP status, use {@link getAllDSTPSData}.
* @param d The name (xmlid) of the Delivery Service for which TPS stats will be fetched
* @param start The desired start date/time of the data range (must not have nonzero milliseconds!)
* @param end The desired end date/time of the data range (must not have nonzero milliseconds!)
* @param interval A string that describes the interval across which to 'bucket' data e.g. '60s'
* @param useMids If given (and true) will get stats for the Mid-tier instead of the Edge-tier (which is the default behavior)
*/
public getDSTPS (d: string,
start: Date,
end: Date,
interval: string,
useMids?: boolean): Observable<any> {
let path = '/api/' + this.API_VERSION + '/deliveryservice_stats?metricType=tps_total';
path += '&interval=' + interval;
path += '&deliveryServiceName=' + d;
path += '&startDate=' + start.toISOString();
path += '&endDate=' + end.toISOString();
path += '&serverType=' + (useMids ? 'mid' : 'edge');
return this.get(path).pipe(map(
r => {
if (r && r.body && r.body.response) {
return r.body.response;
}
return null;
}
));
}
/**
* Gets total TPS data for a Delivery Service, as well as TPS data by HTTP response type.
* @param d The name (xmlid) of the Delivery Service for which TPS stats will be fetched
* @param start The desired start date/time of the data range (must not have nonzero milliseconds!)
* @param end The desired end date/time of the data range (must not have nonzero milliseconds!)
* @param interval A string that describes the interval across which to 'bucket' data e.g. '60s'
* @param useMids If given (and true) will get stats for the Mid-tier instead of the Edge-tier (which is the default behavior)
*/
public getAllDSTPSData (d: string,
start: Date,
end: Date,
interval: string,
useMids?: boolean): Observable<TPSData> {
let path = '/api/' + this.API_VERSION + '/deliveryservice_stats?';
path += 'interval=' + interval;
path += '&deliveryServiceName=' + d;
path += '&startDate=' + start.toISOString();
path += '&endDate=' + end.toISOString();
path += '&serverType=' + (useMids ? 'mid' : 'edge');
path += '&metricType=';
const paths = [
path + 'tps_total',
path + 'tps_2xx',
path + 'tps_3xx',
path + 'tps_4xx',
path + 'tps_5xx',
];
const observables = paths.map(x => this.get(x).pipe(map(r => constructDataSetFromResponse(r.body.response))));
const tasks = merge(observables).pipe(mergeAll());
return tasks.pipe(reduce(
(output: TPSData, result: DataSetWithSummary): TPSData => {
switch (result.dataSet.label) {
case 'tps_total':
output.total = result;
break;
case 'tps_1xx':
output.informational = result;
break;
case 'tps_2xx':
output.success = result;
break;
case 'tps_3xx':
output.redirection = result;
break;
case 'tps_4xx':
output.clientError = result;
break;
case 'tps_5xx':
output.serverError = result;
break;
default:
console.debug(result);
throw new Error("Unknown data set type: '" + result.dataSet.label + "'");
}
return output;
},
{
total: null,
informational: null,
success: null,
redirection: null,
clientError: null,
serverError: null
} as TPSData
)) as Observable<TPSData>;
}
/**
* Gets an array of all users in Traffic Ops
* @returns An Observable that will emit an Array of User objects.
*/
public getUsers (nameOrID?: string | number): Observable<Array<User> | User> {
const path = '/api/' + this.API_VERSION + '/users';
if (nameOrID) {
switch (typeof nameOrID) {
case 'string':
return this.get(path + '?username=' + encodeURIComponent(nameOrID)).pipe(map(
r => {
for (const u of (r.body.response as Array<User>)) {
if (u.username === nameOrID) {
return u;
}
}
return null;
}
));
case 'number':
return this.get(path + '?id=' + nameOrID.toString()).pipe(map(
r => {
for (const u of (r.body.response as Array<User>)) {
if (u.id === nameOrID) {
return u;
}
}
return null;
}
));
default:
throw new TypeError("expected a username or ID, got '" + typeof(nameOrID) + "'");
return null;
}
}
return this.get(path).pipe(map(
r => {
return r.body.response as Array<User>;
}
));
}
public getRoles (id: number): Observable<Role>;
public getRoles (name: string): Observable<Role>;
public getRoles (): Observable<Array<Role>>;
/**
* Fetches one or all Roles from Traffic Ops
* @param name Optionally, the name of a single Role which will be fetched
* @param id Optionally, the integral, unique identifier of a single Role which will be fetched
* @throws {TypeError} When called with an improper argument.
* @returns an Observable that will emit either an Array of Roles, or a single Role, depending on whether
* `name`/`id` was passed
* (In the event that `name`/`id` is given but does not match any Role, `null` will be emitted)
*/
public getRoles (nameOrID?: string | number) {
const path = '/api/' + this.API_VERSION + '/roles';
if (nameOrID) {
switch (typeof nameOrID) {
case 'string':
return this.get(path + '?name=' + nameOrID).pipe(map(
r => {
for (const role of (r.body.response as Array<Role>)) {
if (role.name === nameOrID) {
return role;
}
}
return null;
}
));
break;
case 'number':
return this.get(path + '?id=' + nameOrID.toString()).pipe(map(
r => {
for (const role of (r.body.response as Array<Role>)) {
if (role.id === nameOrID) {
return role;
}
}
}
));
break;
default:
throw new TypeError("expected a name or ID, got '" + typeof(nameOrID) + "'");
break;
}
}
return this.get(path).pipe(map(
r => {
return r.body.response as Array<Role>;
}
));
}
public getCapabilities (name: string): Observable<Capability>;
public getCapabilities (): Observable<Array<Capability>>;
/**
* Fetches one or all Capabilities from Traffic Ops
* @param name Optionally, the name of a single Capability which will be fetched
* @throws {TypeError} When called with an improper argument.
* @returns an Observable that will emit either an Array of Capabilities, or a single Capability,
* depending on whether `name`/`id` was passed
* (In the event that `name`/`id` is given but does not match any Capability, `null` will be emitted)
*/
public getCapabilities (name?: string) {
const path = '/api/' + this.API_VERSION + '/capabilities';
if (name) {
return this.get(path + '?name=' + encodeURIComponent(name)).pipe(map(
r => {
for (const cap of (r.body.response as Array<Capability>)) {
if (cap.name === name) {
return cap;
}
}
return null;
}
));
}
return this.get(path).pipe(map(
r => {
return r.body.response as Array<Capability>;
}
));
}
/**
* Gets one or all Types from Traffic Ops
* @param name Optionally, the name of a single Type which will be returned
* @returns An Observable that will emit either a Map of Type names to full Type objects, or a single Type, depending on whether
* `name` was passed
* (In the event that `name` is given but does not match any Type, `null` will be emitted)
*/
public getTypes (name?: string): Observable<Map<string, Type> | Type> {
const path = '/api/' + this.API_VERSION + '/types';
if (name) {
return this.get(path + '?name=' + encodeURIComponent(name)).pipe(map(
r => {
for (const t of (r.body.response as Array<Type>)) {
if (t.name === name) {
return t;
}
}
return null;
}
));
}
return this.get(path).pipe(map(
r => {
const ret = new Map<string, Type>();
for (const t of (r.body.response as Array<Type>)) {
ret.set(t.name, t);
}
return ret;
}
));
}
/**
* This method is handled seperately from :js:method:`APIService.getTypes` because this information
* (should) never change, and therefore can be cached. This method makes an HTTP request iff the values are not already
* cached.
* @returns An Observable that will emit an array of all of the Type objects in Traffic Ops that refer specifically to Delivery Service
* types.
*/
public getDSTypes (): Observable<Array<Type>> {
if (this.deliveryServiceTypes) {
return new Observable(
o => {
o.next(this.deliveryServiceTypes);
o.complete();
return {unsubscribe () {}};
}
);
}
const path = '/api/' + this.API_VERSION + '/types?useInTable=deliveryservice';
return this.get(path).pipe(map(
r => {
this.deliveryServiceTypes = r.body.response as Array<Type>;
return r.body.response as Array<Type>;
}
));
}
/**
* Gets one or all CDNs from Traffic Ops
* @param id The integral, unique identifier of a single CDN to be returned
* @returns An Observable that will emit either a Map of CDN names to full CDN objects, or a single CDN, depending on whether `id` was
* passed.
* (In the event that `id` is passed but does not match any CDN, `null` will be emitted)
*/
public getCDNs (id?: number): Observable<Map<string, CDN> | CDN> {
const path = '/api/' + this.API_VERSION + '/cdns';
if (id) {
return this.get(path + '?id=' + String(id)).pipe(map(
r => {
for (const c of (r.body.response as Array<CDN>)) {
if (c.id === id) {
return c;
}
}
}
));
}
return this.get(path).pipe(map(
r => {
const ret = new Map<string, CDN>();
for (const c of (r.body.response as Array<CDN>)) {
ret.set(c.name, c);
}
return ret;
}
));
}
public getInvalidationJobs (opts?: {id: number} |
{userId: number} |
{user: User} |
{dsId: number} |
{deliveryService: DeliveryService}): Observable<Array<InvalidationJob>> {
let path = '/api/' + this.API_VERSION + '/jobs';
if (opts) {
path += '?';
if (opts.hasOwnProperty('id')) {
path += 'id=' + String((opts as {id: number}).id);
} else if (opts.hasOwnProperty('dsId')) {
path += 'dsId=' + String((opts as {dsId: number}).dsId);
} else if (opts.hasOwnProperty('userId')) {
path += 'userId=' + String((opts as {userId: number}).userId);
} else if (opts.hasOwnProperty('deliveryService')) {
path += 'dsId=' + String((opts as {deliveryService: DeliveryService}).deliveryService.id);
} else {
path += 'userId=' + String((opts as {user: User}).user.id);
}
}
return this.get(path).pipe(map(
r => {
return r.body.response as Array<InvalidationJob>;
}
));
}
public createInvalidationJob (job: InvalidationJob): Observable<boolean> {
const path = '/api/' + this.API_VERSION + '/user/current/jobs';
return this.post(path, job).pipe(map(
r => true,
e => false
));
}
}