diff --git a/docs/dashboard.rst b/docs/dashboard.rst index 22bc50a2..89a3e138 100644 --- a/docs/dashboard.rst +++ b/docs/dashboard.rst @@ -273,7 +273,7 @@ The header of the Conciliation Page has exactly the same contents as the header Conciliation Page Contents ~~~~~~~~~~~~~~~~~~~~~~~~~~ -On the right side of the page, the list of process conflicts is displayed into a table. +On the left side of the page, the list of process conflicts is displayed into a table. A process conflict is raised when the same program is running in multiple |Supvisors| instances. So the table lists, for each conflict: @@ -289,7 +289,7 @@ So the table lists, for each conflict: * for each process, a list of automatic strategies (refer to :ref:`conciliation`) helping to the solving of this conflict. -The left side of the page contains a simple box that enables the user to perform a global conciliation on all conflicts, +The right side of the page contains a simple box that enables the user to perform a global conciliation on all conflicts, using one of the automatic strategies proposed by |Supvisors|. diff --git a/supvisors/tests/test_viewconciliation.py b/supvisors/tests/test_viewconciliation.py index e4e8cfc7..96fce587 100644 --- a/supvisors/tests/test_viewconciliation.py +++ b/supvisors/tests/test_viewconciliation.py @@ -58,34 +58,78 @@ def test_write_contents(mocker, supvisors, view): def test_write_conciliation_strategies(view): """ Test the ConciliationView.write_conciliation_strategies method. """ # patch context - view.sup_ctx.master_address = {'10.0.0.1'} - view.view_ctx = Mock(**{'format_url.return_value': 'an url'}) + view.view_ctx = Mock(**{'format_url.side_effect': lambda x, y, namespec, action: f'{action} url'}) # build root structure with one single element - global_strategy_a_mid = create_element() - global_strategy_li_elt = create_element({'global_strategy_a_mid': global_strategy_a_mid}) - global_strategy_li_mid = create_element() - global_strategy_li_mid .repeat.return_value = [(global_strategy_li_elt, 'infanticide')] - contents_elt = create_element({'global_strategy_li_mid': global_strategy_li_mid}) + infanticide_strategy_a_mid = create_element() + senicide_strategy_a_mid = create_element() + stop_strategy_a_mid = create_element() + restart_strategy_a_mid = create_element() + running_failure_strategy_a_mid = create_element() + contents_elt = create_element({'infanticide_strategy_a_mid': infanticide_strategy_a_mid, + 'senicide_strategy_a_mid': senicide_strategy_a_mid, + 'stop_strategy_a_mid': stop_strategy_a_mid, + 'restart_strategy_a_mid': restart_strategy_a_mid, + 'running_failure_strategy_a_mid': running_failure_strategy_a_mid}) # test call view.write_conciliation_strategies(contents_elt) - assert global_strategy_a_mid.attributes.call_args_list == [call(href='an url')] - assert global_strategy_a_mid.content.call_args_list == [call('Infanticide')] + assert view.view_ctx.format_url.call_args_list == [call('', CONCILIATION_PAGE, namespec='', action='senicide'), + call('', CONCILIATION_PAGE, namespec='', action='infanticide'), + call('', CONCILIATION_PAGE, namespec='', action='stop'), + call('', CONCILIATION_PAGE, namespec='', action='restart'), + call('', CONCILIATION_PAGE, namespec='', + action='running_failure')] + assert infanticide_strategy_a_mid.attributes.call_args_list == [call(href='infanticide url')] + assert senicide_strategy_a_mid.attributes.call_args_list == [call(href='senicide url')] + assert stop_strategy_a_mid.attributes.call_args_list == [call(href='stop url')] + assert restart_strategy_a_mid.attributes.call_args_list == [call(href='restart url')] + assert running_failure_strategy_a_mid.attributes.call_args_list == [call(href='running_failure url')] + + +def test_get_conciliation_data(mocker, view): + """ Test the get_conciliation_data method. """ + # patch context + process_1 = Mock(namespec='proc_1', running_identifiers={'10.0.0.1', '10.0.0.2'}, + info_map={'10.0.0.1': {'uptime': 12}, '10.0.0.2': {'uptime': 11}}) + process_2 = Mock(namespec='proc_2', running_identifiers={'10.0.0.3', '10.0.0.2'}, + info_map={'10.0.0.3': {'uptime': 10}, '10.0.0.2': {'uptime': 11}}) + mocker.patch.object(view.sup_ctx, 'conflicts', return_value=[process_1, process_2]) + # test call + expected = [{'row_type': ProcessRowTypes.APPLICATION_PROCESS, 'namespec': 'proc_1', 'nb_items': 2}, + {'row_type': ProcessRowTypes.INSTANCE_PROCESS, 'namespec': 'proc_1', + 'identifier': '10.0.0.1', 'uptime': 12}, + {'row_type': ProcessRowTypes.INSTANCE_PROCESS, 'namespec': 'proc_1', + 'identifier': '10.0.0.2', 'uptime': 11}, + {'row_type': ProcessRowTypes.APPLICATION_PROCESS, 'namespec': 'proc_2', 'nb_items': 2}, + {'row_type': ProcessRowTypes.INSTANCE_PROCESS, 'namespec': 'proc_2', + 'identifier': '10.0.0.2', 'uptime': 11}, + {'row_type': ProcessRowTypes.INSTANCE_PROCESS, 'namespec': 'proc_2', + 'identifier': '10.0.0.3', 'uptime': 10}] + actual = view.get_conciliation_data() + print(actual) + # no direct method in pytest to compare 2 lists of dicts + for actual_single in actual: + assert any(actual_single == expected_single for expected_single in expected) def test_write_conciliation_table(mocker, supvisors, view): """ Test the write_conciliation_table method. """ - mocked_name = mocker.patch.object(view, '_write_conflict_name') - mocked_node = mocker.patch.object(view, '_write_conflict_identifier') - mocked_time = mocker.patch.object(view, '_write_conflict_uptime') - mocked_actions = mocker.patch.object(view, '_write_conflict_process_actions') - mocked_strategies = mocker.patch.object(view, '_write_conflict_strategies') + mocked_process = mocker.patch.object(view, '_write_conflict_process') + mocked_detail = mocker.patch.object(view, '_write_conflict_detail') mocked_data = mocker.patch.object(view, 'get_conciliation_data') # patch context view.supvisors.fsm.state = SupvisorsStates.OPERATION mocker.patch.object(view.sup_ctx, 'conflicts', return_value=True) # sample data - data = [{'namespec': 'proc_1', 'rowspan': 2}, {'namespec': 'proc_1', 'rowspan': 0}, - {'namespec': 'proc_2', 'rowspan': 2}, {'namespec': 'proc_2', 'rowspan': 0}] + data = [{'row_type': ProcessRowTypes.APPLICATION_PROCESS, 'namespec': 'proc_1', 'nb_items': 2}, + {'row_type': ProcessRowTypes.INSTANCE_PROCESS, 'namespec': 'proc_1', + 'identifier': '10.0.0.1', 'uptime': 12}, + {'row_type': ProcessRowTypes.INSTANCE_PROCESS, 'namespec': 'proc_1', + 'identifier': '10.0.0.2', 'uptime': 11}, + {'row_type': ProcessRowTypes.APPLICATION_PROCESS, 'namespec': 'proc_2', 'nb_items': 2}, + {'row_type': ProcessRowTypes.INSTANCE_PROCESS, 'namespec': 'proc_2', + 'identifier': '10.0.0.2', 'uptime': 11}, + {'row_type': ProcessRowTypes.INSTANCE_PROCESS, 'namespec': 'proc_2', + 'identifier': '10.0.0.3', 'uptime': 10}] # build simple root structure mocked_tr_elt = [create_element() for _ in data] conflict_tr_mid = create_element() @@ -97,10 +141,8 @@ def test_write_conciliation_table(mocker, supvisors, view): assert not mocked_data.called assert conflicts_div_mid.replace.call_args_list == [call('No conflict')] assert all(tr_elt.attrib['class'] == '' for tr_elt in mocked_tr_elt) - assert not mocked_name.called - assert not mocked_node.called - assert not mocked_actions.called - assert not mocked_strategies.called + assert not mocked_process.called + assert not mocked_detail.called conflicts_div_mid.reset_mock() # test call in CONCILIATION state with conflicts view.supvisors.fsm.state = SupvisorsStates.CONCILIATION @@ -108,75 +150,92 @@ def test_write_conciliation_table(mocker, supvisors, view): assert mocked_data.call_args_list == [call()] # check elements background assert mocked_tr_elt[0].attrib['class'] == 'brightened' - assert mocked_tr_elt[1].attrib['class'] == 'brightened' - assert mocked_tr_elt[2].attrib['class'] == 'shaded' + assert mocked_tr_elt[1].attrib['class'] == 'shaded' + assert mocked_tr_elt[2].attrib['class'] == 'brightened' assert mocked_tr_elt[3].attrib['class'] == 'shaded' - assert mocked_name.call_args_list == [call(mocked_tr_elt[0], data[0], False), - call(mocked_tr_elt[1], data[1], False), - call(mocked_tr_elt[2], data[2], True), call(mocked_tr_elt[3], data[3], True)] - assert mocked_node.call_args_list == [call(mocked_tr_elt[0], data[0]), call(mocked_tr_elt[1], data[1]), - call(mocked_tr_elt[2], data[2]), call(mocked_tr_elt[3], data[3])] - assert mocked_time.call_args_list == [call(mocked_tr_elt[0], data[0]), call(mocked_tr_elt[1], data[1]), - call(mocked_tr_elt[2], data[2]), call(mocked_tr_elt[3], data[3])] - assert mocked_actions.call_args_list == [call(mocked_tr_elt[0], data[0]), call(mocked_tr_elt[1], data[1]), - call(mocked_tr_elt[2], data[2]), call(mocked_tr_elt[3], data[3])] - assert mocked_strategies.call_args_list == [call(mocked_tr_elt[0], data[0], False), - call(mocked_tr_elt[1], data[1], False), - call(mocked_tr_elt[2], data[2], True), - call(mocked_tr_elt[3], data[3], True)] - # test call in conciliation state - view.supvisors.fsm.state = SupvisorsStates.CONCILIATION + assert mocked_tr_elt[4].attrib['class'] == 'brightened' + assert mocked_tr_elt[5].attrib['class'] == 'shaded' + assert mocked_process.call_args_list == [call(mocked_tr_elt[0], + {'row_type': ProcessRowTypes.APPLICATION_PROCESS, + 'namespec': 'proc_1', 'nb_items': 2}, False), + call(mocked_tr_elt[3], + {'row_type': ProcessRowTypes.APPLICATION_PROCESS, + 'namespec': 'proc_2', 'nb_items': 2}, True)] + assert mocked_detail.call_args_list == [call(mocked_tr_elt[1], + {'row_type': ProcessRowTypes.INSTANCE_PROCESS, + 'namespec': 'proc_1', 'identifier': '10.0.0.1', 'uptime': 12}), + call(mocked_tr_elt[2], + {'row_type': ProcessRowTypes.INSTANCE_PROCESS, + 'namespec': 'proc_1', 'identifier': '10.0.0.2', 'uptime': 11}), + call(mocked_tr_elt[4], + {'row_type': ProcessRowTypes.INSTANCE_PROCESS, + 'namespec': 'proc_2', 'identifier': '10.0.0.2', 'uptime': 11}), + call(mocked_tr_elt[5], + {'row_type': ProcessRowTypes.INSTANCE_PROCESS, + 'namespec': 'proc_2', 'identifier': '10.0.0.3', 'uptime': 10})] + + +def test_write_conflict_process(mocker, view): + """ Test the _write_conflict_process method. """ + mocked_node = mocker.patch.object(view, '_write_conflict_identifier') + mocked_time = mocker.patch.object(view, '_write_conflict_uptime') + mocked_actions = mocker.patch.object(view, '_write_conflict_process_actions') + mocked_strategies = mocker.patch.object(view, '_write_conflict_strategies') + # build xhtml structure + section_td_mid = create_element() + process_td_mid = create_element() + conflict_instance_td_mid = create_element() + pstop_td_mid = create_element() + pkeep_td_mid = create_element() + strategy_td_mid = create_element() + tr_elt = create_element({'section_td_mid': section_td_mid, 'process_td_mid': process_td_mid, + 'conflict_instance_td_mid': conflict_instance_td_mid, + 'pstop_td_mid': pstop_td_mid, 'pkeep_td_mid': pkeep_td_mid, + 'strategy_td_mid': strategy_td_mid}) + # test call + info = {'row_type': ProcessRowTypes.APPLICATION_PROCESS, 'namespec': 'proc_1', 'nb_items': 2} + view._write_conflict_process(tr_elt, info, True) + assert section_td_mid.attrib == {'class': 'shaded', 'rowspan': '3'} + assert process_td_mid.content.call_args_list == [call('proc_1')] + assert not mocked_node.called + assert not mocked_time.called + assert not mocked_actions.called + assert mocked_strategies.call_args_list == [call(tr_elt, info, True)] + assert conflict_instance_td_mid.content.call_args_list == [call('')] + assert pstop_td_mid.content.call_args_list == [call('')] + assert pkeep_td_mid.content.call_args_list == [call('')] -def test_write_conflict_name(view): - """ Test the _write_conflict_name method. """ - # build root structure with one single element - mocked_name_mid = Mock(attrib={}) - mocked_root = Mock(**{'findmeld.return_value': mocked_name_mid}) - # test call with different values of rowspan and shade - # first line, shaded - info = {'namespec': 'dummy_proc', 'rowspan': 2} - view._write_conflict_name(mocked_root, info, True) - assert mocked_root.findmeld.call_args_list == [call('name_td_mid')] - assert mocked_name_mid.attrib['rowspan'] == '2' - assert mocked_name_mid.attrib['class'] == 'shaded' - assert mocked_name_mid.content.call_args_list == [call('dummy_proc')] - # reset mocks - mocked_root.findmeld.reset_mock() - mocked_name_mid.content.reset_mock() - mocked_name_mid.attrib = {} - # first line, non shaded - view._write_conflict_name(mocked_root, info, False) - assert mocked_root.findmeld.call_args_list == [call('name_td_mid')] - assert mocked_name_mid.attrib['rowspan'] == '2' - assert mocked_name_mid.attrib['class'] == 'brightened' - assert mocked_name_mid.content.call_args_list == [call('dummy_proc')] - assert not mocked_name_mid.replace.called - # reset mocks - mocked_root.findmeld.reset_mock() - mocked_name_mid.content.reset_mock() - mocked_name_mid.attrib = {} - # not first line, shaded - info['rowspan'] = 0 - view._write_conflict_name(mocked_root, info, True) - assert mocked_root.findmeld.call_args_list == [call('name_td_mid')] - assert 'rowspan' not in mocked_name_mid.attrib - assert 'class' not in mocked_name_mid.attrib - assert not mocked_name_mid.content.called - assert mocked_name_mid.replace.call_args_list == [call('')] - # reset mocks - mocked_root.findmeld.reset_mock() - mocked_name_mid.replace.reset_mock() - # not first line, non shaded - view._write_conflict_name(mocked_root, info, False) - assert mocked_root.findmeld.call_args_list == [call('name_td_mid')] - assert 'rowspan' not in mocked_name_mid.attrib - assert 'class' not in mocked_name_mid.attrib - assert not mocked_name_mid.content.called - assert mocked_name_mid.replace.call_args_list == [call('')] - - -def test_write_conflict_node(view): +def test_write_conflict_detail(mocker, view): + """ Test the _write_conflict_detail method. """ + mocked_node = mocker.patch.object(view, '_write_conflict_identifier') + mocked_time = mocker.patch.object(view, '_write_conflict_uptime') + mocked_actions = mocker.patch.object(view, '_write_conflict_process_actions') + mocked_strategies = mocker.patch.object(view, '_write_conflict_strategies') + # build xhtml structure + section_td_mid = create_element() + process_td_mid = create_element() + conflict_instance_td_mid = create_element() + pstop_td_mid = create_element() + pkeep_td_mid = create_element() + strategy_td_mid = create_element() + tr_elt = create_element({'section_td_mid': section_td_mid, 'process_td_mid': process_td_mid, + 'conflict_instance_td_mid': conflict_instance_td_mid, + 'pstop_td_mid': pstop_td_mid, 'pkeep_td_mid': pkeep_td_mid, + 'strategy_td_mid': strategy_td_mid}) + # test call + info = {'row_type': ProcessRowTypes.INSTANCE_PROCESS, 'namespec': 'proc_1', 'identifier': '10.0.0.2', 'uptime': 11} + view._write_conflict_detail(tr_elt, info) + assert section_td_mid.replace.call_args_list == [call('')] + assert process_td_mid.content.call_args_list == [call(SUB_SYMBOL)] + assert mocked_node.call_args_list == [call(tr_elt, info)] + assert mocked_time.call_args_list == [call(tr_elt, info)] + assert mocked_actions.call_args_list == [call(tr_elt, info)] + assert not mocked_strategies.called + assert strategy_td_mid.replace.call_args_list == [call('')] + + +def test_write_conflict_identifier(view): """ Test the _write_conflict_identifier method. """ # patch context view.view_ctx = Mock(**{'format_url.return_value': 'an url'}) @@ -184,11 +243,11 @@ def test_write_conflict_node(view): mocked_addr_mid = Mock() mocked_root = Mock(**{'findmeld.return_value': mocked_addr_mid}) # test call - view._write_conflict_identifier(mocked_root, {'identifier': '10.0.0.1'}) + view._write_conflict_identifier(mocked_root, {'identifier': '10.0.0.1:25000'}) assert mocked_root.findmeld.call_args_list == [call('conflict_instance_a_mid')] assert mocked_addr_mid.attributes.call_args_list == [call(href='an url')] assert mocked_addr_mid.content.call_args_list == [call('10.0.0.1')] - assert view.view_ctx.format_url.call_args_list == [call('10.0.0.1', PROC_INSTANCE_PAGE)] + assert view.view_ctx.format_url.call_args_list == [call('10.0.0.1:25000', PROC_INSTANCE_PAGE)] def test_write_conflict_uptime(view): @@ -228,81 +287,38 @@ def test_write_conflict_strategies(view): """ Test the _write_conflict_strategies method. """ # patch context view.sup_ctx.master_identifier = '10.0.0.1' - view.view_ctx = Mock(**{'format_url.return_value': 'an url'}) + view.view_ctx = Mock(**{'format_url.side_effect': lambda x, y, namespec, action: f'{action} url'}) # build root structure with one single element - mocked_a_mid = Mock() - mocked_li_mid = Mock(**{'findmeld.return_value': mocked_a_mid}) - mocked_li_template = Mock(**{'repeat.return_value': [(mocked_li_mid, 'senicide')]}) - mocked_td_mid = Mock(attrib={}, **{'findmeld.return_value': mocked_li_template}) - mocked_root = Mock(**{'findmeld.return_value': mocked_td_mid}) - # test call with different values of rowspan and shade - # first line, shaded - info = {'namespec': 'dummy_proc', 'rowspan': 2} - view._write_conflict_strategies(mocked_root, info, True) - assert mocked_td_mid.attrib['rowspan'] == '2' - assert mocked_td_mid.attrib['class'] == 'shaded' - assert mocked_a_mid.attributes.call_args_list == [call(href='an url')] - assert mocked_a_mid.content.call_args_list == [call('Senicide')] - assert view.view_ctx.format_url.call_args_list == [call('10.0.0.1', CONCILIATION_PAGE, - action='senicide', namespec='dummy_proc')] - # reset mocks - view.view_ctx.format_url.reset_mock() - mocked_a_mid.attributes.reset_mock() - mocked_a_mid.content.reset_mock() - mocked_td_mid.attrib = {} - # first line, not shaded - info = {'namespec': 'dummy_proc', 'rowspan': 2} - view._write_conflict_strategies(mocked_root, info, False) - assert mocked_td_mid.attrib['rowspan'] == '2' - assert mocked_td_mid.attrib['class'] == 'brightened' - assert mocked_a_mid.attributes.call_args_list == [call(href='an url')] - assert mocked_a_mid.content.call_args_list == [call('Senicide')] - assert view.view_ctx.format_url.call_args_list == [call('10.0.0.1', CONCILIATION_PAGE, - action='senicide', namespec='dummy_proc')] - # reset mocks - view.view_ctx.format_url.reset_mock() - mocked_a_mid.attributes.reset_mock() - mocked_a_mid.content.reset_mock() - mocked_td_mid.attrib = {} - # not first line, shaded - info = {'namespec': 'dummy_proc', 'rowspan': 0} - view._write_conflict_strategies(mocked_root, info, True) - assert 'rowspan' not in mocked_td_mid.attrib - assert not mocked_a_mid.attributes.called - assert not mocked_a_mid.content.called - assert not mocked_a_mid.content.called - assert not view.view_ctx.format_url.called - assert mocked_td_mid.replace.call_args_list == [call('')] - # reset mocks - mocked_td_mid.replace.reset_mock() - # not first line, not shaded - info = {'namespec': 'dummy_proc', 'rowspan': 0} - view._write_conflict_strategies(mocked_root, info, False) - assert 'rowspan' not in mocked_td_mid.attrib - assert not mocked_a_mid.attributes.called - assert not mocked_a_mid.content.called - assert not mocked_a_mid.content.called - assert not view.view_ctx.format_url.called - assert mocked_td_mid.replace.call_args_list == [call('')] - - -def test_get_conciliation_data(mocker, view): - """ Test the get_conciliation_data method. """ - # patch context - process_1 = Mock(namespec='proc_1', running_identifiers={'10.0.0.1', '10.0.0.2'}, - info_map={'10.0.0.1': {'uptime': 12}, '10.0.0.2': {'uptime': 11}}) - process_2 = Mock(namespec='proc_2', running_identifiers={'10.0.0.3', '10.0.0.2'}, - info_map={'10.0.0.3': {'uptime': 10}, '10.0.0.2': {'uptime': 11}}) - mocker.patch.object(view.sup_ctx, 'conflicts', return_value=[process_1, process_2]) + infanticide_strategy_a_mid = create_element() + senicide_strategy_a_mid = create_element() + stop_strategy_a_mid = create_element() + restart_strategy_a_mid = create_element() + running_failure_strategy_a_mid = create_element() + strategy_td_mid = create_element({'infanticide_local_strategy_a_mid': infanticide_strategy_a_mid, + 'senicide_local_strategy_a_mid': senicide_strategy_a_mid, + 'stop_local_strategy_a_mid': stop_strategy_a_mid, + 'restart_local_strategy_a_mid': restart_strategy_a_mid, + 'running_failure_local_strategy_a_mid': running_failure_strategy_a_mid}) + tr_elt = create_element({'strategy_td_mid': strategy_td_mid}) # test call - expected = [{'namespec': 'proc_1', 'rowspan': 2, 'identifier': '10.0.0.1', 'uptime': 12}, - {'namespec': 'proc_1', 'rowspan': 0, 'identifier': '10.0.0.2', 'uptime': 11}, - {'namespec': 'proc_2', 'rowspan': 2, 'identifier': '10.0.0.2', 'uptime': 11}, - {'namespec': 'proc_2', 'rowspan': 0, 'identifier': '10.0.0.3', 'uptime': 10}] - actual = view.get_conciliation_data() - # no direct method in pytest to compare 2 lists of dicts - for actual_single in actual: - assert any(actual_single == expected_single for expected_single in expected) + info = {'row_type': ProcessRowTypes.APPLICATION_PROCESS, 'namespec': 'proc_1', 'nb_items': 2} + view._write_conflict_strategies(tr_elt, info, False) + assert strategy_td_mid.attrib == {'class': 'brightened', 'rowspan': '3'} + assert view.view_ctx.format_url.call_args_list == [call('10.0.0.1', CONCILIATION_PAGE, namespec='proc_1', + action='senicide'), + call('10.0.0.1', CONCILIATION_PAGE, namespec='proc_1', + action='infanticide'), + call('10.0.0.1', CONCILIATION_PAGE, namespec='proc_1', + action='stop'), + call('10.0.0.1', CONCILIATION_PAGE, namespec='proc_1', + action='restart'), + call('10.0.0.1', CONCILIATION_PAGE, namespec='proc_1', + action='running_failure')] + assert infanticide_strategy_a_mid.attributes.call_args_list == [call(href='infanticide url')] + assert senicide_strategy_a_mid.attributes.call_args_list == [call(href='senicide url')] + assert stop_strategy_a_mid.attributes.call_args_list == [call(href='stop url')] + assert restart_strategy_a_mid.attributes.call_args_list == [call(href='restart url')] + assert running_failure_strategy_a_mid.attributes.call_args_list == [call(href='running_failure url')] def test_stop_action(mocker, supvisors, view): diff --git a/supvisors/ui/application.html b/supvisors/ui/application.html index 7d465b49..038628c2 100644 --- a/supvisors/ui/application.html +++ b/supvisors/ui/application.html @@ -36,7 +36,7 @@

Supervisors

@@ -116,7 +116,7 @@

Statistics Periods
- +
@@ -128,14 +128,14 @@

- - - + + + - - + +
StartStopRestartStartStopRestart
RefreshAuto-refreshRefreshAuto-refresh
@@ -144,12 +144,12 @@

- +
@@ -177,22 +177,22 @@

- - + + - - - - - - - - - + + + + + + + +
- - + + Name State
---- -- + -- -- ------StartStopRestartClearStdoutStderr----StartStopRestartClearStdoutStderr
diff --git a/supvisors/ui/conciliation.html b/supvisors/ui/conciliation.html index 1f0c4272..a3b2dfa5 100644 --- a/supvisors/ui/conciliation.html +++ b/supvisors/ui/conciliation.html @@ -13,6 +13,7 @@ + @@ -35,7 +36,7 @@

Supervisors

  • - + Supervisor @@ -49,7 +50,7 @@

    Applications

    @@ -105,24 +106,24 @@

    - + Restart - + Shutdown - + Refresh - + Auto-refresh @@ -133,21 +134,11 @@

    -
    -
    - -
    Conciliation Strategies
    -
    - -
    -
    -
    -
    - +
    - + @@ -156,29 +147,59 @@

    - + - + - - - - - - + + + + + + +
    NameName Supervisor Uptime Actions
    NameName Supervisor Uptime Actions Strategy
    ----StopKeep
      -
    • - -- -
    • -
    StopKeep +
    + +
    + + +
    + +
    +
    + +
    +
    +
    Global Conciliation Strategies
    +
    + +
    + + +
    + +
    +
    +
    diff --git a/supvisors/ui/css/conciliation.css b/supvisors/ui/css/conciliation.css index d30f78ed..8950a503 100644 --- a/supvisors/ui/css/conciliation.css +++ b/supvisors/ui/css/conciliation.css @@ -25,3 +25,21 @@ overflow: auto; padding: 10px; } + +/* update the background of the global strategies box */ +div.strategies { + background-image: linear-gradient(180deg, var(--light2-color), var(--light1-color)); +} + +div.strategies div.button_group { + background-image: none; +} + +div.strategies div.button_group:not(:first-child) { + padding-top: 0; +} + +/* update the area of the local strategies box */ +div.local_strategies div { + padding: 1px 3px; +} diff --git a/supvisors/ui/css/process_table.css b/supvisors/ui/css/process_table.css index cec67d5d..5afe5834 100644 --- a/supvisors/ui/css/process_table.css +++ b/supvisors/ui/css/process_table.css @@ -28,15 +28,14 @@ } /* process table */ -#process_left_side { +.process_table { font-family: Verdana, Arial, sans-serif; text-align: justify; margin-right: 5px; overflow-y: auto; } -#process_left_side -process_table thead, .process_table thead tr { +.process_table thead, .process_table thead tr { border: solid 1px var(--border-color); } @@ -110,7 +109,6 @@ tbody.hoverable tr:hover, tbody.hoverable tr:hover td:not(.state_cell) { color: var(--selected-color); } - .stats_contents { flex: 0; display: flex; diff --git a/supvisors/web/viewconciliation.py b/supvisors/web/viewconciliation.py index a8bdeea1..5e6dbde8 100644 --- a/supvisors/web/viewconciliation.py +++ b/supvisors/web/viewconciliation.py @@ -37,7 +37,7 @@ def __init__(self, context): """ Call of the superclass constructors. """ MainView.__init__(self, context) self.page_name: str = CONCILIATION_PAGE - # get applicable conciliation strategies + # get applicable conciliation strategies (USER excluded) self.strategies: List[str] = [x.name.lower() for x in ConciliationStrategies] self.strategies.remove(ConciliationStrategies.USER.name.lower()) # process actions @@ -53,24 +53,27 @@ def write_contents(self, contents_elt) -> None: # Conciliation part def write_conciliation_strategies(self, contents_elt): """ Rendering of the global conciliation actions. """ - global_strategy_li_mid = contents_elt.findmeld('global_strategy_li_mid') - for li_elt, item in global_strategy_li_mid.repeat(self.strategies): - elt = li_elt.findmeld('global_strategy_a_mid') + for strategy in self.strategies: + elt = contents_elt.findmeld(f'{strategy}_strategy_a_mid') # conciliation requests MUST be sent to MASTER and namespec MUST be reset master = self.sup_ctx.master_identifier - parameters = {NAMESPEC: '', ACTION: item} + parameters = {NAMESPEC: '', ACTION: strategy} url = self.view_ctx.format_url(master, CONCILIATION_PAGE, **parameters) elt.attributes(href=url) - elt.content(item.title()) def get_conciliation_data(self): """ Get information about all conflicting processes. """ - return [{'namespec': process.namespec, - 'rowspan': len(process.running_identifiers) if idx == 0 else 0, - 'identifier': identifier, - 'uptime': process.info_map[identifier]['uptime']} - for process in self.sup_ctx.conflicts() - for idx, identifier in enumerate(sorted(process.running_identifiers))] + data = [] + for process in self.sup_ctx.conflicts(): + data.append({'row_type': ProcessRowTypes.APPLICATION_PROCESS, + 'namespec': process.namespec, + 'nb_items': len(process.running_identifiers)}) + for identifier in sorted(process.running_identifiers): + data.append({'row_type': ProcessRowTypes.INSTANCE_PROCESS, + 'namespec': process.namespec, + 'identifier': identifier, + 'uptime': process.info_map[identifier]['uptime']}) + return data def write_conciliation_table(self, contents_elt): """ Rendering of the conflicts table. """ @@ -79,45 +82,52 @@ def write_conciliation_table(self, contents_elt): # get data for table data = self.get_conciliation_data() # create a row per conflict - shaded_tr = True - for tr_elt, item in conflicts_div_mid.findmeld('conflict_tr_mid').repeat(data): - # first get the rowspan and change shade when rowspan is 0 (first line of conflict) - rowspan = item['rowspan'] - if rowspan: - shaded_tr = not shaded_tr - # set row background - apply_shade(tr_elt, shaded_tr) - # write information and actions - self._write_conflict_name(tr_elt, item, shaded_tr) - self._write_conflict_identifier(tr_elt, item) - self._write_conflict_uptime(tr_elt, item) - self._write_conflict_process_actions(tr_elt, item) - self._write_conflict_strategies(tr_elt, item, shaded_tr) + shaded_proc_tr, shaded_detail_tr = False, False # used to invert background style + for tr_elt, info in conflicts_div_mid.findmeld('conflict_tr_mid').repeat(data): + if info['row_type'] == ProcessRowTypes.APPLICATION_PROCESS: + self._write_conflict_process(tr_elt, info, shaded_proc_tr) + # set line background and invert + apply_shade(tr_elt, shaded_proc_tr) + shaded_proc_tr = not shaded_proc_tr + shaded_detail_tr = shaded_proc_tr + elif info['row_type'] == ProcessRowTypes.INSTANCE_PROCESS: + self._write_conflict_detail(tr_elt, info) + # set line background and invert + apply_shade(tr_elt, shaded_detail_tr) + shaded_detail_tr = not shaded_detail_tr else: # remove conflicts table conflicts_div_mid.replace('No conflict') - @staticmethod - def _write_conflict_name(tr_elt, info, shaded_tr): - """ In a conflicts table, write the process name in conflict. """ - elt = tr_elt.findmeld('name_td_mid') - rowspan = info['rowspan'] - if rowspan > 0: - namespec = info['namespec'] - elt.attrib['rowspan'] = str(rowspan) - elt.content(namespec) - # apply shade logic to td element too for background-image to work - apply_shade(elt, shaded_tr) - else: - elt.replace('') + def _write_conflict_process(self, tr_elt, info, shaded_tr): + """ In a conflicts table, write the process entry in conflict. """ + section_elt = tr_elt.findmeld('section_td_mid') + section_elt.attrib['rowspan'] = str(info['nb_items'] + 1) + apply_shade(section_elt, shaded_tr) + # write process row + tr_elt.findmeld('process_td_mid').content(info['namespec']) + tr_elt.findmeld('conflict_instance_td_mid').content('') + tr_elt.findmeld('pstop_td_mid').content('') + tr_elt.findmeld('pkeep_td_mid').content('') + self._write_conflict_strategies(tr_elt, info, shaded_tr) + + def _write_conflict_detail(self, tr_elt, info): + """ In a conflicts table, write the process entry in conflict. """ + tr_elt.findmeld('section_td_mid').replace('') + tr_elt.findmeld('process_td_mid').content(SUB_SYMBOL) + self._write_conflict_identifier(tr_elt, info) + self._write_conflict_uptime(tr_elt, info) + self._write_conflict_process_actions(tr_elt, info) + tr_elt.findmeld('strategy_td_mid').replace('') def _write_conflict_identifier(self, tr_elt, info): """ In a conflicts table, write the Supvisors instance identifier where runs the process in conflict. """ identifier = info['identifier'] + nick_identifier = self.supvisors.mapper.get_nick_identifier(identifier) elt = tr_elt.findmeld('conflict_instance_a_mid') url = self.view_ctx.format_url(identifier, PROC_INSTANCE_PAGE) elt.attributes(href=url) - elt.content(identifier) + elt.content(nick_identifier) @staticmethod def _write_conflict_uptime(tr_elt, info): @@ -128,7 +138,7 @@ def _write_conflict_uptime(tr_elt, info): def _write_conflict_process_actions(self, tr_elt, info): """ In a conflicts table, write the actions that can be requested on the process in conflict. """ namespec = info['namespec'] - identifier = info['identifier'] + identifier = info.get('identifier', '') for action in self.process_methods: elt = tr_elt.findmeld(action + '_a_mid') parameters = {NAMESPEC: namespec, IDENTIFIER: identifier, ACTION: action} @@ -139,25 +149,19 @@ def _write_conflict_strategies(self, tr_elt, info, shaded_tr): """ In a conflicts table, write the strategies that can be requested on the process in conflict. """ # extract info namespec = info['namespec'] - rowspan = info['rowspan'] # update element structure td_elt = tr_elt.findmeld('strategy_td_mid') - if rowspan > 0: - # apply shade logic to td element too for background-image to work - apply_shade(td_elt, shaded_tr) - # fill the strategies - td_elt.attrib['rowspan'] = str(rowspan) - strategy_iterator = td_elt.findmeld('local_strategy_li_mid').repeat(self.strategies) - for li_elt, st_item in strategy_iterator: - elt = li_elt.findmeld('local_strategy_a_mid') - # conciliation requests MUST be sent to MASTER - master = self.sup_ctx.master_identifier - parameters = {NAMESPEC: namespec, ACTION: st_item} - url = self.view_ctx.format_url(master, CONCILIATION_PAGE, **parameters) - elt.attributes(href=url) - elt.content(st_item.title()) - else: - td_elt.replace('') + # apply shade logic to td element too for background-image to work + apply_shade(td_elt, shaded_tr) + # fill the strategies + td_elt.attrib['rowspan'] = str(info['nb_items'] + 1) + for strategy in self.strategies: + elt = td_elt.findmeld(f'{strategy}_local_strategy_a_mid') + # conciliation requests MUST be sent to the Supvisors Master + master = self.sup_ctx.master_identifier + parameters = {NAMESPEC: namespec, ACTION: strategy} + url = self.view_ctx.format_url(master, CONCILIATION_PAGE, **parameters) + elt.attributes(href=url) def make_callback(self, namespec: str, action: str): """ Triggers processing iaw action requested. """