diff --git a/Control/README.md b/Control/README.md index 36e9b4436..b1a5fcc31 100644 --- a/Control/README.md +++ b/Control/README.md @@ -11,6 +11,7 @@ - [O2Control gRPC](#o2control-grpc) - [Apricot gRPC](#apricot-grpc) - [Grafana](#grafana) + - [Bookkeeping](#bookkeeping) - [Consul](#consul) - [Notification service](#notification-service) - [InfoLogger GUI](#infologger-gui) @@ -66,8 +67,13 @@ It communicates with [Control agent](https://github.com/AliceO2Group/Control) ov * `package` - name of the gRPC package ### Grafana -* `hostname` - Grafana instance hostname -* `port` - Grafana instance port +* `url` - built URL which points to grafana instance: `://:` + +### Bookkeeping +* `url` - URL which points to Bookkeeping API: `://:`, `://` +* `token` - token needed for permissions to retrieve data from Bookkeeping + +Bookkeeping is going to be used as the source of latest `CALIBRATION` runs as per the [definition](https://github.com/AliceO2Group/Bookkeeping/blob/main/docs/RUN_DEFINITIONS.md). Detectors may need these run before stable beams, with some needing _none_, some only _one_ run and others _multiple_ ones defined by the `RUN TYPE` attribute. As this can vary depending on the period, the types corresponding to a detector will be defined and retrieved from the KV store of [O2Apricot](https://github.com/AliceO2Group/Control/tree/master/apricot) (key and value TBD). ### Consul Use of a Consul instance is optional diff --git a/Control/config-default.js b/Control/config-default.js index d8dc8ca72..76ca557b2 100644 --- a/Control/config-default.js +++ b/Control/config-default.js @@ -27,6 +27,10 @@ module.exports = { grafana: { url: 'http://localhost:3000' }, + bookkeeping: { + url: 'http://localhost:4000', + token: 'some-token' + }, consul: { ui: 'localhost:8500', hostname: 'localhost', diff --git a/Control/lib/adapters/RunSummaryAdapter.js b/Control/lib/adapters/RunSummaryAdapter.js new file mode 100644 index 000000000..3bb367fce --- /dev/null +++ b/Control/lib/adapters/RunSummaryAdapter.js @@ -0,0 +1,54 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +/** + * RunSummaryAdapter - Given an object with RUN information as per Bookkeeping Database (https://github.com/AliceO2Group/Bookkeeping/blob/main/lib/domain/entities/Run.js), + * return a minified version of it with only the summary + */ +class RunSummaryAdapter { + /** + * RunSummaryAdapter + */ + constructor() {} + + /** + * Converts the given object to an entity object. + * + * @param {Object} run - Run Entity as per Bookkeeping https://github.com/AliceO2Group/Bookkeeping/blob/main/lib/domain/entities/Run.js + * @returns {RunSummary} entity of a task with needed information + */ + static toEntity(run) { + const { + runNumber, + environmentId, + definition, + calibrationStatus, + runType, + startTime, + endTime, + detectors = [], + } = run; + return { + runNumber, + environmentId, + definition, + calibrationStatus, + runType: runType?.name, + startTime, + detectors: detectors.sort(), + endTime, + }; + } +} + +module.exports = RunSummaryAdapter; diff --git a/Control/lib/services/Bookkeeping.service.js b/Control/lib/services/Bookkeeping.service.js new file mode 100644 index 000000000..89aac531c --- /dev/null +++ b/Control/lib/services/Bookkeeping.service.js @@ -0,0 +1,108 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. +*/ + +const {Log} = require('@aliceo2/web-ui'); +const {httpGetJson} = require('./../utils.js'); +const RunSummaryAdapter = require('./../adapters/RunSummaryAdapter.js'); + +/** + * BookkeepingService class to be used to retrieve data from Bookkeeping + */ +class BookkeepingService { + /** + * Constructor for configuring the service to retrieve data via Bookkeeping HTTP API + * @param {Object} config = {url: string, token: string} - configuration for using BKP service + */ + constructor({url = '', token = ''}) { + this._url = url; + const {protocol, hostname, port} = new URL(this._url); + this._hostname = hostname; + this._port = port; + this._protocol = protocol; + + this._token = token; + + this._runTypes = {}; // in-memory object which is filled with runTypes on server start + this._logger = new Log(`${process.env.npm_config_log_label ?? 'cog'}/bkp-service`); + } + + /** + * Method to initialize the run service with static data such as runTypes + * @return {void} + */ + async init() { + this._runTypes = await this._getRunTypes(); + } + + /** + * Given a definition, a type of a run and a detector, fetch from Bookkeeping the last RUN matching the parameters + * @param {String} definition - definition of the run to query + * @param {String} type - type of the run to query + * @param {String} detector - detector which contained the run + * @return {RunSummary|{}} - run object from Bookkeeping + */ + async getRun(definition, type, detector) { + if (this._runTypes[type]) { + let filter = `filter[definitions]=${definition}&filter[runTypes]=${this._runTypes[type]}&page[limit]=1&`; + filter += `filter[detectors][operator]=and&filter[detectors][values]=${detector}` + try { + const {data} = await httpGetJson(this._hostname, this._port, `/api/runs?${filter}&token=${this._token}`, { + protocol: this._protocol, + rejectUnauthorized: false, + }); + if (data?.length > 0) { + return RunSummaryAdapter.toEntity(data[0]); + } + } catch (error) { + this._logger.debug(error); + } + } + return {}; + } + + /** + * Method to fetch run types from Bookkeeping and build a map of types to IDs as needed for filtering in RUNs API + * @returns {Object} - map of runtypes to their ID + */ + async _getRunTypes() { + try { + const runTypesMap = {}; + const {data} = await httpGetJson(this._hostname, this._port, `/api/runTypes?token=${this._token}`, { + protocol: this._protocol, + rejectUnauthorized: false, + }); + for (const type of data) { + runTypesMap[type.name] = type.id; + } + return runTypesMap; + } catch (error) { + this._logger.debug(error); + } + return {}; + } + + /** + * Getters/Setters + */ + + /** + * Return the object storing run types by their name with ID + * @return {Object} + */ + get runTypes() { + return this._runTypes; + } +} + +module.exports = {BookkeepingService}; diff --git a/Control/lib/typedefs/RunInfo.js b/Control/lib/typedefs/RunInfo.js new file mode 100644 index 000000000..a97d55351 --- /dev/null +++ b/Control/lib/typedefs/RunInfo.js @@ -0,0 +1,27 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +/** + * @typedef RunSummary + * + * RunSummary is an object which contains just a summary of an entire run entity: https://github.com/AliceO2Group/Bookkeeping/blob/main/lib/domain/entities/Run.js + * + * @property {Number} runNumber + * @property {String} environmentId + * @property {String} definition + * @property {String} calibrationStatus + * @property {String} runType + * @property {Number} startTime + * @property {Number} endTime + * @property {Array} detectors + */ diff --git a/Control/test/lib/services/mocha-bookkeeping.service.test.js b/Control/test/lib/services/mocha-bookkeeping.service.test.js new file mode 100644 index 000000000..cfc803f55 --- /dev/null +++ b/Control/test/lib/services/mocha-bookkeeping.service.test.js @@ -0,0 +1,155 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. +*/ +/* eslint-disable max-len */ + +const assert = require('assert'); +const nock = require('nock'); + +const {BookkeepingService} = require('../../../lib/services/Bookkeeping.service'); + +describe('BookkeepingService test suite', () => { + const url = 'http://bkp-test.cern.ch:8888'; + let bkp = new BookkeepingService({url, token: ''}); + + describe(`'init' test suite`, async () => { + before(() => { + bkp = new BookkeepingService({url, token: ''}); + nock(url) + .get('/api/runTypes?token=') + .reply(200, { + data: [ + {name: 'NOISE', id: 1}, {name: 'PHYSICS', id: 2}, {name: 'SYNTHETIC', id: 3} + ] + }); + nock(url) + .get('/api/runTypes?token=no-data') + .reply(200, {data: []}); + nock(url) + .get('/api/runTypes?token=error') + .replyWithError('Unable to connect'); + }); + after(() => nock.cleanAll()); + + it('should successfully load runTypes from bookkeeping', async () => { + await bkp.init(); + assert.deepStrictEqual(bkp.runTypes, {NOISE: 1, PHYSICS: 2, SYNTHETIC: 3}); + }); + + it('should successfully load an empty object if no runTypes are provided', async () => { + bkp._token = 'no-data'; + await bkp.init(); + assert.deepStrictEqual(bkp.runTypes, {}); + }); + + it('should successfully load an empty object even if bookkeeping returned an error', async () => { + bkp._token = 'error'; + await bkp.init(); + assert.deepStrictEqual(bkp.runTypes, {}); + }); + }); + + describe(`'_getRunTypes' test suite`, async () => { + before(() => { + bkp = new BookkeepingService({url, token: ''}); + nock(url) + .get('/api/runTypes?token=') + .reply(200, { + data: [ + {name: 'NOISE', id: 1}, {name: 'PHYSICS', id: 2}, {name: 'SYNTHETIC', id: 3} + ] + }); + nock(url) + .get('/api/runTypes?token=no-data') + .reply(200, {data: []}); + nock(url) + .get('/api/runTypes?token=error') + .replyWithError('Unable to connect'); + }); + after(() => nock.cleanAll()); + + it('should successfully return runTypes as object from bookkeeping', async () => { + const runTypes = await bkp._getRunTypes(); + assert.deepStrictEqual(runTypes, {NOISE: 1, PHYSICS: 2, SYNTHETIC: 3}); + }); + + it('should successfully load an empty object if no runTypes are provided', async () => { + bkp._token = 'no-data'; + const runTypes = await bkp._getRunTypes(); + assert.deepStrictEqual(runTypes, {}); + }); + + it('should successfully load an empty object even if bookkeeping returned an error', async () => { + bkp._token = 'error'; + const runTypes = await bkp._getRunTypes(); + assert.deepStrictEqual(runTypes, {}); + }); + }); + + describe(`'getRun' test suite`, async () => { + let runToReturn = { + runNumber: 123, + environmentId: 'abc', + definition: 'CALIBRATION', + calibrationStatus: 'good', + runType: {name: 'NOISE', id: 1}, + detectors: ['TPC'], + startTime: Date.now() - 100, + endTime: Date.now(), + extraField: '', + someOther: 1234 + }; + before(() => { + bkp = new BookkeepingService({url, token: ''}); + bkp._runTypes = {NOISE: 1, PHYSICS: 2, SYNTHETIC: 3}; + nock(url) + .get('/api/runs?filter[definitions]=CALIBRATION&filter[runTypes]=1&page[limit]=1&' + + 'filter[detectors][operator]=and&filter[detectors][values]=TPC&token=') + .reply(200, { + data: [runToReturn] + }); + + nock(url) + .get('/api/runs?filter[definitions]=CALIBRATION&filter[runTypes]=2&page[limit]=1&' + + 'filter[detectors][operator]=and&filter[detectors][values]=TPC&token=') + .reply(200, { + data: [] + }); + + nock(url) + .get('/api/runs?filter[definitions]=CALIBRATION&filter[runTypes]=3&page[limit]=1&' + + 'filter[detectors][operator]=and&filter[detectors][values]=TPC&token=') + .replyWithError('Unable'); + }); + after(() => nock.cleanAll()); + + it('should successfully return a run based on existing runType and provided def, type and detector', async () => { + const run = await bkp.getRun('CALIBRATION', 'NOISE', 'TPC'); + const runInfo = JSON.parse(JSON.stringify(runToReturn)); + runInfo.runType = runInfo.runType.name; + delete runInfo.extraField; + delete runInfo.someOther; + assert.deepStrictEqual(run, runInfo); + }); + + it('should successfully return an empty run if none was found', async () => { + const run = await bkp.getRun('CALIBRATION', 'PHYSICS', 'TPC'); + assert.deepStrictEqual(run, {}); + }); + + it('should successfully return an empty run even if bkp service throws error', async () => { + const run = await bkp.getRun('CALIBRATION', 'SYNTHETIC', 'TPC'); + assert.deepStrictEqual(run, {}); + }); + }); +});