1
0
Fork 0
mirror of https://git.rwth-aachen.de/acs/public/villas/web/ synced 2025-03-09 00:00:01 +01:00

Merge branch 'rework-amqp-actions' into 'master'

Rework AMQP actions

See merge request acs/public/villas/web!78
This commit is contained in:
Sonja Happ 2021-02-23 15:18:31 +00:00
commit 21d647a2bd
7 changed files with 347 additions and 99 deletions

View file

@ -138,6 +138,14 @@ class NotificationsFactory {
};
}
static ACTION_INFO() {
return {
title: 'Action successfully requested',
level: 'info'
};
}
}
export default NotificationsFactory;

View file

@ -17,6 +17,9 @@
import React from 'react';
import { Button, DropdownButton, Dropdown, InputGroup, FormControl } from 'react-bootstrap';
import AppDispatcher from "../common/app-dispatcher";
import NotificationsFactory from "../common/data-managers/notifications-factory";
import NotificationsDataManager from "../common/data-managers/notifications-data-manager";
class ICAction extends React.Component {
constructor(props) {
@ -47,9 +50,183 @@ class ICAction extends React.Component {
};
}
}
return null
}
runAction(action, when) {
if (action.data.action === 'none') {
console.warn("No command selected. Nothing was sent.");
return;
}
if (!this.props.hasConfigs){
let newAction = {};
newAction["action"] = action.data.action
newAction["when"] = when
for (let index of this.props.selectedICs) {
let ic = this.props.ics[index];
let icID = ic.id;
/* VILLAScontroller protocol
see: https://villas.fein-aachen.org/doc/controller-protocol.html
RESET SHUTDOWN
{
"action": "reset/shutdown/stop/pause/resume"
"when": "1234567"
}
DELETE
{
"action": "delete"
"parameters":{
"uuid": "uuid-of-the-manager-for-this-IC"
}
"when": "1234567"
}
CREATE is not possible within ICAction (see add IC)
*/
if (newAction.action === "delete"){
// prepare parameters for delete incl. correct IC id
newAction["parameters"] = {};
newAction.parameters["uuid"] = ic.uuid;
// get the ID of the manager IC
let managerIC = null;
for (let i of this.props.ics){
if (i.uuid === ic.manager){
managerIC = i;
}
}
if (managerIC == null){
NotificationsDataManager.addNotification(NotificationsFactory.DELETE_ERROR("Could not find manager IC with UUID " + ic.manager));
continue;
}
icID = managerIC.id; // send delete action to manager of IC
}
AppDispatcher.dispatch({
type: 'ics/start-action',
icid: icID,
action: newAction,
result: null,
token: this.props.token
});
} // end for loop over selected ICs
} else {
/*VILLAScontoller protocol
see: https://villas.fein-aachen.org/doc/controller-protocol.html
*
* STOP PAUSE RESUME
{
"action": "reset/shutdown/stop/pause/resume"
"when": "1234567"
}
*
* START
{
"action": "start"
"when": 1234567
"parameters": {
Start parameters for this IC as configured in the component config
}
"model": {
"type": "url"
"url": "https://villas.k8s.eonerc.rwth-aachen.de/api/v2/files/{fileID}" where fileID is the model file configured in the component config
"token": "asessiontoken"
}
"results":{
"type": "url"
"url" : "https://villas.k8s.eonerc.rwth-aachen.de/api/v2/results/{resultID}/file" where resultID is the ID of the result created for this run
"token": "asessiontoken"
}
}
*
*
* */
let newActions = [];
for (let config of this.props.selectedConfigs) {
let newAction = {}
newAction["action"] = action.data.action
newAction["when"] = when
// get IC for component config
let ic = null;
for (let component of this.props.ics) {
if (component.id === config.icID) {
ic = component;
}
}
if (ic == null) {
continue;
}
// the following is not required by the protocol; it is an internal help
newAction["icid"] = ic.id
if (newAction.action === 'start') {
newAction["parameters"] = config.startParameters;
if (config.fileIDs.length > 0){
newAction["model"] = {}
newAction.model["type"] = "url"
newAction.model["token"] = this.props.token
// TODO do not default to the first file of the config
newAction.model["url"] = "/files/" + config.fileIDs[0].toString()
}
newAction["results"] = {}
newAction.results["type"] = "url"
newAction.results["token"] = this.props.token
newAction.results["url"] = "/results/RESULTID/file" // RESULTID serves as placeholder and is replaced later
}
// add the new action
newActions.push(newAction);
} // end for loop over selected configs
let newResult = {}
newResult["result"] = {}
if (action.data.action === 'start') {
let configSnapshots = [];
// create config snapshots in case action is start
for (let config of this.props.selectedConfigs) {
let index = this.props.configs.indexOf(config)
configSnapshots.push(this.props.snapshotConfig(index));
}
// create new result for new run
newResult.result["description"] = "Start at " + when;
newResult.result["scenarioID"] = this.props.selectedConfigs[0].scenarioID
newResult.result["configSnapshots"] = configSnapshots
}
console.log("Dispatching actions for configs", newActions, newResult)
AppDispatcher.dispatch({
type: 'ics/start-action',
action: newActions,
result: newResult,
token: this.props.token
});
}
}
setAction = id => {
// search action
for (let action of this.props.actions) {
@ -65,7 +242,13 @@ class ICAction extends React.Component {
render() {
let sendCommandDisabled = this.props.runDisabled || this.state.selectedAction == null || this.state.selectedAction.id === "-1"
let sendCommandDisabled = false;
if (!this.props.hasConfigs && this.props.selectedICs.length === 0 || this.state.selectedAction == null || this.state.selectedAction.id === "-1"){
sendCommandDisabled = true;
}
if (this.props.hasConfigs && this.props.selectedConfigs.length === 0|| this.state.selectedAction == null || this.state.selectedAction.id === "-1"){
sendCommandDisabled = true;
}
let time = this.state.time.getFullYear().pad(4) + '-' +
this.state.time.getMonth().pad(2) + '-' +
@ -98,7 +281,7 @@ class ICAction extends React.Component {
<Button
variant="secondary"
disabled={sendCommandDisabled}
onClick={() => this.props.runAction(this.state.selectedAction, this.state.time)}>Run</Button>
onClick={() => this.runAction(this.state.selectedAction, this.state.time)}>Run</Button>
</InputGroup>
<small className="text-muted">Select time for synced command execution</small>
</div>;

View file

@ -66,16 +66,43 @@ class InfrastructureComponentStore extends ArrayStore {
return state;
case 'ics/start-action':
if (!Array.isArray(action.data))
action.data = [ action.data ]
if (!Array.isArray(action.action))
action.action = [ action.action ]
ICsDataManager.doActions(action.ic, action.data, action.token);
ICsDataManager.doActions(action.icid, action.action, action.token, action.result);
return state;
case 'ics/action-started':
NotificationsDataManager.addNotification(NotificationsFactory.ACTION_INFO());
return state;
case 'ics/action-error':
console.log(action.error);
return state;
case 'ics/action-result-added':
for (let a of action.actions){
if (a.results !== undefined && a.results != null){
// adapt URL for newly created result ID
a.results.url = a.results.url.replace("RESULTID", action.data.result.id);
a.results.url = ICsDataManager.makeURL(a.results.url);
a.results.url = window.location.host + a.results.url;
}
if (a.model !== undefined && a.model != null && JSON.stringify(a.model) !== JSON.stringify({})) {
// adapt URL for model file
a.model.url = ICsDataManager.makeURL(a.model.url);
a.model.url = window.location.host + a.model.url;
}
ICsDataManager.doActions(a.icid, [a], action.token)
}
return state;
case 'ics/action-result-add-error':
console.log(action.error);
return state
case 'ics/get-status':
ICsDataManager.getStatus(action.url, action.token, action.ic);
return super.reduce(state, action);

View file

@ -24,24 +24,86 @@ class IcsDataManager extends RestDataManager {
super('ic', '/ic');
}
doActions(ic, actions, token = null) {
for (let action of actions) {
if (action.when)
// Send timestamp as Unix Timestamp
action.when = Math.round(action.when.getTime() / 1000);
}
doActions(icid, actions, token = null, result=null) {
RestAPI.post(this.makeURL(this.url + '/' + ic.id + '/action'), actions, token).then(response => {
if (icid !== undefined && icid != null && JSON.stringify(icid) !== JSON.stringify({})) {
for (let action of actions) {
if (action.when) {
// Send timestamp as Unix Timestamp
action.when = Math.round(action.when.getTime() / 1000);
}
}
// sending action to a specific IC via IC list
RestAPI.post(this.makeURL(this.url + '/' + icid + '/action'), actions, token).then(response => {
AppDispatcher.dispatch({
type: 'ics/action-started',
data: response
type: 'ics/action-started',
data: response
});
}).catch(error => {
}).catch(error => {
AppDispatcher.dispatch({
type: 'ics/action-error',
type: 'ics/action-error',
error
});
});
} else {
// sending the same action to multiple ICs via scenario controls
// distinguish between "start" action and any other
if (actions[0].action !== "start"){
for (let a of actions){
// sending action to a specific IC via IC list
if (a.when) {
// Send timestamp as Unix Timestamp
a.when = Math.round(a.when.getTime() / 1000);
}
RestAPI.post(this.makeURL(this.url + '/' + a.icid + '/action'), [a], token).then(response => {
AppDispatcher.dispatch({
type: 'ics/action-started',
data: response
});
}).catch(error => {
AppDispatcher.dispatch({
type: 'ics/action-error',
error
});
});
}
} else{
// for start actions procedure is different
// first a result needs to be created, then the start actions can be sent
RestAPI.post(this.makeURL( '/results'), result, token).then(response => {
AppDispatcher.dispatch({
type: 'ics/action-result-added',
data: response,
actions: actions,
token: token,
});
AppDispatcher.dispatch({
type: "results/added",
data: response.result,
});
}).catch(error => {
AppDispatcher.dispatch({
type: 'ics/action-result-add-error',
error
});
});
});
}
}
}
getStatus(url,token,ic){

View file

@ -36,6 +36,8 @@ import ICDialog from './ic-dialog';
import ICAction from './ic-action';
import DeleteDialog from '../common/dialogs/delete-dialog';
import NotificationsDataManager from "../common/data-managers/notifications-data-manager";
import NotificationsFactory from "../common/data-managers/notifications-factory";
class InfrastructureComponents extends Component {
static getStores() {
@ -149,11 +151,35 @@ class InfrastructureComponents extends Component {
this.setState({ newModal : false });
if (data) {
AppDispatcher.dispatch({
type: 'ics/start-add',
data,
token: this.state.sessionToken,
});
if (!data.managedexternally) {
AppDispatcher.dispatch({
type: 'ics/start-add',
data,
token: this.state.sessionToken,
});
} else {
// externally managed IC: dispatch create action to selected manager
let newAction = {};
newAction["action"] = "create";
newAction["parameters"] = data;
newAction["when"] = new Date()
// find the manager IC
let managerIC = this.state.ics.find(ic => ic.uuid === data.manager)
if (managerIC === null || managerIC === undefined){
NotificationsDataManager.addNotification(NotificationsFactory.ADD_ERROR("Could not find manager IC with UUID " + data.manager));
return;
}
AppDispatcher.dispatch({
type: 'ics/start-action',
icid: managerIC.id,
action: newAction,
result: null,
token: this.state.sessionToken
});
}
}
}
@ -240,18 +266,7 @@ class InfrastructureComponents extends Component {
this.setState({ selectedICs: selectedICs });
}
runAction(action, when) {
for (let index of this.state.selectedICs) {
action.when = when;
AppDispatcher.dispatch({
type: 'ics/start-action',
ic: this.state.ics[index],
data: action.data,
token: this.state.sessionToken,
});
}
}
static isICOutdated(component) {
if (!component.stateUpdateAt)
@ -420,19 +435,19 @@ class InfrastructureComponents extends Component {
modifier={(stateUpdateAt, component) => this.stateUpdateModifier(stateUpdateAt, component)}
/>
{this.state.currentUser.role === "Admin" && editable ?
{this.state.currentUser.role === "Admin" ?
<TableColumn
width='200'
editButton
width='150'
editButton = {(index) => !this.isExternalIC(index)}
exportButton
deleteButton
deleteButton = {(index) => !this.isExternalIC(index)}
onEdit={index => this.setState({editModal: true, modalIC: ics[index], modalIndex: index})}
onExport={index => this.exportIC(index)}
onDelete={index => this.setState({deleteModal: true, modalIC: ics[index], modalIndex: index})}
/>
:
<TableColumn
width='100'
width='50'
exportButton
onExport={index => this.exportIC(index)}
/>
@ -496,12 +511,15 @@ class InfrastructureComponents extends Component {
{this.state.currentUser.role === "Admin" && this.state.numberOfExternalICs > 0 ?
<div style={{float: 'left'}}>
<ICAction
runDisabled={this.state.selectedICs.length === 0}
runAction={(action, when) => this.runAction(action, when)}
hasConfigs = {false}
ics={this.state.ics}
selectedICs={this.state.selectedICs}
token={this.state.sessionToken}
actions={[
{id: '-1', title: 'Action', data: {action: 'none'}},
{id: '0', title: 'Reset', data: {action: 'reset'}},
{id: '1', title: 'Shutdown', data: {action: 'shutdown'}},
{id: '2', title: 'Delete', data: {action: 'delete'}}
]}
/>
</div>

View file

@ -170,7 +170,7 @@ class NewICDialog extends React.Component {
{this.props.managers.length > 0 ?
<>
<FormGroup controlId="managedexternally">
<OverlayTrigger key="3" placement={'left'} overlay={<Tooltip id={`tooltip-${"me"}`}>An externally managed component is created and managed by an IC manager via AMQP</Tooltip>} >
<OverlayTrigger key="-1" placement={'left'} overlay={<Tooltip id={`tooltip-${"me"}`}>An externally managed component is created and managed by an IC manager via AMQP</Tooltip>} >
<FormCheck type={"checkbox"} label={"Managed externally"} defaultChecked={this.state.managedexternally} onChange={e => this.handleChange(e)}>
</FormCheck>
</OverlayTrigger>

View file

@ -394,60 +394,6 @@ class Scenario extends React.Component {
}
runAction(action, when) {
if (action.data.action === 'none') {
console.warn("No command selected. Nothing was sent.");
return;
}
let configs = [];
for (let index of this.state.selectedConfigs) {
// get IC for component config
let ic = null;
for (let component of this.state.ics) {
if (component.id === this.state.configs[index].icID) {
ic = component;
}
}
if (ic == null) {
continue;
}
if (action.data.action === 'start') {
configs.push(this.copyConfig(index));
action.data.parameters = this.state.configs[index].startParameters;
}
action.data.when = when;
console.log("Sending action: ", action.data)
AppDispatcher.dispatch({
type: 'ics/start-action',
ic: ic,
data: action.data,
token: this.state.sessionToken
});
}
if (configs.length !== 0) { //create result (only if command was 'start')
let componentConfigs = {};
componentConfigs["configs"] = configs;
let data = {};
data["Description"] = "Run " + this.state.scenario.name; // default description, to be change by user later
data["ResultFileIDs"] = [];
data["scenarioID"] = this.state.scenario.id;
data["ConfigSnapshots"] = JSON.stringify(componentConfigs, null, 2);
AppDispatcher.dispatch({
type: 'results/start-add',
data,
token: this.state.sessionToken,
})
}
};
getICName(icID) {
for (let ic of this.state.ics) {
if (ic.id === icID) {
@ -710,7 +656,7 @@ class Scenario extends React.Component {
}
openResultConfigSnaphots(result) {
if (!result.configSnapshots || result.configSnapshots == "") {
if (result.configSnapshots === null || result.configSnapshots === undefined) {
this.setState({
modalResultConfigs: {"configs": []},
modalResultConfigsIndex: result.id,
@ -718,7 +664,7 @@ class Scenario extends React.Component {
});
} else {
this.setState({
modalResultConfigs: JSON.parse(result.configSnapshots),
modalResultConfigs: result.configSnapshots,
modalResultConfigsIndex: result.id,
resultConfigsModal: true
});
@ -928,8 +874,12 @@ class Scenario extends React.Component {
{this.state.ExternalICInUse ? (
<div style={{ float: 'left' }}>
<ICAction
runDisabled={this.state.selectedConfigs.length === 0}
runAction={(action, when) => this.runAction(action, when)}
hasConfigs={true}
ics={this.state.ics}
configs={this.state.configs}
selectedConfigs = {this.state.selectedConfigs}
snapshotConfig = {(index) => this.copyConfig(index)}
token = {this.state.sessionToken}
actions={[
{ id: '-1', title: 'Action', data: { action: 'none' } },
{ id: '0', title: 'Start', data: { action: 'start' } },