diff --git a/public/components/common/restart-cluster-manager-callout.tsx b/public/components/common/restart-cluster-manager-callout.tsx deleted file mode 100644 index cf06d7aec8..0000000000 --- a/public/components/common/restart-cluster-manager-callout.tsx +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Wazuh app - React component for registering agents. - * Copyright (C) 2015-2022 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ - - import React, { Component, Fragment } from 'react'; - - // Eui components -import { - EuiFlexGroup, - EuiFlexItem, - EuiButton, - EuiCallOut, - EuiOverlayMask, - EuiConfirmModal, - EuiText, - EuiIcon -} from '@elastic/eui'; - -import { getToasts } from '../../kibana-services'; -import { updateWazuhNotReadyYet } from '../../redux/actions/appStateActions'; -import { clusterReq, restartClusterOrManager } from '../../controllers/management/components/management/configuration/utils/wz-fetch'; -import { connect } from 'react-redux'; - -interface IWzRestartClusterManagerCalloutProps{ - updateWazuhNotReadyYet: (wazuhNotReadyYet) => void - onRestarted: () => void - onRestartedError: () => void -}; - -interface IWzRestartClusterManagerCalloutState{ - warningRestarting: boolean - warningRestartModalVisible: boolean - isCluster: boolean -}; - -class WzRestartClusterManagerCallout extends Component{ - constructor(props){ - super(props); - this.state = { - warningRestarting: false, - warningRestartModalVisible: false, - isCluster: false - }; - } - toggleWarningRestartModalVisible(){ - this.setState({ warningRestartModalVisible: !this.state.warningRestartModalVisible }) - } - showToast(color, title, text = '', time = 3000){ - getToasts().add({ - color, - title, - text, - toastLifeTimeMs: time - }); - } - restartClusterOrManager = async () => { - try{ - this.setState({ warningRestarting: true, warningRestartModalVisible: false}); - const data = await restartClusterOrManager(this.props.updateWazuhNotReadyYet); - this.props.onRestarted(); - this.showToast('success', `${data.restarted} was restarted`); - }catch(error){ - this.setState({ warningRestarting: false }); - this.props.updateWazuhNotReadyYet(false); - this.props.onRestartedError(); - this.showToast('danger', 'Error', error.message || error ); - } - }; - async componentDidMount(){ - try{ - const clusterStatus = await clusterReq(); - this.setState( { isCluster: clusterStatus.data.data.enabled === 'yes' && clusterStatus.data.data.running === 'yes' }); - }catch(error){} - } - render(){ - const { warningRestarting, warningRestartModalVisible } = this.state; - return ( - - {!warningRestarting && ( - - - - - - Changes will not take effect until a restart is performed. - - - - this.toggleWarningRestartModalVisible()} - > - {'Restart'} - - - - - )} - {warningRestartModalVisible && ( - - this.toggleWarningRestartModalVisible()} - onConfirm={() => this.restartClusterOrManager()} - cancelButtonText="Cancel" - confirmButtonText="Confirm" - defaultFocusedButton="cancel" - > - - )} - - ) - } -} - -const mapDispatchToProps = dispatch => { - return { - updateWazuhNotReadyYet: wazuhNotReadyYet => dispatch(updateWazuhNotReadyYet(wazuhNotReadyYet)) - } -}; - -export default connect(null, mapDispatchToProps)(WzRestartClusterManagerCallout) \ No newline at end of file diff --git a/public/components/common/restart-modal/restart-modal.tsx b/public/components/common/restart-modal/restart-modal.tsx new file mode 100644 index 0000000000..6287f4e4f3 --- /dev/null +++ b/public/components/common/restart-modal/restart-modal.tsx @@ -0,0 +1,258 @@ +import React, { useEffect, useState } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiEmptyPrompt, + EuiEmptyPromptProps, + EuiFlexGroup, + EuiFlexItem, + EuiOverlayMask, + EuiProgress, +} from '@elastic/eui'; +import { getHttp } from '../../../kibana-services'; +import { WazuhConfig } from '../../../react-services/wazuh-config'; +import { RestartHandler } from '../../../react-services/wz-restart'; +import { getAssetURL, getThemeAssetURL } from '../../../utils/assets'; +import { useDispatch, useSelector } from 'react-redux'; +import { + updateRestartStatus, + updateRestartAttempt, + updateSyncCheckAttempt, + updateUnsynchronizedNodes, +} from '../../../redux/actions/appStateActions'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; + +/** + * The restart modal to show feedback to the user. + * @param props component's props + * @returns components's body + */ +export const RestartModal = (props: { useDelay?: boolean, showWarningRestart? }) => { + // Component props + const { useDelay = false } = props; + + // Use default HEALTHCHECK_DELAY + const [timeToHC, setTimeToHC] = useState(RestartHandler.HEALTHCHECK_DELAY / 1000); + + // Use default SYNC_DELAY + const [syncDelay, setSyncDelay] = useState(RestartHandler.SYNC_DELAY / 1000); + + // Restart polling counter + const restartAttempt = useSelector((state) => state.appStateReducers.restartAttempt); + + // Cluster nodes that did not synced + const unsyncedNodes = useSelector((state) => state.appStateReducers.unsynchronizedNodes); + + // Sync polling counter + const syncPollingAttempt = useSelector((state) => state.appStateReducers.syncCheckAttempt); + + // Current status of the restarting process + const restartStatus = useSelector((state) => state.appStateReducers.restartStatus); + + // Max attempts = MAX_SYNC_POLLING_ATTEMPTS + MAX_RESTART_POLLING_ATTEMPTS + const maxAttempts = useDelay + ? RestartHandler.MAX_RESTART_POLLING_ATTEMPTS + RestartHandler.MAX_SYNC_POLLING_ATTEMPTS + : RestartHandler.MAX_RESTART_POLLING_ATTEMPTS; + + // Current attempt + const attempts = useDelay ? restartAttempt + syncPollingAttempt : restartAttempt; + + // Load Wazuh logo + const wzConfig = new WazuhConfig().getConfig(); + const logotypeURL = getHttp().basePath.prepend( + wzConfig['customization.logo.sidebar'] + ? getAssetURL(wzConfig['customization.logo.sidebar']) + : getThemeAssetURL('icon.svg') + ); + + // Apply SYNC_DELAY if useDelay prop is enabled. + useEffect(() => { + if (useDelay) { + countdown(syncDelay, setSyncDelay); + } + }, []); + + // Apply HEALTHCHECK_DELAY when the restart has failed + useEffect(() => { + restartStatus === RestartHandler.RESTART_STATES.RESTART_ERROR && + countdown(timeToHC, setTimeToHC); + + restartStatus === RestartHandler.RESTART_STATES.RESTARTED_INFO && + restartedTimeout(); + }, [restartStatus]); + + // TODO + const countdown = (time: number, setState) => { + let countDown = time; + + const interval = setInterval(() => { + setState(countDown); + if (countDown === 0 && setState === setTimeToHC) { + clearInterval(interval); + RestartHandler.goToHealthcheck(); + } else if (countDown === 0) { + clearInterval(interval); + } + + countDown--; + }, 1000 /* 1 second */); + }; + + const restartedTimeout = () => { + setTimeout(() => { + dispatch(updateRestartStatus(RestartHandler.RESTART_STATES.RESTARTED)); + if(props.showWarningRestart){ + props.showWarningRestart() + } + }, RestartHandler.INFO_RESTART_SUCCESS_DELAY); + } + + // TODO review if importing these functions in wz-restart work. + const dispatch = useDispatch(); + const updateRedux = { + updateRestartAttempt: (restartAttempt) => dispatch(updateRestartAttempt(restartAttempt)), + updateSyncCheckAttempt: (syncCheckAttempt) => + dispatch(updateSyncCheckAttempt(syncCheckAttempt)), + updateUnsynchronizedNodes: (unsynchronizedNodes) => + dispatch(updateUnsynchronizedNodes(unsynchronizedNodes)), + updateRestartStatus: (restartStatus) => dispatch(updateRestartStatus(restartStatus)), + }; + + const forceRestart = async () => { + try{ + await RestartHandler.restartWazuh(updateRedux); + }catch(error){ + const options = { + context: `${RestartModal.name}.forceRestart`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } + }; + + + const abort = () => { + dispatch(updateRestartStatus(RestartHandler.RESTART_STATES.RESTARTED)); + dispatch(updateUnsynchronizedNodes([])); + dispatch(updateRestartAttempt(0)); + dispatch(updateSyncCheckAttempt(0)); + } + + // Build the modal depending on the restart state. + let emptyPromptProps: Partial; + switch (restartStatus) { + default: + case RestartHandler.RESTART_STATES.RESTARTED_INFO: + emptyPromptProps = { + title: ( + <> + +

Wazuh restarted

+ + ), + body: ( + <> + + + ), + }; + break; + + case RestartHandler.RESTART_STATES.RESTARTING: + emptyPromptProps = { + title: ( + <> + +

Restarting Wazuh

+ + ), + body: ( + <> + + + ), + }; + break; + + case RestartHandler.RESTART_STATES.RESTART_ERROR: + emptyPromptProps = { + iconType: 'alert', + iconColor: 'danger', + title:

Unable to connect to Wazuh.

, + body:

There was an error restarting Wazuh. The Healthcheck will run automatically.

, + actions: ( + + Go to Healthcheck ({timeToHC} s) + + ), + }; + break; + + case RestartHandler.RESTART_STATES.SYNC_ERROR: + emptyPromptProps = { + iconType: 'alert', + iconColor: 'danger', + title:

Synchronization failed

, + body: ( +

+ The nodes {unsyncedNodes.join(', ')} did not synchronize. Restarting Wazuh might set the + cluster into an inconsistent state. Close and try again later. +

+ ), + actions: ( + + + + Force restart + + + + + Close + + + + ), + }; + break; + + case RestartHandler.RESTART_STATES.SYNCING: + emptyPromptProps = { + title: ( + <> + +

Synchronizing Wazuh

+ + ), + body: ( + <> + + + ), + }; + break; + } + + // + return ( + +
+ +
+
+ ); +}; diff --git a/public/components/common/wz-restart-callout.tsx b/public/components/common/wz-restart-callout.tsx new file mode 100644 index 0000000000..76fd2df5a5 --- /dev/null +++ b/public/components/common/wz-restart-callout.tsx @@ -0,0 +1,183 @@ +/* + * Wazuh app - React component for registering agents. + * Copyright (C) 2015-2022 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ + +import React, { Component, Fragment } from 'react'; + +// Eui components +import { + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiCallOut, + EuiOverlayMask, + EuiConfirmModal, + EuiText, + EuiIcon, +} from '@elastic/eui'; + +import { getToasts } from '../../kibana-services'; +import { updateRestartAttempt, updateSyncCheckAttempt, updateUnsynchronizedNodes, updateRestartStatus } from '../../redux/actions/appStateActions'; +import { RestartHandler } from '../../react-services/wz-restart'; +import { connect } from 'react-redux'; +import { RestartModal } from './restart-modal/restart-modal'; + +interface IWzRestartCalloutProps { + updateRestartAttempt: (restartAttempt) => void; + updateSyncCheckAttempt: (syncCheckAttempt) => void; + updateUnsynchronizedNodes: (unsynchronizedNodes) => void; + updateRestartStatus: (restartStatus) => void; + onRestarted: () => void; + onRestartedError: () => void; + restartStatus: string; +} + +interface IWzRestartCalloutState { + warningRestarting: boolean; + warningRestartModalVisible: boolean; + isCluster: boolean; + isRestarting: boolean; + timeoutRestarting: boolean; +} + +class WzRestartCallout extends Component { + constructor(props) { + super(props); + this.state = { + warningRestarting: false, + warningRestartModalVisible: false, + isCluster: false, + isRestarting: false, + timeoutRestarting: false, + }; + } + toggleWarningRestartModalVisible() { + this.setState({ + warningRestartModalVisible: !this.state.warningRestartModalVisible, + isRestarting: false, + }); + } + showToast(color, title, text = '', time = 3000) { + getToasts().add({ + color, + title, + text, + toastLifeTimeMs: time, + }); + } + restartWazuh = async () => { + try { + this.setState({ + warningRestarting: true, + warningRestartModalVisible: false, + isRestarting: true, + timeoutRestarting: true, + }); + const updateRedux = { + updateRestartAttempt: this.props.updateRestartAttempt, + updateSyncCheckAttempt: this.props.updateSyncCheckAttempt, + updateUnsynchronizedNodes: this.props.updateUnsynchronizedNodes, + updateRestartStatus: this.props.updateRestartStatus, + }; + await RestartHandler.restartWazuh(updateRedux, this.state.isCluster); + this.setState({ isRestarting: false }); + } catch (error) { + this.setState({ warningRestarting: false, isRestarting: false }); + this.props.onRestartedError(); + this.showToast('danger', 'Error', error.message || error); + } + }; + async componentDidMount() { + try { + const clusterStatus = await RestartHandler.clusterReq(); + this.setState({ + isCluster: + clusterStatus.data.data.enabled === 'yes' && clusterStatus.data.data.running === 'yes', + }); + } catch (error) {} + } + render() { + const { + warningRestarting, + warningRestartModalVisible, + isCluster, + timeoutRestarting, + } = this.state; + return ( + + {!warningRestarting && ( + + + + + + Changes will not take effect until a restart is performed. + + + + this.toggleWarningRestartModalVisible()} + > + {'Restart'} + + + + + )} + {warningRestartModalVisible && ( + + this.toggleWarningRestartModalVisible()} + onConfirm={() => { + isCluster + ? this.props.updateRestartStatus(RestartHandler.RESTART_STATES.SYNCING) + : this.props.updateRestartStatus(RestartHandler.RESTART_STATES.RESTARTING); + this.restartWazuh(); + }} + cancelButtonText="Cancel" + confirmButtonText="Confirm" + defaultFocusedButton="cancel" + > + + )} + {timeoutRestarting && + this.props.restartStatus !== RestartHandler.RESTART_STATES.RESTARTED && ( + this.props.onRestarted()} /> + )} + + ); + } +} + +const mapStateToProps = state => ({ + restartStatus: state.appStateReducers.restartStatus, +}); + +const mapDispatchToProps = (dispatch) => { + return { + updateRestartAttempt: (restartAttempt) => + dispatch(updateRestartAttempt(restartAttempt)), + updateSyncCheckAttempt: (syncCheckAttempt) => + dispatch(updateSyncCheckAttempt(syncCheckAttempt)), + updateUnsynchronizedNodes: (unsynchronizedNodes) => + dispatch(updateUnsynchronizedNodes(unsynchronizedNodes)), + updateRestartStatus: (restartStatus) => + dispatch(updateRestartStatus(restartStatus)), + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(WzRestartCallout); diff --git a/public/controllers/management/components/management/configuration/edit-configuration/edit-configuration.js b/public/controllers/management/components/management/configuration/edit-configuration/edit-configuration.js index 6bd807fe20..dd740995d9 100644 --- a/public/controllers/management/components/management/configuration/edit-configuration/edit-configuration.js +++ b/public/controllers/management/components/management/configuration/edit-configuration/edit-configuration.js @@ -30,19 +30,19 @@ import WzConfigurationPath from '../util-components/configuration-path'; import WzRefreshClusterInfoButton from '../util-components/refresh-cluster-info-button'; import { WzButtonPermissions } from '../../../../../../components/common/permissions/button'; import withLoading from '../util-hocs/loading'; -import { updateWazuhNotReadyYet } from '../../../../../../redux/actions/appStateActions'; +import { updateRestartStatus, updateRestartAttempt, updateSyncCheckAttempt, updateUnsynchronizedNodes } from '../../../../../../redux/actions/appStateActions'; import { updateClusterNodes, updateClusterNodeSelected, } from '../../../../../../redux/actions/configurationActions'; import { fetchFile, - restartNodeSelected, saveFileManager, saveFileCluster, clusterNodes, clusterReq, } from '../utils/wz-fetch'; +import { RestartHandler } from '../../../../../../react-services/wz-restart'; import { validateXML } from '../utils/xml'; import { getToasts } from '../../../../../..//kibana-services'; @@ -52,6 +52,7 @@ import { compose } from 'redux'; import { UI_LOGGER_LEVELS } from '../../../../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../../../../react-services/common-services'; +import { RestartModal } from '../../../../../../components/common/restart-modal/restart-modal'; class WzEditConfiguration extends Component { constructor(props) { @@ -66,6 +67,7 @@ class WzEditConfiguration extends Component { hasChanges: false, infoChangesAfterRestart: false, disableSaveRestartButtons: false, + timeoutRestarting: false }; } @@ -157,34 +159,19 @@ class WzEditConfiguration extends Component { async confirmRestart() { try { - this.setState({ restarting: true, saving: true, infoChangesAfterRestart: false }); - await restartNodeSelected(this.props.clusterNodeSelected, this.props.updateWazuhNotReadyYet); - this.props.updateWazuhNotReadyYet(''); + this.setState({ restarting: true, saving: true, infoChangesAfterRestart: false, timeoutRestarting:true }); + const updateRedux = { + updateRestartStatus: this.props.updateRestartStatus, + updateSyncCheckAttempt: this.props.updateSyncCheckAttempt, + updateUnsynchronizedNodes: this.props.updateUnsynchronizedNodes, + updateRestartAttempt: this.props.updateRestartAttempt, + } + await RestartHandler.restartSelectedNode( + this.props.clusterNodeSelected, updateRedux + ); this.setState({ restart: false, saving: false, restarting: false }); await this.checkIfClusterOrManager(); - if (this.props.clusterNodes) { - this.addToast({ - title: ( - - -   - - Nodes could take some time to restart, it may be necessary to perform a refresh to - see them all. - - - ), - color: 'success', - }); - } - if(!this.props.clusterNodeSelected){ - this.addToast({ - title: 'Manager was restarted', - color: 'success', - }); - } } catch (error) { - this.props.updateWazuhNotReadyYet(''); this.setState({ restart: false, saving: false, restarting: false }); const options = { context: `${WzEditConfiguration.name}.confirmRestart`, @@ -200,6 +187,7 @@ class WzEditConfiguration extends Component { } } + async checkIfClusterOrManager() { try { // in case which enable/disable cluster configuration, update Redux Store @@ -234,7 +222,7 @@ class WzEditConfiguration extends Component { } } render() { - const { restart, restarting, saving, editorValue, disableSaveRestartButtons } = this.state; + const { restart, restarting, saving, editorValue, disableSaveRestartButtons, timeoutRestarting } = this.state; const { clusterNodeSelected, agent } = this.props; const xmlError = editorValue && validateXML(editorValue); return ( @@ -312,6 +300,10 @@ class WzEditConfiguration extends Component { /> )} + { + timeoutRestarting && this.props.restartStatus !== RestartHandler.RESTART_STATES.RESTARTED && ( + + )} ); } @@ -321,18 +313,22 @@ const mapStateToProps = (state) => ({ wazuhNotReadyYet: state.appStateReducers.wazuhNotReadyYet, clusterNodes: state.configurationReducers.clusterNodes, clusterNodeSelected: state.configurationReducers.clusterNodeSelected, + restartStatus: state.appStateReducers.restartStatus, }); const mapDispatchToProps = (dispatch) => ({ updateClusterNodes: (clusterNodes) => dispatch(updateClusterNodes(clusterNodes)), updateClusterNodeSelected: (clusterNodeSelected) => dispatch(updateClusterNodeSelected(clusterNodeSelected)), - updateWazuhNotReadyYet: (value) => dispatch(updateWazuhNotReadyYet(value)), + updateRestartAttempt: (value) => dispatch(updateRestartAttempt(value)), + updateRestartStatus: (value) => dispatch(updateRestartStatus(value)), + updateSyncCheckAttempt: (value) => dispatch(updateSyncCheckAttempt(value)), + updateUnsynchronizedNodes: (value) => dispatch(updateUnsynchronizedNodes(value)), }); WzEditConfiguration.propTypes = { wazuhNotReadyYet: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), - updateWazuhNotReadyYet: PropTypes.func, + updateRestartAttempt: PropTypes.func, }; export default connect(mapStateToProps, mapDispatchToProps)(WzEditConfiguration); diff --git a/public/controllers/management/components/management/configuration/utils/wz-fetch.js b/public/controllers/management/components/management/configuration/utils/wz-fetch.js index 5ce3ef50ce..381b9c1ffe 100644 --- a/public/controllers/management/components/management/configuration/utils/wz-fetch.js +++ b/public/controllers/management/components/management/configuration/utils/wz-fetch.js @@ -12,7 +12,6 @@ import { WzRequest } from '../../../../../../react-services/wz-request'; import { replaceIllegalXML } from './xml'; -import { getToasts } from '../../../../../../kibana-services'; import { delayAsPromise } from '../../../../../../../common/utils'; /** @@ -186,6 +185,7 @@ export const checkDaemons = async (isCluster) => { }; /** + * @deprecated * Make ping to Wazuh API * @param updateWazuhNotReadyYet * @param {boolean} isCluster @@ -262,106 +262,6 @@ export const fetchFile = async selectedNode => { } }; -/** - * Restart a node or manager - * @param {} selectedNode Cluster Node - * @param updateWazuhNotReadyYet - */ -export const restartNodeSelected = async (selectedNode, updateWazuhNotReadyYet) => { - try { - const clusterStatus = (((await clusterReq()) || {}).data || {}).data || {}; - const isCluster = clusterStatus.enabled === 'yes' && clusterStatus.running === 'yes'; - // Dispatch a Redux action - updateWazuhNotReadyYet(`Restarting ${isCluster ? selectedNode : 'Manager'}, please wait.`); //FIXME: if it enables/disables cluster, this will show Manager instead node name - isCluster ? await restartNode(selectedNode) : await restartManager(); - return await makePing(updateWazuhNotReadyYet, isCluster); - } catch (error) { - throw error; - } -}; - -/** - * Restart manager (single-node API call) - * @returns {object|Promise} - */ -export const restartManager = async () => { - try { - const validationError = await WzRequest.apiReq( - 'GET', - `/manager/configuration/validation`, {} - ); - const isOk = validationError.status === 'OK'; - if (!isOk && validationError.detail) { - const str = validationError.detail; - throw new Error(str); - } - const result = await WzRequest.apiReq('PUT', `/manager/restart`, {}); - return result; - } catch (error) { - throw error; - } -}; - -/** - * Restart cluster - * @returns {object|Promise} - */ -export const restartCluster = async () => { - try { - const validationError = await WzRequest.apiReq( - 'GET', - `/cluster/configuration/validation`, {} - ); - - const isOk = validationError.status === 'OK'; - if (!isOk && validationError.detail) { - const str = validationError.detail; - throw new Error(str); - } - // this.performClusterRestart(); // TODO: convert AngularJS to React - await WzRequest.apiReq('PUT', `/cluster/restart`, { - delay: 15000 - }); - // this.$rootScope.$broadcast('removeRestarting', {}); TODO: isRestarting: false? - return { - data: { - data: 'Restarting cluster' - } - }; - } catch (error) { - throw error; - } -}; - -/** - * Restart a cluster node - * @returns {object|Promise} - */ -export const restartNode = async node => { - try { - const node_param = node && typeof node == 'string' ? `?nodes_list=${node}` : ''; - - const validationError = await WzRequest.apiReq( - 'GET', - `/cluster/configuration/validation`, {} - ); - - const isOk = validationError.status === 200; - if (!isOk && validationError.detail) { - const str = validationError.detail; - throw new Error(str); - } - const result = await WzRequest.apiReq( - 'PUT', - `/cluster/restart${node_param}`, {delay: 15000} - ); - - return result; - } catch (error) { - throw error; - } -}; - export const saveConfiguration = async (selectedNode, xml) => { try { const clusterStatus = (((await clusterReq()) || {}).data || {}).data || {}; @@ -513,28 +413,4 @@ export const checkCurrentSecurityPlatform = async () => { } catch (error) { throw error; } -}; - -/** - * Restart cluster or Manager - */ -export const restartClusterOrManager = async (updateWazuhNotReadyYet) => { - try { - const clusterStatus = (((await clusterReq()) || {}).data || {}).data || {}; - const isCluster = clusterStatus.enabled === 'yes' && clusterStatus.running === 'yes'; - getToasts().add({ - color: 'success', - title: isCluster - ? 'Restarting cluster, it will take up to 30 seconds.' - : 'The manager is being restarted', - toastLifeTimeMs: 3000, - }); - isCluster ? await restartCluster() : await restartManager(); - // Dispatch a Redux action - updateWazuhNotReadyYet(`Restarting ${isCluster ? 'Cluster' : 'Manager'}, please wait.`); - await makePing(updateWazuhNotReadyYet, isCluster); - return { restarted: isCluster ? 'Cluster' : 'Manager' }; - } catch (error) { - throw error; - } -}; +}; \ No newline at end of file diff --git a/public/controllers/management/components/management/ruleset/actions-buttons.js b/public/controllers/management/components/management/ruleset/actions-buttons.js index 515496f6b9..d9d87dc056 100644 --- a/public/controllers/management/components/management/ruleset/actions-buttons.js +++ b/public/controllers/management/components/management/ruleset/actions-buttons.js @@ -26,7 +26,7 @@ import { import exportCsv from '../../../../../react-services/wz-csv'; import { UploadFiles } from '../../upload-files'; import columns from './utils/columns'; -import { resourceDictionary, RulesetHandler, RulesetResources } from './utils/ruleset-handler'; +import { resourceDictionary, RulesetHandler } from './utils/ruleset-handler'; import { WzButtonPermissions } from '../../../../../components/common/permissions/button'; import { connect } from 'react-redux'; diff --git a/public/controllers/management/components/management/ruleset/list-editor.js b/public/controllers/management/components/management/ruleset/list-editor.js index b91b4e6997..abf701f483 100644 --- a/public/controllers/management/components/management/ruleset/list-editor.js +++ b/public/controllers/management/components/management/ruleset/list-editor.js @@ -18,10 +18,8 @@ import { EuiTitle, EuiToolTip, EuiButtonIcon, - EuiButton, EuiText, EuiButtonEmpty, - EuiPopover, EuiFieldText, EuiSpacer, EuiPanel, @@ -38,7 +36,7 @@ import { getToasts } from '../../../../../kibana-services'; import exportCsv from '../../../../../react-services/wz-csv'; import { updateWazuhNotReadyYet } from '../../../../../redux/actions/appStateActions'; -import WzRestartClusterManagerCallout from '../../../../../components/common/restart-cluster-manager-callout'; +import WzRestartCallout from '../../../../../components/common/wz-restart-callout'; import { WzButtonPermissions } from '../../../../../components/common/permissions/button'; import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; @@ -602,7 +600,7 @@ class WzListEditor extends Component { {this.state.showWarningRestart && ( - this.setState({ showWarningRestart: false })} onRestartedError={() => this.setState({ showWarningRestart: true })} /> diff --git a/public/controllers/management/components/management/ruleset/ruleset-editor.js b/public/controllers/management/components/management/ruleset/ruleset-editor.js index 73046b2d84..adff07289c 100644 --- a/public/controllers/management/components/management/ruleset/ruleset-editor.js +++ b/public/controllers/management/components/management/ruleset/ruleset-editor.js @@ -39,7 +39,7 @@ import validateConfigAfterSent from './utils/valid-configuration'; import { getToasts } from '../../../../../kibana-services'; import { updateWazuhNotReadyYet } from '../../../../../redux/actions/appStateActions'; -import WzRestartClusterManagerCallout from '../../../../../components/common/restart-cluster-manager-callout'; +import WzRestartCallout from '../../../../../components/common/wz-restart-callout'; import { validateXML } from '../configuration/utils/xml'; import { WzButtonPermissions } from '../../../../../components/common/permissions/button'; import 'brace/theme/textmate'; @@ -353,7 +353,7 @@ class WzRulesetEditor extends Component { {this.state.showWarningRestart && ( - this.setState({ showWarningRestart: false })} onRestartedError={() => this.setState({ showWarningRestart: true })} /> diff --git a/public/controllers/management/components/management/ruleset/ruleset-overview.js b/public/controllers/management/components/management/ruleset/ruleset-overview.js index d6d31a2924..0bedd14fb6 100644 --- a/public/controllers/management/components/management/ruleset/ruleset-overview.js +++ b/public/controllers/management/components/management/ruleset/ruleset-overview.js @@ -19,7 +19,7 @@ import WzRulesetSearchBar from './ruleset-search-bar'; import WzRulesetActionButtons from './actions-buttons'; import './ruleset-overview.scss'; import { withUserAuthorizationPrompt, withGlobalBreadcrumb } from '../../../../../components/common/hocs'; -import WzRestartClusterManagerCallout from '../../../../../components/common/restart-cluster-manager-callout'; +import WzRestartCallout from '../../../../../components/common/wz-restart-callout'; import { compose } from 'redux'; import { resourceDictionary } from './utils/ruleset-handler'; @@ -68,7 +68,7 @@ class WzRulesetOverview extends Component { {this.state.showWarningRestart && ( <> - this.updateRestartManagers(false)} onRestartedError={() => this.updateRestartManagers(true)} /> diff --git a/public/controllers/management/components/management/status/actions-buttons-main.js b/public/controllers/management/components/management/status/actions-buttons-main.js index ce95c92052..94f496d18a 100644 --- a/public/controllers/management/components/management/status/actions-buttons-main.js +++ b/public/controllers/management/components/management/status/actions-buttons-main.js @@ -13,7 +13,6 @@ import React, { Component, Fragment } from 'react'; // Eui components import { EuiFlexItem, - EuiButtonEmpty, EuiSelect, EuiOverlayMask, EuiConfirmModal @@ -37,6 +36,9 @@ import { WzButtonPermissions } from '../../../../../components/common/permission import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; import { getErrorOrchestrator } from '../../../../../react-services/common-services'; +import { RestartHandler } from '../../../../../react-services/wz-restart'; +import { updateRestartAttempt, updateRestartStatus } from '../../../../../redux/actions/appStateActions'; +import { RestartModal } from '../../../../../components/common/restart-modal/restart-modal' class WzStatusActionButtons extends Component { _isMounted = false; @@ -48,7 +50,8 @@ class WzStatusActionButtons extends Component { this.statusHandler = StatusHandler; this.state = { isModalVisible: false, - isRestarting: false + isRestarting: false, + timeoutRestarting: false, }; } @@ -56,60 +59,29 @@ class WzStatusActionButtons extends Component { this._isMounted = true; } - componentDidUpdate() {} - componentWillUnmount() { this._isMounted = false; } /** - * Restart cluster + * Restart cluster or manager */ - async restartCluster() { - this.setState({ isRestarting: true }); + async restartWazuh() { + this.setState({ isRestarting: true, timeoutRestarting: true }); try { - const result = await this.statusHandler.restartCluster(); + const updateRedux = {updateRestartAttempt: this.props.updateRestartAttempt , updateRestartStatus: this.props.updateRestartStatus}; + await RestartHandler.restartWazuh(updateRedux); this.setState({ isRestarting: false }); - this.showToast( - 'success', - 'Restarting cluster, it will take up to 30 seconds.', - 3000 - ); } catch (error) { this.setState({ isRestarting: false }); const options = { - context: `${WzStatusActionButtons.name}.restartCluster`, + context: `${WzStatusActionButtons.name}.restartWazuh`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, error: { error: error, message: error.message || error, - title: `${error.name}: Error restarting cluster`, - }, - }; - getErrorOrchestrator().handleError(options); - } - } - - /** - * Restart manager - */ - async restartManager() { - this.setState({ isRestarting: true }); - try { - await this.statusHandler.restartManager(); - this.setState({ isRestarting: false }); - this.showToast('success', 'Restarting manager.', 3000); - } catch (error) { - this.setState({ isRestarting: false }); - const options = { - context: `${WzStatusActionButtons.name}.restartManager`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: `${error.name}: Error restarting manager`, + title: `${error.name}: Error restarting Wazuh}`, }, }; getErrorOrchestrator().handleError(options); @@ -214,9 +186,14 @@ class WzStatusActionButtons extends Component { listNodes, selectedNode, clusterEnabled, - isRestarting } = this.props.state; + const { + isRestarting, + isModalVisible, + timeoutRestarting, + } = this.state + let options = this.transforToOptions(listNodes); // Select node @@ -226,7 +203,7 @@ class WzStatusActionButtons extends Component { options={options} value={selectedNode} onChange={this.changeNode} - disabled={isLoading || this.state.isRestarting} + disabled={isLoading || isRestarting} aria-label="Select node" /> ); @@ -239,16 +216,15 @@ class WzStatusActionButtons extends Component { iconType="refresh" onClick={async () => this.setState({ isModalVisible: true })} isDisabled={isLoading} - isLoading={this.state.isRestarting} + isLoading={isRestarting} > - {clusterEnabled && 'Restart cluster'} - {!clusterEnabled && 'Restart manager'} + Restart {clusterEnabled ? 'cluster' : 'manager'} ); let modal; - if (this.state.isModalVisible) { + if (isModalVisible) { modal = ( { - if (clusterEnabled) { - this.restartCluster(); - } else { - this.restartManager(); - } + this.props.updateRestartStatus(RestartHandler.RESTART_STATES.RESTARTING) + this.restartWazuh() this.setState({ isModalVisible: false }); }} cancelButtonText="Cancel" @@ -274,13 +247,20 @@ class WzStatusActionButtons extends Component { ); } + let restarting + + if (timeoutRestarting && this.props.restartStatus !== RestartHandler.RESTART_STATES.RESTARTED) { + restarting = ; + } + return ( {selectedNode !== null && ( - {restartButton} + {restartButton} )} {selectedNode && {selectNode}} {modal} + {restarting} ); } @@ -288,7 +268,8 @@ class WzStatusActionButtons extends Component { const mapStateToProps = state => { return { - state: state.statusReducers + state: state.statusReducers, + restartStatus: state.appStateReducers.restartStatus, }; }; @@ -299,7 +280,9 @@ const mapDispatchToProps = dispatch => { updateNodeInfo: nodeInfo => dispatch(updateNodeInfo(nodeInfo)), updateSelectedNode: selectedNode => dispatch(updateSelectedNode(selectedNode)), updateStats: stats => dispatch(updateStats(stats)), - updateAgentInfo: agentInfo => dispatch(updateAgentInfo(agentInfo)) + updateAgentInfo: agentInfo => dispatch(updateAgentInfo(agentInfo)), + updateRestartAttempt: restartAttempt => dispatch(updateRestartAttempt(restartAttempt)), + updateRestartStatus: restartStatus => dispatch(updateRestartStatus(restartStatus)), }; }; diff --git a/public/controllers/management/components/management/status/status-overview.js b/public/controllers/management/components/management/status/status-overview.js index a2f20feb3d..d764785239 100644 --- a/public/controllers/management/components/management/status/status-overview.js +++ b/public/controllers/management/components/management/status/status-overview.js @@ -20,7 +20,6 @@ import { EuiPage, EuiSpacer, EuiFlexGrid, - EuiButton, } from '@elastic/eui'; import { connect } from 'react-redux'; diff --git a/public/controllers/management/components/management/status/utils/status-handler.js b/public/controllers/management/components/management/status/utils/status-handler.js index 3b9344690f..ededecaad6 100644 --- a/public/controllers/management/components/management/status/utils/status-handler.js +++ b/public/controllers/management/components/management/status/utils/status-handler.js @@ -138,53 +138,4 @@ export default class StatusHandler { throw error; } } - - /** - * Restart cluster - */ - static async restartCluster() { - try { - const validationError = await WzRequest.apiReq( - 'GET', - `/cluster/configuration/validation`, - {} - ); - - const data = ((validationError || {}).data || {}).data || {}; - const isOk = data.status === 'OK'; - if (!isOk && Array.isArray(data.details)) { - const str = data.details.join(); - throw new Error(str); - } - await WzRequest.apiReq('PUT', `/cluster/restart`, { delay: 15000 }); - return { data: { data: 'Restarting cluster' } }; - } catch (error) { - throw error; - } - } - - /** - * Restart manager (single-node API call) - */ - static async restartManager() { - try { - const validationError = await WzRequest.apiReq( - 'GET', - `/manager/configuration/validation`, - {} - ); - - const data = ((validationError || {}).data || {}).data || {}; - const isOk = data.status === 'OK'; - if (!isOk && Array.isArray(data.details)) { - const str = data.details.join(); - throw new Error(str); - } - - const result = await WzRequest.apiReq('PUT', `/manager/restart`, {}); - return result; - } catch (error) { - throw error; - } - } } diff --git a/public/controllers/management/management.js b/public/controllers/management/management.js index 3629b75f10..42432383d7 100644 --- a/public/controllers/management/management.js +++ b/public/controllers/management/management.js @@ -30,13 +30,12 @@ export class ManagementController { * @param {*} $scope * @param {*} $location */ - constructor($scope, $rootScope, $location, configHandler, errorHandler, $interval) { + constructor($scope, $rootScope, $location, errorHandler, $interval) { this.$scope = $scope; this.$rootScope = $rootScope; this.$location = $location; this.shareAgent = new ShareAgent(); this.wazuhConfig = new WazuhConfig(); - this.configHandler = configHandler; this.errorHandler = errorHandler; this.$interval = $interval; this.tab = 'welcome'; @@ -141,11 +140,6 @@ export class ManagementController { this.$scope.$applyAsync(); }); - this.$rootScope.$on('performRestart', (ev) => { - ev.stopPropagation(); - this.clusterInfo.status === 'enabled' ? this.restartCluster() : this.restartManager(); - }); - this.$rootScope.timeoutIsReady; this.$rootScope.$watch('resultState', () => { if (this.$rootScope.timeoutIsReady) { @@ -248,59 +242,6 @@ export class ManagementController { return item && Array.isArray(array) && array.includes(item); } - async restartManager() { - try { - if (this.isRestarting) return; - this.isRestarting = true; - await this.configHandler.restartManager(); - this.isRestarting = false; - this.$scope.$applyAsync(); - ErrorHandler.info('Restarting manager.'); - } catch (error) { - this.isRestarting = false; - this.$scope.$applyAsync(); - - const errorOptions = { - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - context: `${ManagementController.name}.restartManager`, - error: { - error: error, - message: error?.message || '', - title: 'Error restarting manager', - }, - }; - - getErrorOrchestrator().handleError(errorOptions); - } - } - - async restartCluster() { - try { - if (this.isRestarting) return; - this.isRestarting = true; - await this.configHandler.restartCluster(); - this.isRestarting = false; - this.$scope.$applyAsync(); - ErrorHandler.info('Restarting cluster, it will take up to 30 seconds.'); - } catch (error) { - this.isRestarting = false; - this.$scope.$applyAsync(); - const errorOptions = { - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - context: `${ManagementController.name}.restartCluster`, - error: { - error: error, - message: error?.message || '', - title: 'Error restarting cluster', - }, - }; - - getErrorOrchestrator().handleError(errorOptions); - } - } - setConfigTab(tab, nav = false) { this.globalConfigTab = tab; if (nav) { diff --git a/public/react-services/check-daemons-status.js b/public/react-services/check-daemons-status.js index b2a3bc7b02..6d28c7d306 100644 --- a/public/react-services/check-daemons-status.js +++ b/public/react-services/check-daemons-status.js @@ -16,6 +16,9 @@ import { delayAsPromise } from '../../common/utils'; let busy = false; +/** + * @deprecated + */ export class CheckDaemonsStatus { static async makePing(tries = 10) { try { diff --git a/public/react-services/index.ts b/public/react-services/index.ts index 9d9c197cf0..c80c54f796 100644 --- a/public/react-services/index.ts +++ b/public/react-services/index.ts @@ -21,4 +21,5 @@ export * from './wz-csv'; export * from './wz-request'; export * from './wz-security-opensearch-dashboards-security'; export * from './wz-user-permissions'; -export * from './query-config' \ No newline at end of file +export * from './query-config' +export * from './wz-restart' \ No newline at end of file diff --git a/public/react-services/wz-restart.js b/public/react-services/wz-restart.js new file mode 100644 index 0000000000..74ba9775b7 --- /dev/null +++ b/public/react-services/wz-restart.js @@ -0,0 +1,305 @@ +import { WzRequest } from './wz-request'; +import { delayAsPromise } from '../../common/utils'; +import { getHttp } from '../kibana-services'; + +/** + * Wazuh restart wizard. + * + * Controls the Wazuh restart process. + */ +export class RestartHandler { + static MAX_RESTART_POLLING_ATTEMPTS = 30; + static MAX_SYNC_POLLING_ATTEMPTS = 10; + static POLLING_DELAY = 2000; // milliseconds + static SYNC_DELAY = 20000; // milliseconds + static HEALTHCHECK_DELAY = 10000; // milliseconds + static INFO_RESTART_SUCCESS_DELAY = 500; // seconds + static RESTART_STATES = { + // TODO change to enum (requires TS) + RESTART_ERROR: 'restart_error', + SYNC_ERROR: 'sync_error', + RESTARTING: 'restarting', + SYNCING: 'syncing', + RESTARTED: 'restarted', + RESTARTED_INFO: 'restarted_info', + }; + + /** + * Get Cluster status from Wazuh API + * @returns {Promise} + */ + static async clusterReq() { + try { + return WzRequest.apiReq('GET', '/cluster/status', {}); + } catch (error) { + return Promise.reject(error); + } + } + + /** + * Check daemons status + * @param {Object} updateRedux - Redux update function + * @param {number} attempt + * @param {Boolean} isCluster - Is cluster or not + * @returns {object|Promise} + */ + static async checkDaemons(updateRedux, attempt, isCluster) { + try { + updateRedux.updateRestartAttempt(attempt); + + const response = await WzRequest.apiReq( + 'GET', + '/manager/status', + {}, + { checkCurrentApiIsUp: false } + ); + + const daemons = ((((response || {}).data || {}).data || {}).affected_items || [])[0] || {}; + + const wazuhdbExists = typeof daemons['wazuh-db'] !== 'undefined'; + const execd = daemons['wazuh-execd'] === 'running'; + const modulesd = daemons['wazuh-modulesd'] === 'running'; + const wazuhdb = wazuhdbExists ? daemons['wazuh-db'] === 'running' : true; + + let clusterd = true; + if (isCluster) { + clusterd = daemons['wazuh-clusterd'] === 'running'; + } + + const isValid = execd && modulesd && wazuhdb && (isCluster ? clusterd : true); + + if (isValid) { + updateRedux.updateRestartAttempt(0); + return isValid; + } else { + updateRedux.updateRestartAttempt(0); + console.warn('Wazuh not ready yet'); + return false; + } + } catch (error) { + throw error; + } + } + + /** + * Check sync status + * @param {Object} updateRedux - Redux update function + * @param {number} attempt + * @returns {object|Promise} + */ + static async checkSync(updateRedux, attempt) { + try { + const { updateSyncCheckAttempt, updateUnsynchronizedNodes } = updateRedux; + updateSyncCheckAttempt(attempt); + const response = await WzRequest.apiReq('GET', '/cluster/ruleset/synchronization', {}); + + if (response.data.error != 0){ + throw response.data.message + } + + const nodes = response.data.data.affected_items; + + const isSynced = nodes.every((node) => node.synced); + + if (!isSynced) { + const unsyncedNodes = nodes.flatMap((node) => (node.synced ? [] : node.name)); + updateUnsynchronizedNodes(unsyncedNodes); + throw new Error(`Nodes ${unsyncedNodes.join(', ')} are not synced`); + } + + updateSyncCheckAttempt(RestartHandler.MAX_SYNC_POLLING_ATTEMPTS); + + return isSynced; + } catch (error) { + throw error; + } + } + + /** + * Make ping to Wazuh API + * @param updateRedux + * @param breakCondition + * @param {boolean} isCluster + * @return {Promise} + */ + static async makePingSync(updateRedux, breakCondition, isCluster = true) { + try { + let isValid = false; + + const maxAttempts = + breakCondition === this.checkDaemons + ? this.MAX_RESTART_POLLING_ATTEMPTS + : this.MAX_SYNC_POLLING_ATTEMPTS; + + for (let attempt = 1; attempt <= maxAttempts && !isValid; attempt++) { + await delayAsPromise(this.POLLING_DELAY); + try { + isValid = await breakCondition(updateRedux, attempt, isCluster); + } catch (error) { + // console.error(error); + //The message in the console is disabled because the user will see the error message on the healthcheck page. + } + } + + if (!isValid) { + return isValid; + } + return Promise.resolve( + `Wazuh is ${breakCondition === this.checkDaemons ? 'restarted' : 'synced'}` + ); + } catch (error) { + return false; + } + } + + /** + * Healthcheck redirect + */ + static goToHealthcheck() { + window.location.href = getHttp().basePath.prepend('/app/wazuh#/health-check'); + } + + /** + * Restart manager (single-node API call) + * @param isCluster - Is cluster or not + * @returns {object|Promise} + */ + static async restart(isCluster) { + try { + const clusterOrManager = isCluster ? 'cluster' : 'manager'; + + const validationError = await WzRequest.apiReq( + 'GET', + `/${clusterOrManager}/configuration/validation`, + {} + ); + + const isOk = validationError.status === 'OK'; + if (!isOk && validationError.detail) { + const str = validationError.detail; + throw new Error(str); + } + + await WzRequest.apiReq('PUT', `/${clusterOrManager}/restart`, {}); + + return { + data: { + data: `Restarting ${clusterOrManager}`, + }, + }; + } catch (error) { + throw error; + } + } + + /** + * Restart a cluster node + * @param node - Node name + * @returns {object|Promise} + */ + static async restartNode(node) { + try { + const node_param = node && typeof node == 'string' ? `?nodes_list=${node}` : ''; + + const validationError = await WzRequest.apiReq( + 'GET', + `/cluster/configuration/validation`, + {} + ); + + const isOk = validationError.status === 200; + if (!isOk && validationError.detail) { + const str = validationError.detail; + throw new Error(str); + } + const result = await WzRequest.apiReq('PUT', `/cluster/restart${node_param}`, {}); + return result; + } catch (error) { + throw error; + } + } + + /** + * Restart a node or manager + * @param {} selectedNode Cluster Node + * @param updateRedux Redux update function + */ + static async restartSelectedNode(selectedNode, updateRedux) { + try { + // Dispatch a Redux action + updateRedux.updateRestartStatus(this.RESTART_STATES.RESTARTING); + const clusterStatus = (((await this.clusterReq()) || {}).data || {}).data || {}; + const isCluster = clusterStatus.enabled === 'yes' && clusterStatus.running === 'yes'; + isCluster ? await this.restartNode(selectedNode) : await this.restart(isCluster); + + const isRestarted = await this.makePingSync(updateRedux, this.checkDaemons, isCluster); + if (!isRestarted) { + updateRedux.updateRestartStatus(this.RESTART_STATES.RESTART_ERROR); + this.restartValues(updateRedux); + throw new Error('Not restarted'); + } + updateRedux.updateRestartStatus(this.RESTART_STATES.RESTARTED_INFO); + return { restarted: isCluster ? 'Cluster' : 'Manager' }; + } catch (error) { + RestartHandler.clearState(updateRedux, this.RESTART_STATES.RESTART_ERROR); + throw error; + } + } + + /** + * Restart cluster or Manager + * @param updateRedux Redux update function + * @param useDelay need to delay synchronization? + */ + static async restartWazuh(updateRedux, useDelay = false) { + try { + if (useDelay) { + updateRedux.updateRestartStatus(this.RESTART_STATES.SYNCING); + + const isSync = await this.makePingSync(updateRedux, this.checkSync); + if (!isSync) { + updateRedux.updateRestartStatus(this.RESTART_STATES.SYNC_ERROR); + this.restartValues(updateRedux); + throw new Error('Not synced'); + } + } + + updateRedux.updateRestartStatus(this.RESTART_STATES.RESTARTING); + + const clusterStatus = (((await this.clusterReq()) || {}).data || {}).data || {}; + const isCluster = clusterStatus.enabled === 'yes' && clusterStatus.running === 'yes'; + // Dispatch a Redux action + await this.restart(isCluster); + const isRestarted = await this.makePingSync(updateRedux, this.checkDaemons, isCluster); + + if (!isRestarted) { + updateRedux.updateRestartStatus(this.RESTART_STATES.RESTART_ERROR); + this.restartValues(updateRedux); + throw new Error('Not restarted'); + } + + updateRedux.updateRestartStatus(this.RESTART_STATES.RESTARTED_INFO); + this.restartValues(updateRedux); + + return { restarted: isCluster ? 'Cluster' : 'Manager' }; + } catch (error) { + const errorType = error === 'Not synced' ? this.RESTART_STATES.SYNC_ERROR : this.RESTART_STATES.RESTART_ERROR; + RestartHandler.clearState(updateRedux, errorType); + throw error; + } + } + + static clearState(updateRedux, errorType) { + updateRedux.updateRestartAttempt(0); + updateRedux.updateRestartStatus(errorType); + } + + /** + * Resets attempts to 0 in redux + * @param {*} updateRedux + */ + static restartValues(updateRedux) { + updateRedux.updateSyncCheckAttempt && updateRedux.updateSyncCheckAttempt(0); + updateRedux.updateRestartAttempt(0); + } +} diff --git a/public/redux/actions/appStateActions.js b/public/redux/actions/appStateActions.js index e26d7ed3b1..1d3093a494 100644 --- a/public/redux/actions/appStateActions.js +++ b/public/redux/actions/appStateActions.js @@ -199,7 +199,7 @@ export const showFlyoutLogtest = (showFlyout) => { export const updateDockedLogtest = (dockedFlyout) => { return { type: 'UPDATE_DOCKED_LOGTEST', - dockedFlyoutLogtest: dockedFlyout + dockedFlyoutLogtest: dockedFlyout }; }; @@ -235,4 +235,52 @@ export const updateLogtestToken = (logtestToken) => { type: 'UPDATE_LOGTEST_TOKEN', logtestToken: logtestToken }; +}; + +/** + * Update restart attempt + * @param {Number} restartAttempt + * @returns + */ +export const updateRestartAttempt = (restartAttempt) => { + return { + type: 'UPDATE_RESTART_ATTEMPT', + restartAttempt + }; +}; + +/** + * Update sync check attempt + * @param {Number} syncCheckAttempt + * @returns + */ +export const updateSyncCheckAttempt = (syncCheckAttempt) => { + return { + type: 'UPDATE_SYNC_CHECK_ATTEMPT', + syncCheckAttempt + }; +} + +/** + * Update unsynchronized nodes + * @param {Array} unsynchronizedNodes + * @returns + */ +export const updateUnsynchronizedNodes = (unsynchronizedNodes) => { + return { + type: 'UPDATE_UNSYNCHRONIZED_NODES', + unsynchronizedNodes + }; +}; + +/** + * Update status of the restarting process + * @param {string} restartStatus + * @returns + */ +export const updateRestartStatus = (restartStatus) => { + return { + type: 'UPDATE_RESTART_STATUS', + restartStatus + }; }; \ No newline at end of file diff --git a/public/redux/reducers/appStateReducers.js b/public/redux/reducers/appStateReducers.js index c69e603300..f2135b6ec4 100644 --- a/public/redux/reducers/appStateReducers.js +++ b/public/redux/reducers/appStateReducers.js @@ -31,6 +31,10 @@ const initialState = { withUserLogged: false, allowedAgents: [], logtestToken: '', + restartAttempt: 0, + syncCheckAttempt: 0, + unsynchronizedNodes: [], + restartStatus: '' }; const appStateReducers = (state = initialState, action) => { @@ -139,7 +143,7 @@ const appStateReducers = (state = initialState, action) => { ...state, withUserLogged: action.withUserLogged, }; - } + } if (action.type === 'GET_ALLOWED_AGENTS') { return { @@ -147,7 +151,7 @@ const appStateReducers = (state = initialState, action) => { allowedAgents: action.allowedAgents }; } - + if (action.type === 'UPDATE_LOGTEST_TOKEN') { return { ...state, @@ -155,6 +159,34 @@ const appStateReducers = (state = initialState, action) => { }; } + if (action.type === 'UPDATE_RESTART_ATTEMPT') { + return { + ...state, + restartAttempt: action.restartAttempt + }; + } + + if (action.type === 'UPDATE_SYNC_CHECK_ATTEMPT') { + return { + ...state, + syncCheckAttempt: action.syncCheckAttempt + }; + } + + if (action.type === 'UPDATE_UNSYNCHRONIZED_NODES') { + return { + ...state, + unsynchronizedNodes: action.unsynchronizedNodes + }; + } + + if (action.type === 'UPDATE_RESTART_STATUS') { + return { + ...state, + restartStatus: action.restartStatus + }; + } + return state; }; diff --git a/public/services/check-daemon-status.js b/public/services/check-daemon-status.js index 558df37dd0..b8acb3ff8f 100644 --- a/public/services/check-daemon-status.js +++ b/public/services/check-daemon-status.js @@ -2,6 +2,9 @@ import store from '../redux/store'; import { updateWazuhNotReadyYet } from '../redux/actions/appStateActions'; import { WzRequest } from '../react-services/wz-request'; +/** + * @deprecated + */ export class CheckDaemonsStatus { constructor($rootScope, $timeout) { this.$rootScope = $rootScope; diff --git a/public/services/config-handler.js b/public/services/config-handler.js deleted file mode 100644 index e9547e35fe..0000000000 --- a/public/services/config-handler.js +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Wazuh app - Group handler service - * Copyright (C) 2015-2022 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ -import { WzRequest } from '../react-services/wz-request'; -import { ErrorHandler } from '../react-services/error-handler'; - -export class ConfigHandler { - constructor($rootScope, errorHandler) { - this.$rootScope = $rootScope; - this.errorHandler = errorHandler; - } - - /** - * Send ossec.conf content for manager (single-node API call) - * @param {*} content XML raw content for ossec.conf file - */ - async saveManagerConfiguration(content) { - try { - const result = await WzRequest.apiReq( - 'PUT', - `/manager/configuration`, - { content, origin: 'xmleditor' } - ); - return result; - } catch (error) { - return Promise.reject(error); - } - } - - /** - * Send ossec.conf content for a cluster node - * @param {*} node Node name - * @param {*} content XML raw content for ossec.conf file - */ - async saveNodeConfiguration(node, content) { - try { - const result = await WzRequest.apiReq( - 'PUT', - `/cluster/${node}/configuration`, - { content, origin: 'xmleditor' } - ); - return result; - } catch (error) { - return Promise.reject(error); - } - } - - async performClusterRestart() { - try { - await WzRequest.apiReq('PUT', `/cluster/restart`, { delay: 15000 }); - this.$rootScope.$broadcast('removeRestarting', {}); - } catch (error) { - this.$rootScope.$broadcast('removeRestarting', {}); - throw new Error('Error restarting cluster'); - } - } - - /** - * Restart manager (single-node API call) - */ - async restartManager() { - try { - const validationError = await WzRequest.apiReq( - 'GET', - `/manager/configuration/validation`, - {} - ); - - const data = ((validationError || {}).data || {}).data || {}; - const isOk = data.status === 'OK'; - if (!isOk && Array.isArray(data.details)) { - const str = data.details.join(); - throw new Error(str); - } - - const result = await WzRequest.apiReq('PUT', `/manager/restart`, {}); - return result; - } catch (error) { - return Promise.reject(error); - } - } - - /** - * Restart cluster - */ - async restartCluster() { - try { - const validationError = await WzRequest.apiReq( - 'GET', - `/cluster/configuration/validation`, - {} - ); - - const data = ((validationError || {}).data || {}).data || {}; - const isOk = data.status === 'OK'; - if (!isOk && Array.isArray(data.details)) { - const str = data.details.join(); - throw new Error(str); - } - this.performClusterRestart(); - return { data: { data: 'Restarting cluster' } }; - } catch (error) { - return Promise.reject(error); - } - } - - /** - * Restart a cluster node - */ - async restartNode(node) { - try { - const validationError = await WzRequest.apiReq( - 'GET', - `/cluster/${node}/configuration/validation`, - {} - ); - - const data = ((validationError || {}).data || {}).data || {}; - const isOk = data.status === 'OK'; - if (!isOk && Array.isArray(data.details)) { - const str = data.details.join(); - throw new Error(str); - } - const result = await WzRequest.apiReq( - 'PUT', - `/cluster/${node}/restart`, - {} - ); - return result; - } catch (error) { - return Promise.reject(error); - } - } -} diff --git a/public/services/index.js b/public/services/index.js index a82f4124c9..f74440a36d 100644 --- a/public/services/index.js +++ b/public/services/index.js @@ -18,7 +18,6 @@ import { ReportingService } from './reporting'; import { VisFactoryService } from './vis-factory-handler'; import './region-maps'; import './order-object-by'; -import { ConfigHandler } from './config-handler'; import { CheckDaemonsStatus } from './check-daemon-status'; import { getAngularModule } from '../kibana-services'; @@ -30,5 +29,4 @@ app .service('commonData', CommonData) .service('reportingService', ReportingService) .service('visFactoryService', VisFactoryService) - .service('configHandler', ConfigHandler) .service('checkDaemonsStatus', CheckDaemonsStatus); diff --git a/public/styles/common.scss b/public/styles/common.scss index 12c46a1b1e..f7effee792 100644 --- a/public/styles/common.scss +++ b/public/styles/common.scss @@ -1147,6 +1147,41 @@ wz-xml-file-editor { margin: 0px; } +.wz-modal-restart { + border: 1px solid #D3DAE6; + box-shadow: 0 40px 64px 0 rgba(65, 78, 101, 0.1), 0 24px 32px 0 rgba(65, 78, 101, 0.1), 0 16px 16px 0 rgba(65, 78, 101, 0.1), 0 8px 8px 0 rgba(65, 78, 101, 0.1), 0 4px 4px 0 rgba(65, 78, 101, 0.1), 0 2px 2px 0 rgba(65, 78, 101, 0.1); + border-color: #c6cad1; + border-top-color: #e3e4e8; + border-bottom-color: #aaafba; + background-color: #FFF; + border-radius: 4px; + min-width: 500px; +} + +.wz-modal-restart-error { + border: 1px solid #D3DAE6; + box-shadow: 0 40px 64px 0 rgba(65, 78, 101, 0.1), 0 24px 32px 0 rgba(65, 78, 101, 0.1), 0 16px 16px 0 rgba(65, 78, 101, 0.1), 0 8px 8px 0 rgba(65, 78, 101, 0.1), 0 4px 4px 0 rgba(65, 78, 101, 0.1), 0 2px 2px 0 rgba(65, 78, 101, 0.1); + border-color: #c6cad1; + border-top-color: #e3e4e8; + border-bottom-color: #aaafba; + background-color: rgb(248, 233, 233); + border-radius: 4px; + min-width: 400px; +} + +.wz-modal-restart-title { + margin-top: 20px; + margin-bottom: 15px; + font-size: 1.6rem; + line-height: 2.2rem; + font-weight: 700; + color: #1a1c21; +} + +.wz-modal-restart-logo { + width: 50px; +} + .header-global-wrapper + .app-wrapper:not(.hidden-chrome) { top: 48px!important; left: 48px!important;