xenadmin/XenAdmin/TabPages/ManageCdnUpdatesPage.cs
Konstantina Chremmou de1120f18b
More fixes including CA-381728, CA-381618, CA-381225 (#3205)
* CA-381728: If no post-update tasks exist, XenCenter should explicitly state so.

Also, livepatches should be shown if other guidance is absent.

Signed-off-by: Konstantina Chremmou <Konstantina.Chremmou@cloud.com>

* Missing placeholders for building locally without applying branding.

Signed-off-by: Konstantina Chremmou <Konstantina.Chremmou@cloud.com>

* Missing help links.

Also removed scripts that are not used any more.

Signed-off-by: Konstantina Chremmou <Konstantina.Chremmou@cloud.com>

* CA-381618: The HA/WLB off check is a pool, not a host check.

Signed-off-by: Konstantina Chremmou <Konstantina.Chremmou@cloud.com>

* CA-381225: Corrected order of running update guidance (also uses input from CA-381718).

Signed-off-by: Konstantina Chremmou <Konstantina.Chremmou@cloud.com>

* UPDATES_URL has been renamed to XC_UPDATES_URL.

Signed-off-by: Konstantina Chremmou <Konstantina.Chremmou@cloud.com>

---------

Signed-off-by: Konstantina Chremmou <Konstantina.Chremmou@cloud.com>
2023-08-21 13:35:49 +01:00

599 lines
23 KiB
C#

/* Copyright (c) Cloud Software Group, Inc.
*
* Redistribution and use in source and binary forms,
* with or without modification, are permitted provided
* that the following conditions are met:
*
* * Redistributions of source code must retain the above
* copyright notice, this list of conditions and the
* following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using XenAdmin.Actions;
using XenAdmin.Controls.MainWindowControls;
using XenAdmin.Core;
using XenAdmin.Dialogs;
using XenAdmin.Network;
using XenAdmin.TabPages.CdnUpdates;
using XenAdmin.Wizards.PatchingWizard;
using XenAPI;
namespace XenAdmin.TabPages
{
public partial class ManageCdnUpdatesPage : NotificationsBasePage
{
private int checksQueue;
private int _rowInsertionIndex;
private volatile bool _buildInProgress;
private volatile bool _buildRequired;
public ManageCdnUpdatesPage()
{
InitializeComponent();
tsSplitButtonSynchronize.DefaultItem = tsmiSynchronizeSelected;
tsSplitButtonSynchronize.Text = tsmiSynchronizeSelected.Text;
}
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
try
{
//ensure we won't try to rebuild the list while setting the initial view
checksQueue++;
Rebuild();
}
finally
{
checksQueue--;
}
}
#region NotificationPage overrides
protected override void RefreshPage()
{
toolStripDropDownButtonServerFilter.InitializeHostList(Helpers.CloudOrGreater);
Rebuild();
}
protected override void RegisterEventHandlers()
{
Updates.CdnUpdateInfoChanged += Cdn_UpdateInfoChanged;
}
protected override void DeregisterEventHandlers()
{
Updates.CdnUpdateInfoChanged -= Cdn_UpdateInfoChanged;
}
public override string HelpID => "ManageCdnUpdatesTabPage";
public override NotificationsSubMode NotificationsSubMode => NotificationsSubMode.UpdatesFromCdn;
#endregion
private void Cdn_UpdateInfoChanged(IXenConnection obj)
{
Rebuild();
}
public override bool FilterIsOn => toolStripDropDownButtonServerFilter.FilterIsOn;
private void Rebuild()
{
if (_buildInProgress)
{
_buildRequired = true;
return;
}
Program.Invoke(Program.MainWindow, () =>
{
if (!Visible || checksQueue > 0)
return;
try
{
_buildInProgress = true;
OnFiltersChanged();
BuildList();
UpdateButtonEnablement();
}
finally
{
_buildInProgress = false;
if (_buildRequired)
{
_buildRequired = false;
Rebuild();
}
}
});
}
private void BuildList()
{
dataGridViewEx1.SuspendLayout();
try
{
dataGridViewEx1.Rows.Clear();
var connections = ConnectionsManager.XenConnectionsCopy
.Where(c => c.IsConnected && Helpers.CloudOrGreater(c)).ToList();
connections.Sort();
foreach (var connection in connections)
{
Updates.CdnUpdateInfoPerConnection.TryGetValue(connection, out var poolUpdateInfo);
CdnExpandableRow row = null;
if (Helpers.GetPool(connection) == null)
{
if (poolUpdateInfo == null)
{
var host = connection.Cache.Hosts.FirstOrDefault();
if (host != null && !toolStripDropDownButtonServerFilter.HideByLocation(host.uuid))
row = new HostUpdateInfoRow(connection, host, null, null);
}
else
{
var hostUpdateInfo = poolUpdateInfo.HostsWithUpdates.FirstOrDefault();
var host = connection.Resolve(new XenRef<Host>(hostUpdateInfo?.HostOpaqueRef));
if (host != null && !toolStripDropDownButtonServerFilter.HideByLocation(host.uuid))
row = new HostUpdateInfoRow(connection, host, poolUpdateInfo, hostUpdateInfo);
}
}
else
{
var hostUuidList = connection.Cache.Hosts.Select(h => h.uuid).ToList();
if (!toolStripDropDownButtonServerFilter.HideByLocation(hostUuidList))
row = new PoolUpdateInfoRow(connection, poolUpdateInfo);
}
if (row != null)
dataGridViewEx1.Rows.Add(row);
}
}
finally
{
dataGridViewEx1.ResumeLayout();
}
}
public void UpdateButtonEnablement()
{
toolStripButtonExportAll.Enabled = dataGridViewEx1.RowCount > 0;
//need to check this because it takes a while until xapi removes the operation from pool.allowed_operations
var syncActions = ConnectionsManager.History.Where(a =>
!a.IsCompleted && a is SyncWithCdnAction syncA && !syncA.Cancelled).ToList();
Pool selectedPool = null;
if (dataGridViewEx1.SelectedRows.Count > 0)
{
switch (dataGridViewEx1.SelectedRows[0])
{
case PoolUpdateInfoRow poolRow:
selectedPool = poolRow.Pool;
break;
case HostUpdateInfoRow hostRow when Helpers.GetPool(hostRow.Connection) == null:
selectedPool = Helpers.GetPoolOfOne(hostRow.Connection);
break;
}
}
tsmiSynchronizeSelected.Enabled = selectedPool != null && selectedPool.repositories.Count > 0 &&
!syncActions.Select(a => a.Connection).Contains(selectedPool.Connection) &&
selectedPool.allowed_operations.Contains(pool_allowed_operations.sync_updates);
var allAllowed = syncActions.Count == 0;
if (allAllowed)
{
foreach (DataGridViewRow row in dataGridViewEx1.Rows)
{
if (row is PoolUpdateInfoRow puiRow &&
(puiRow.Pool.repositories.Count == 0 ||
!puiRow.Pool.allowed_operations.Contains(pool_allowed_operations.sync_updates)))
{
allAllowed = false;
break;
}
if (row is HostUpdateInfoRow huiRow && Helpers.GetPool(huiRow.Connection) == null)
{
var pool = Helpers.GetPoolOfOne(huiRow.Connection);
if (pool != null && (pool.repositories.Count == 0 ||
!pool.allowed_operations.Contains(pool_allowed_operations.sync_updates)))
{
allAllowed = false;
break;
}
}
}
}
tsmiSynchronizeAll.Enabled = allAllowed;
tsSplitButtonSynchronize.Enabled = tsmiSynchronizeSelected.Enabled || tsmiSynchronizeAll.Enabled;
if (tsSplitButtonSynchronize.DefaultItem != null && !tsSplitButtonSynchronize.DefaultItem.Enabled)
{
foreach (ToolStripItem item in tsSplitButtonSynchronize.DropDownItems)
{
if (item.Enabled)
{
tsSplitButtonSynchronize.DefaultItem = item;
tsSplitButtonSynchronize.Text = item.Text;
break;
}
}
}
}
private void ToggleRow(CdnExpandableRow row)
{
row.IsExpanded = !row.IsExpanded;
dataGridViewEx1.SuspendLayout();
try
{
if (row.IsExpanded)
ExpandRow(row);
else
CollapseRow(row);
}
finally
{
dataGridViewEx1.ResumeLayout();
}
}
private void ExpandRow(CdnExpandableRow row)
{
_rowInsertionIndex = row.Index;
foreach (var childRow in row.ChildRows)
{
if (row is HostUpdateInfoRow huiRow && toolStripDropDownButtonServerFilter.HideByLocation(huiRow.Host.uuid))
continue;
dataGridViewEx1.Rows.Insert(++_rowInsertionIndex, childRow);
childRow.ParentRow = row;
childRow.Level = row.Level + 1;
if (childRow.IsExpanded)
ExpandRow(childRow);
}
}
private void CollapseRow(CdnExpandableRow row)
{
foreach (var childRow in row.ChildRows)
{
dataGridViewEx1.Rows.Remove(childRow);
childRow.ParentRow = null;
childRow.Level = 0;
if (childRow.IsExpanded)
CollapseRow(childRow);
}
}
private void SyncPool(Pool pool)
{
var syncAction = new SyncWithCdnAction(pool);
UpdateButtonEnablement();
syncAction.Completed += a => Updates.CheckForCdnUpdates(a.Connection);
syncAction.RunAsync();
}
#region Toolstrip handlers
private void toolStripDropDownButtonServerFilter_FilterChanged()
{
Rebuild();
}
private void tsSplitButtonSynchronize_DropDownItemClicked(object sender, ToolStripItemClickedEventArgs e)
{
tsSplitButtonSynchronize.DefaultItem = e.ClickedItem;
tsSplitButtonSynchronize.Text = tsSplitButtonSynchronize.DefaultItem.Text;
}
private void tsmiSynchronizeSelected_Click(object sender, EventArgs e)
{
if (dataGridViewEx1.SelectedRows.Count > 0)
{
var row = dataGridViewEx1.SelectedRows[0];
Pool pool = null;
if (row is PoolUpdateInfoRow puiRow)
pool = puiRow.Pool;
else if (row is HostUpdateInfoRow huiRow && Helpers.GetPool(huiRow.Connection) == null)
pool = Helpers.GetPoolOfOne(huiRow.Connection);
if (pool != null)
SyncPool(pool);
}
}
private void tsmiSynchronizeAll_Click(object sender, EventArgs e)
{
DialogResult result;
if (FilterIsOn)
{
using (var dlog = new NoIconDialog(Messages.YUM_REPO_SYNC_FILTER_CONFIRMATION,
new ThreeButtonDialog.TBDButton(Messages.YUM_REPO_SYNC_YES_ALL_BUTTON, DialogResult.Yes),
new ThreeButtonDialog.TBDButton(Messages.YUM_REPO_SYNC_YES_VISIBLE_BUTTON, DialogResult.No, ThreeButtonDialog.ButtonType.NONE),
ThreeButtonDialog.ButtonCancel))
{
result = dlog.ShowDialog(this);
}
}
else
{
using (var dlog = new NoIconDialog(Messages.YUM_REPO_SYNC_ALL_CONFIRMATION,
new ThreeButtonDialog.TBDButton(Messages.YUM_REPO_SYNC_YES_ALL_BUTTON, DialogResult.Yes),
ThreeButtonDialog.ButtonCancel))
{
result = dlog.ShowDialog(this);
}
}
switch (result)
{
case DialogResult.Cancel:
return;
case DialogResult.Yes:
var connections = ConnectionsManager.XenConnectionsCopy
.Where(c => c.IsConnected && Helpers.CloudOrGreater(c)).ToList();
foreach (var connection in connections)
{
var pool = Helpers.GetPoolOfOne(connection);
if (pool == null)
continue;
SyncPool(pool);
}
return;
case DialogResult.No:
foreach (DataGridViewRow row in dataGridViewEx1.Rows)
{
if (row is PoolUpdateInfoRow puiRow)
{
SyncPool(puiRow.Pool);
}
else if (row is HostUpdateInfoRow huiRow)
{
if (Helpers.GetPool(huiRow.Connection) != null)
continue;
var pool = Helpers.GetPoolOfOne(huiRow.Connection);
if (pool == null)
continue;
SyncPool(pool);
}
}
return;
}
}
private void toolStripButtonUpdate_Click(object sender, EventArgs e)
{
var wizard = (PatchingWizard)Program.MainWindow.ShowForm(typeof(PatchingWizard));
wizard.PrepareToInstallUpdate(true);
}
private void toolStripButtonExportAll_Click(object sender, EventArgs e)
{
bool exportAll = true;
if (FilterIsOn)
{
using (var dlog = new NoIconDialog(Messages.UPDATE_EXPORT_ALL_OR_FILTERED,
new ThreeButtonDialog.TBDButton(Messages.EXPORT_ALL_BUTTON, DialogResult.Yes),
new ThreeButtonDialog.TBDButton(Messages.EXPORT_FILTERED_BUTTON, DialogResult.No, ThreeButtonDialog.ButtonType.NONE),
ThreeButtonDialog.ButtonCancel))
{
var result = dlog.ShowDialog(this);
if (result == DialogResult.No)
exportAll = false;
else if (result == DialogResult.Cancel)
return;
}
}
string fileName;
using (SaveFileDialog dialog = new SaveFileDialog
{
AddExtension = true,
Filter = string.Format("{0} (*.txt)|*.txt|{1} (*.*)|*.*", Messages.TXT_DESCRIPTION, Messages.ALL_FILES),
FilterIndex = 0,
Title = Messages.EXPORT_ALL,
RestoreDirectory = true,
DefaultExt = "csv",
CheckPathExists = false,
OverwritePrompt = true
})
{
if (dialog.ShowDialog(this) != DialogResult.OK)
return;
fileName = dialog.FileName;
}
new DelegatedAsyncAction(null,
string.Format(Messages.EXPORT_UPDATES, fileName),
string.Format(Messages.EXPORTING_UPDATES, fileName),
string.Format(Messages.EXPORTED_UPDATES, fileName),
delegate
{
using (StreamWriter stream = new StreamWriter(fileName, false, Encoding.UTF8))
{
if (exportAll)
{
foreach (var kvp in Updates.CdnUpdateInfoPerConnection)
{
var connection = kvp.Key;
var poolUpdateInfo = kvp.Value;
// Don't need to print the pool name if it's a standalone host
if (Helpers.GetPool(connection) != null)
{
stream.WriteLine(connection.Name);
stream.WriteLine();
}
var hosts = poolUpdateInfo.HostsWithUpdates
.Select(hui => connection.Resolve(new XenRef<Host>(hui.HostOpaqueRef)))
.Where(h => h != null).OrderBy(h => h).ToList();
foreach (var host in hosts)
{
var hostUpdateInfo = poolUpdateInfo.HostsWithUpdates.FirstOrDefault(hui => hui.HostOpaqueRef == host.opaque_ref);
if (hostUpdateInfo == null)
continue;
stream.WriteLine(host.Name());
stream.WriteLine();
stream.WriteLine(Messages.HOTFIX_POST_UPDATE_ACTIONS, string.Join(Environment.NewLine, hostUpdateInfo.RecommendedGuidance.Select(Cdn.FriendlyInstruction)));
stream.WriteLine();
var categories = hostUpdateInfo.GetUpdateCategories(poolUpdateInfo);
foreach (var category in categories)
{
stream.WriteLine($"{category.Item1.GetCategoryTitle(category.Item2.Count)}");
stream.WriteLine();
foreach (var update in category.Item2)
{
stream.WriteLine(update.Summary);
stream.WriteLine(update.CollateDetails());
stream.WriteLine();
}
}
stream.WriteLine(string.Join(", ", hostUpdateInfo.Rpms));
stream.WriteLine();
}
}
}
else
{
foreach (var row in dataGridViewEx1.Rows)
{
if (!(row is PoolUpdateInfoRow puiRow))
continue;
foreach (var childRow in puiRow.ChildRows)
{
if (!(childRow is HostUpdateInfoRow huiRow) ||
toolStripDropDownButtonServerFilter.HideByLocation(huiRow.Host.uuid))
continue;
foreach (var export in huiRow.Export())
stream.WriteLine(export);
}
}
}
}
}).RunAsync();
}
#endregion
#region DataGridView event handlers
private void dataGridViewEx1_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
{
if (e.ColumnIndex == _columnName.Index &&
e.RowIndex >= 0 &&
e.RowIndex < dataGridViewEx1.RowCount &&
dataGridViewEx1.Rows[e.RowIndex] is CdnExpandableRow row)
{
ToggleRow(row);
}
}
private void dataGridViewEx1_CellMouseClick(object sender, DataGridViewCellMouseEventArgs e)
{
if (e.Clicks == 1 &&
e.ColumnIndex == _columnName.Index &&
e.RowIndex >= 0 &&
e.RowIndex < dataGridViewEx1.RowCount &&
dataGridViewEx1.Rows[e.RowIndex] is CdnExpandableRow row &&
row.Cells[e.ColumnIndex] is CdnExpandableTextAndImageCell cell &&
cell.IsPointInExpander(e.Location))
{
ToggleRow(row);
}
}
private void dataGridViewEx1_KeyDown(object sender, KeyEventArgs e)
{
if (dataGridViewEx1.SelectedRows.Count > 0 &&
dataGridViewEx1.SelectedRows[0] is CdnExpandableRow row)
{
switch (e.KeyCode)
{
case Keys.Right when !row.IsExpanded:
case Keys.Left when row.IsExpanded:
break;
default:
return;
}
ToggleRow(row);
}
}
private void dataGridViewEx1_SelectionChanged(object sender, EventArgs e)
{
UpdateButtonEnablement();
}
#endregion
}
}