From 6d8758f2124b69374c256b42ab60187951a0966b Mon Sep 17 00:00:00 2001 From: Aparna Karve Date: Tue, 24 Jul 2018 12:35:08 -0700 Subject: [PATCH] Merge pull request #442 from michaelkro/playbooks-plan-details [#400] Plan details playbooks support (cherry picked from commit 18e320d73da6da62b5ccf316d1ca0bb666c02790) https://bugzilla.redhat.com/show_bug.cgi?id=1608420 --- app/javascript/components/index.js | 3 +- app/javascript/react/screens/App/Plan/Plan.js | 24 ++++- .../react/screens/App/Plan/PlanActions.js | 69 +++++++++++++- .../react/screens/App/Plan/PlanConstants.js | 4 + .../react/screens/App/Plan/PlanReducer.js | 54 ++++++++++- .../PlanRequestDetailList.js | 95 +++++++++++++++---- 6 files changed, 226 insertions(+), 23 deletions(-) diff --git a/app/javascript/components/index.js b/app/javascript/components/index.js index 5579757ebe..9c0f4d4e84 100644 --- a/app/javascript/components/index.js +++ b/app/javascript/components/index.js @@ -133,7 +133,8 @@ export const coreComponents = [ type: PlanContainer, data: { fetchPlanUrl: '/api/service_templates', - fetchTasksForAllRequestsForPlanUrl: '/api/requests?expand=resource&attributes=miq_request_tasks' + fetchTasksForAllRequestsForPlanUrl: '/api/requests?expand=resource&attributes=miq_request_tasks', + fetchOrchestrationStackUrl: '/api/orchestration_stacks' }, store: true }, diff --git a/app/javascript/react/screens/App/Plan/Plan.js b/app/javascript/react/screens/App/Plan/Plan.js index 4a1a53ceaf..9c7e7d9db3 100644 --- a/app/javascript/react/screens/App/Plan/Plan.js +++ b/app/javascript/react/screens/App/Plan/Plan.js @@ -94,6 +94,7 @@ class Plan extends React.Component { render() { const { + plan, planName, planArchived, planRequestFailed, @@ -104,7 +105,13 @@ class Plan extends React.Component { isQueryingVms, isRejectedVms, downloadLogAction, - downloadLogInProgressTaskIds + downloadLogInProgressTaskIds, + fetchAnsiblePlaybookTemplateAction, + fetchPlanUrl, + ansiblePlaybookTemplate, + fetchOrchestrationStackUrl, + fetchOrchestrationStackAction, + orchestrationStack } = this.props; const { @@ -150,10 +157,17 @@ class Plan extends React.Component { !isRejectedPlanRequest && planRequestTasksMutable.length > 0 && ( )} {!planNotStarted && @@ -212,7 +226,13 @@ Plan.propTypes = { isRejectedVms: PropTypes.bool, resetPlanStateAction: PropTypes.func, downloadLogAction: PropTypes.func, - downloadLogInProgressTaskIds: PropTypes.array + downloadLogInProgressTaskIds: PropTypes.array, + plan: PropTypes.object, + fetchAnsiblePlaybookTemplateAction: PropTypes.func, + ansiblePlaybookTemplate: PropTypes.object, + fetchOrchestrationStackUrl: PropTypes.string, + fetchOrchestrationStackAction: PropTypes.func, + orchestrationStack: PropTypes.object }; Plan.defaultProps = { planName: '', diff --git a/app/javascript/react/screens/App/Plan/PlanActions.js b/app/javascript/react/screens/App/Plan/PlanActions.js index 0fc874dbee..adaadbf396 100644 --- a/app/javascript/react/screens/App/Plan/PlanActions.js +++ b/app/javascript/react/screens/App/Plan/PlanActions.js @@ -10,11 +10,78 @@ import { RESET_PLAN_STATE, FETCH_V2V_MIGRATION_TASK_LOG, DOWNLOAD_LOG_CLICKED, - DOWNLOAD_LOG_COMPLETED + DOWNLOAD_LOG_COMPLETED, + FETCH_V2V_ANSIBLE_PLAYBOOK_TEMPLATE, + FETCH_V2V_ORCHESTRATION_STACK } from './PlanConstants'; import { V2V_NOTIFICATION_ADD } from '../common/NotificationList/NotificationConstants'; +// ***************************************************************************** +// * FETCH_V2FETCH_V2V_ORCHESTRATION_STACK +// ***************************************************************************** +const _getOrchestrationStackActionCreator = (url, playbookScheduleType, task) => dispatch => { + dispatch({ + type: DOWNLOAD_LOG_CLICKED, + payload: task.id + }); + + return dispatch({ + type: FETCH_V2V_ORCHESTRATION_STACK, + payload: new Promise((resolve, reject) => + API.get(url) + .then(response => { + resolve(response); + dispatch({ + type: DOWNLOAD_LOG_COMPLETED, + payload: task.id + }); + const playbookLogFileName = `${task.vmName}-${playbookScheduleType}.log`; + const file = new File([response.data.stdout], playbookLogFileName, { type: 'text/plain;charset=utf-8' }); + saveAs(file); + const successMsg = sprintf(__('"%s" download successful'), playbookLogFileName); + dispatch({ + type: V2V_NOTIFICATION_ADD, + message: successMsg, + notificationType: 'success', + persistent: true, + actionEnabled: false + }); + }) + .catch(e => { + dispatch({ + type: DOWNLOAD_LOG_COMPLETED, + payload: task.id + }); + reject(e); + }) + ) + }); +}; + +export const fetchOrchestrationStackAction = (url, playbookScheduleType, task) => { + const [schedule] = playbookScheduleType.match(/pre|post/); + const uri = new URI(`${url}/${task.options.playbooks[schedule].job_id}`); + + uri.addSearch({ attributes: 'stdout' }); + + return _getOrchestrationStackActionCreator(uri.toString(), playbookScheduleType, task); +}; + +// ***************************************************************************** +// * FETCH_V2V_ANSIBLE_PLAYBOOK_TEMPLATE +// ***************************************************************************** +const _getAnsiblePlaybookTemplateActionCreator = url => dispatch => + dispatch({ + type: FETCH_V2V_ANSIBLE_PLAYBOOK_TEMPLATE, + payload: API.get(url) + }); + +export const fetchAnsiblePlaybookTemplateAction = (url, id) => { + const uri = new URI(`${url}/${id}`); + return _getAnsiblePlaybookTemplateActionCreator(uri.toString()); +}; + // ***************************************************************************** // * FETCH_V2V_ALL_REQUESTS_WITH_TASKS_FOR_PLAN // ***************************************************************************** diff --git a/app/javascript/react/screens/App/Plan/PlanConstants.js b/app/javascript/react/screens/App/Plan/PlanConstants.js index a1b6460400..ed66ff49aa 100644 --- a/app/javascript/react/screens/App/Plan/PlanConstants.js +++ b/app/javascript/react/screens/App/Plan/PlanConstants.js @@ -6,6 +6,8 @@ export const RESET_PLAN_STATE = 'RESET_PLAN_STATE'; export const FETCH_V2V_MIGRATION_TASK_LOG = 'FETCH_V2V_MIGRATION_TASK_LOG'; export const DOWNLOAD_LOG_CLICKED = 'DOWNLOAD_LOG_CLICKED'; export const DOWNLOAD_LOG_COMPLETED = 'DOWNLOAD_LOG_COMPLETED'; +export const FETCH_V2V_ANSIBLE_PLAYBOOK_TEMPLATE = 'FETCH_V2V_ANSIBLE_PLAYBOOK_TEMPLATE'; +export const FETCH_V2V_ORCHESTRATION_STACK = 'FETCH_V2V_ORCHESTRATION_STACK'; export const STATUS_MESSAGE_KEYS = { ACQUIRE_TRANSFORMATION_HOST: 'Acquire Transformation Host', @@ -16,6 +18,7 @@ export const STATUS_MESSAGE_KEYS = { SOURCE_MIGRATED: 'Mark source as migrated', MIGRATING: 'Migrating', MIGRATION_COMPLETE: 'Migration complete', + POST_MIGRATION: 'Post-migration', POWER_OFF: 'Power off', POWER_ON: 'Power-on VM', PRE_MIGRATION: 'Pre-migration', @@ -38,6 +41,7 @@ STATUS_MESSAGES[STATUS_MESSAGE_KEYS.CONVERT_DISKS] = __('Convert disks'); STATUS_MESSAGES[STATUS_MESSAGE_KEYS.SOURCE_MIGRATED] = __('Mark source as migrated'); STATUS_MESSAGES[STATUS_MESSAGE_KEYS.MIGRATING] = __('Migrating'); STATUS_MESSAGES[STATUS_MESSAGE_KEYS.MIGRATION_COMPLETE] = __('Migration complete'); +STATUS_MESSAGES[STATUS_MESSAGE_KEYS.POST_MIGRATION] = __('Post-migration'); STATUS_MESSAGES[STATUS_MESSAGE_KEYS.POWER_OFF] = __('Power off'); STATUS_MESSAGES[STATUS_MESSAGE_KEYS.POWER_ON] = __('Power-on VM'); STATUS_MESSAGES[STATUS_MESSAGE_KEYS.PRE_MIGRATION] = __('Pre-migration'); diff --git a/app/javascript/react/screens/App/Plan/PlanReducer.js b/app/javascript/react/screens/App/Plan/PlanReducer.js index ae4d4cda23..2d53e9704e 100644 --- a/app/javascript/react/screens/App/Plan/PlanReducer.js +++ b/app/javascript/react/screens/App/Plan/PlanReducer.js @@ -12,8 +12,10 @@ import { FETCH_V2V_MIGRATION_TASK_LOG, DOWNLOAD_LOG_CLICKED, DOWNLOAD_LOG_COMPLETED, + FETCH_V2V_ANSIBLE_PLAYBOOK_TEMPLATE, V2V_MIGRATION_STATUS_MESSAGES, - STATUS_MESSAGE_KEYS + STATUS_MESSAGE_KEYS, + FETCH_V2V_ORCHESTRATION_STACK } from './PlanConstants'; export const initialState = Immutable({ @@ -31,7 +33,15 @@ export const initialState = Immutable({ isQueryingVms: false, isRejectedVms: false, errorVms: null, - vms: [] + vms: [], + isFetchingAnsiblePlaybookTemplate: false, + isRejectedAnsiblePlaybookTemplate: false, + errorAnsiblePlaybookTemplate: null, + ansiblePlaybookTemplate: {}, + isFetchingOrchestrationStack: false, + isRejectedOrchestrationStack: false, + errorOrchestrationStack: null, + orchestrationStack: {} }); const excludeDownloadDoneTaskId = (allDownloadLogInProgressTaskIds, taskId) => @@ -57,6 +67,18 @@ const processVMTasks = vmTasks => { options: {} }; + if (task.options.playbooks) { + taskDetails.options.prePlaybookRunning = + task.options.playbooks.pre && task.options.playbooks.pre.job_state === 'active'; + taskDetails.options.postPlaybookRunning = + task.options.playbooks.post && task.options.playbooks.post.job_state === 'active'; + taskDetails.options.prePlaybookComplete = + task.options.playbooks.pre && task.options.playbooks.pre.job_state === 'finished'; + taskDetails.options.postPlaybookComplete = + task.options.playbooks.post && task.options.playbooks.post.job_state === 'finished'; + taskDetails.options.playbooks = task.options.playbooks; + } + taskDetails.options.progress = task.options.progress; taskDetails.options.virtv2v_wrapper = task.options.virtv2v_wrapper; @@ -192,6 +214,34 @@ export default (state = initialState, action) => { .set('isRejectedMigrationTaskLog', true) .set('errorMigrationTaskLog', action.payload); + case `${FETCH_V2V_ANSIBLE_PLAYBOOK_TEMPLATE}_PENDING`: + return state.set('isFetchingAnsiblePlaybookTemplate', true).set('isRejectedAnsiblePlaybookTemplate', false); + case `${FETCH_V2V_ANSIBLE_PLAYBOOK_TEMPLATE}_FULFILLED`: + return state + .set('ansiblePlaybookTemplate', action.payload.data) + .set('isFetchingAnsiblePlaybookTemplate', false) + .set('isRejectedAnsiblePlaybookTemplate', false) + .set('errorAnsiblePlaybookTemplate', null); + case `${FETCH_V2V_ANSIBLE_PLAYBOOK_TEMPLATE}_REJECTED`: + return state + .set('isFetchingAnsiblePlaybookTemplate', false) + .set('isRejectedAnsiblePlaybookTemplate', true) + .set('errorAnsiblePlaybookTemplate', action.payload); + + case `${FETCH_V2V_ORCHESTRATION_STACK}_PENDING`: + return state.set('isFetchingOrchestrationStack', true).set('isRejectedOrchestrationStack', false); + case `${FETCH_V2V_ORCHESTRATION_STACK}_FULFILLED`: + return state + .set('orchestrationStack', action.payload.data) + .set('isFetchingOrchestrationStack', false) + .set('isRejectedOrchestrationStack', false) + .set('errorOrchestrationStack', null); + case `${FETCH_V2V_ORCHESTRATION_STACK}_REJECTED`: + return state + .set('isFetchingOrchestrationStack', false) + .set('isRejectedOrchestrationStack', true) + .set('errorOrchestrationStack', action.payload); + case DOWNLOAD_LOG_CLICKED: return state.set( 'downloadLogInProgressTaskIds', diff --git a/app/javascript/react/screens/App/Plan/components/PlanRequestDetailList/PlanRequestDetailList.js b/app/javascript/react/screens/App/Plan/components/PlanRequestDetailList/PlanRequestDetailList.js index 743ad6465e..e8062f9ed5 100644 --- a/app/javascript/react/screens/App/Plan/components/PlanRequestDetailList/PlanRequestDetailList.js +++ b/app/javascript/react/screens/App/Plan/components/PlanRequestDetailList/PlanRequestDetailList.js @@ -15,7 +15,9 @@ import { Sort, Tooltip, UtilizationBar, - PAGINATION_VIEW + PAGINATION_VIEW, + DropdownButton, + MenuItem } from 'patternfly-react'; import { formatDateTime } from '../../../../../../components/dates/MomentDate'; import listFilter from '../listFilter'; @@ -233,6 +235,41 @@ class PlanRequestDetailList extends React.Component { ); }; + onSelect = (eventKey, task) => { + const { downloadLogAction, fetchOrchestrationStackUrl, fetchOrchestrationStackAction } = this.props; + if (eventKey === 'migration') { + downloadLogAction(task); + } else { + fetchOrchestrationStackAction(fetchOrchestrationStackUrl, eventKey, task); + } + }; + + overlayTriggerClick = task => { + if (task.options.playbooks) { + const playbookStatuses = task.options.playbooks; + let runningPlaybook = null; + + for (const scheduleType in playbookStatuses) { + if (playbookStatuses[scheduleType].job_state === 'active') { + runningPlaybook = scheduleType; + } + } + + if (runningPlaybook) { + const { + plan: { + options: { config_info } + }, + fetchAnsiblePlaybookTemplateUrl, + fetchAnsiblePlaybookTemplateAction + } = this.props; + const configInfoKey = `${runningPlaybook}_service_id`; + + fetchAnsiblePlaybookTemplateAction(fetchAnsiblePlaybookTemplateUrl, config_info[configInfoKey]); + } + } + }; + render() { const { activeFilters, @@ -246,7 +283,7 @@ class PlanRequestDetailList extends React.Component { pageChangeValue } = this.state; - const { downloadLogAction, downloadLogInProgressTaskIds } = this.props; + const { downloadLogInProgressTaskIds, ansiblePlaybookTemplate } = this.props; const paginatedSortedFiltersTasks = this.filterSortPaginatePlanRequestTasks(); @@ -349,11 +386,18 @@ class PlanRequestDetailList extends React.Component { {__('Elapsed Time')}: {formatDateTime(task.startDateTime)} -
- {__('Description')}: - {task.options.progress && - V2V_MIGRATION_STATUS_MESSAGES[task.options.progress.current_description]} -
+ {task.options.prePlaybookRunning || task.options.postPlaybookRunning ? ( +
+ {__('Running playbook service')}: + {ansiblePlaybookTemplate.name} +
+ ) : ( +
+ {__('Description')}: + {task.options.progress && + V2V_MIGRATION_STATUS_MESSAGES[task.options.progress.current_description]} +
+ )}
{__('Conversion Host')}: {task.transformation_host_name} @@ -390,7 +434,13 @@ class PlanRequestDetailList extends React.Component { {task.message}   {/* Todo: revisit FieldLevelHelp props in patternfly-react to support this */} - + this.overlayTriggerClick(task)} + placement="left" + overlay={popoverContent} + > @@ -432,15 +482,20 @@ class PlanRequestDetailList extends React.Component { {__('Download log in progress...')} ) : ( - { - e.preventDefault(); - downloadLogAction(task); - }} + this.onSelect(eventKey, task)} > - {__('Download Log')} - + {(task.options.prePlaybookRunning || task.options.prePlaybookComplete) && ( + {__('Pre-migration log')} + )} + {__('Migration log')} + {(task.options.postPlaybookRunning || task.options.postPlaybookComplete) && ( + {__('Post-migration log')} + )} + ) } stacked @@ -473,7 +528,13 @@ class PlanRequestDetailList extends React.Component { PlanRequestDetailList.propTypes = { planRequestTasks: PropTypes.array, downloadLogAction: PropTypes.func, - downloadLogInProgressTaskIds: PropTypes.array + downloadLogInProgressTaskIds: PropTypes.array, + plan: PropTypes.object, + fetchAnsiblePlaybookTemplateUrl: PropTypes.string, + fetchAnsiblePlaybookTemplateAction: PropTypes.func, + ansiblePlaybookTemplate: PropTypes.object, + fetchOrchestrationStackUrl: PropTypes.string, + fetchOrchestrationStackAction: PropTypes.func }; export default PlanRequestDetailList;