/* Copyright (c) Citrix Systems, Inc. * All rights reserved. * * 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.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.Data; using System.Linq; using System.Text; using System.Windows.Forms; using System.Collections; using XenAdmin.Core; using XenAdmin.Dialogs; using XenAPI; using XenAdmin.Commands; using XenCenterLib; namespace XenAdmin.TabPages { internal partial class VMStoragePage : BaseTabPage { private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); private readonly DataGridViewColumn storageLinkColumn; private VM vm; public VMStoragePage() { InitializeComponent(); storageLinkColumn = ColumnSRVolume; Text = Messages.VIRTUAL_DISKS; dataGridViewStorage.Sort(ColumnDevicePosition, ListSortDirection.Ascending); } public VM VM { set { Program.AssertOnEventThread(); // de-register old listeners... UnregisterHandlers(); vm = value; multipleDvdIsoList1.VM = vm; if (vm != null) vm.PropertyChanged += vm_PropertyChanged; BuildList(); // set all columns to be preferred width but allow user to resize. foreach (DataGridViewTextBoxColumn col in dataGridViewStorage.Columns) { if (col != storageLinkColumn) { col.Width = col.GetPreferredWidth(DataGridViewAutoSizeColumnMode.AllCells, false); } } } } private void vdi_PropertyChanged(object sender, PropertyChangedEventArgs e) { // Nothing changing on any of the vdi's will require a rebuild of the list... Program.Invoke(this, () => { if (e.PropertyName == "allowed_operations") UpdateButtons(); else UpdateData(); }); } private void vbd_PropertyChanged(object sender, PropertyChangedEventArgs e) { // Only rebuild list if VDIs changed - otherwise just refresh if (e.PropertyName == "VDI" || e.PropertyName == "empty" || e.PropertyName == "device") { BuildList(); } else if (e.PropertyName == "allowed_operations") { UpdateButtons(); } else if (e.PropertyName == "currently_attached") { UpdateButtons(); UpdateData(); } else { UpdateData(); } } private void UnregisterHandlers() { if (vm == null) return; foreach (VBD vbd in vm.Connection.ResolveAll(vm.VBDs)) { vbd.PropertyChanged -= vbd_PropertyChanged; VDI vdi = vm.Connection.Resolve(vbd.VDI); if (vdi == null) continue; vdi.PropertyChanged -= vdi_PropertyChanged; } vm.PropertyChanged -= vm_PropertyChanged; multipleDvdIsoList1.DeregisterEvents(); } public override void PageHidden() { UnregisterHandlers(); } private void UpdateData() { try { dataGridViewStorage.SuspendLayout(); foreach (DataGridViewRow r in dataGridViewStorage.Rows) { VBDRow row = r as VBDRow; row.UpdateCells(); } dataGridViewStorage.Sort(dataGridViewStorage.SortedColumn, dataGridViewStorage.SortOrder == SortOrder.Ascending ? ListSortDirection.Ascending : ListSortDirection.Descending); } catch (Exception e) { log.Error("Error updating vm storage list.", e); } finally { dataGridViewStorage.ResumeLayout(); } } private void vm_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == "VBDs") BuildList(); else if (e.PropertyName == "power_state" || e.PropertyName == "Locked") Program.Invoke(this, UpdateButtons); } private void BuildList() { Program.AssertOnEventThread(); if (!this.Visible) return; try { dataGridViewStorage.SuspendLayout(); DataGridViewSelectedRowCollection vbdSavedItems = dataGridViewStorage.SelectedRows; dataGridViewStorage.Rows.Clear(); if (vm == null) return; bool storageLinkColumnVisible = false; List devices_in_use = new List(); foreach (VBD vbd in vm.Connection.ResolveAll(vm.VBDs)) { vbd.PropertyChanged -= new PropertyChangedEventHandler(vbd_PropertyChanged); vbd.PropertyChanged += new PropertyChangedEventHandler(vbd_PropertyChanged); if (!vbd.IsCDROM() && !vbd.IsFloppyDrive()) { VDI vdi = vm.Connection.Resolve(vbd.VDI); if (vdi == null || !vdi.Show(Properties.Settings.Default.ShowHiddenVMs)) continue; SR sr = vm.Connection.Resolve(vdi.SR); if (sr == null || sr.IsToolsSR()) continue; storageLinkColumnVisible = vdi.sm_config.ContainsKey("SVID"); vdi.PropertyChanged -= new PropertyChangedEventHandler(vdi_PropertyChanged); vdi.PropertyChanged += new PropertyChangedEventHandler(vdi_PropertyChanged); dataGridViewStorage.Rows.Add(new VBDRow(vbd, vdi, sr)); int i; if (int.TryParse(vbd.userdevice, out i)) { while (devices_in_use.Count <= i) { devices_in_use.Add(false); } devices_in_use[i] = true; } } //CA-47050: the dnsColumn should be autosized to Fill, but should not become smaller than a minimum //width, which is chosen to be the column's contents (including header) width. To find what this is //set temporarily the column's autosize mode to AllCells. HelpersGUI.ResizeLastGridViewColumn(ColumnDevicePath); } storageLinkColumn.Visible = storageLinkColumnVisible; dataGridViewStorage.Sort(dataGridViewStorage.SortedColumn, dataGridViewStorage.SortOrder == SortOrder.Ascending ? ListSortDirection.Ascending : ListSortDirection.Descending); IEnumerable vbdsSelected = from VBDRow row in vbdSavedItems select row.VBD; foreach (VBDRow row in dataGridViewStorage.Rows) { row.Selected = vbdsSelected.Contains(row.VBD); } } catch (Exception e) { log.ErrorFormat("Exception building VM storage list: {0}", e.Message); } finally { dataGridViewStorage.ResumeLayout(); } UpdateButtons(); } private List SelectedVBDRows { get { if (dataGridViewStorage.SelectedRows.Count == 0) return null; List rows = new List(); foreach (DataGridViewRow r in dataGridViewStorage.SelectedRows) rows.Add(r as VBDRow); return rows; } } private void UpdateButtons() { AttachVirtualDiskCommand attachCmd = new AttachVirtualDiskCommand(Program.MainWindow, vm); AttachButton.Enabled = attachCmd.CanExecute(); AddButton.Enabled = attachCmd.CanExecute(); List vbdRows = SelectedVBDRows; if (dataGridViewStorage.Rows.Count == 0 || vbdRows == null || vm == null) { DeactivateButton.Enabled = false; DetachButton.Enabled = false; DeleteButton.Enabled = false; EditButton.Enabled = false; MoveButton.Enabled = false; return; } EditButton.Enabled = vbdRows.Count == 1 && !vbdRows[0].VBD.Locked && !vbdRows[0].VDI.Locked; List selectedVDIs = new List(); List selectedVBDs = new List(); foreach (VBDRow r in vbdRows) { selectedVDIs.Add(new SelectedItem(r.VDI)); selectedVBDs.Add(new SelectedItem(r.VBD)); } DeleteVirtualDiskCommand deleteCmd = new DeleteVirtualDiskCommand(Program.MainWindow, selectedVDIs); // User has visibility that this disk in use by this VM. Allow unplug + delete in single step (non default behaviour), // but only if we are the only VBD (default behaviour) deleteCmd.AllowRunningVMDelete = true; if (deleteCmd.CanExecute()) { DeleteButtonContainer.RemoveAll(); DeleteButton.Enabled = true; } else { DeleteButtonContainer.SetToolTip(deleteCmd.ToolTipText); DeleteButton.Enabled = false; } Command activationCmd = null; SelectedItemCollection vbdCol = new SelectedItemCollection(selectedVBDs); if (vbdCol.AsXenObjects().Find(delegate(VBD vbd) { return !vbd.currently_attached; }) == null) { // no VBDs are attached so we are deactivating toolStripMenuItemDeactivate.Text = DeactivateButton.Text = Messages.DEACTIVATE; activationCmd = new DeactivateVBDCommand(Program.MainWindow, selectedVBDs); } else { // this is the default cause in the mixed attached/detached scenario. We try to activate all the selection // The command error reports afterwards about the ones which are already attached toolStripMenuItemDeactivate.Text = DeactivateButton.Text = Messages.ACTIVATE; activationCmd = new ActivateVBDCommand(Program.MainWindow, selectedVBDs); } if (activationCmd.CanExecute()) { DeactivateButtonContainer.RemoveAll(); DeactivateButton.Enabled = true; } else { DeactivateButtonContainer.SetToolTip(activationCmd.ToolTipText); DeactivateButton.Enabled = false; } DetachVirtualDiskCommand detachCmd = new DetachVirtualDiskCommand(Program.MainWindow, selectedVDIs, vm); if (detachCmd.CanExecute()) { DetachButtonContainer.RemoveAll(); DetachButton.Enabled = true; } else { DetachButtonContainer.SetToolTip(detachCmd.ToolTipText); DetachButton.Enabled = false; } // Move button Command moveCmd = MoveVirtualDiskDialog.MoveMigrateCommand(Program.MainWindow, selectedVDIs); if (moveCmd.CanExecute()) { MoveButton.Enabled = true; MoveButtonContainer.RemoveAll(); } else { MoveButton.Enabled = false; MoveButtonContainer.SetToolTip(moveCmd.ToolTipText); } } #region Actions on VDIs private void AddVdi() { var cmd = new AddVirtualDiskCommand(Program.MainWindow, vm); if (cmd.CanExecute()) cmd.Execute(); // don't wait for the property change to trigger it, as this can take a while UpdateButtons(); } private void AttachVdi() { AttachVirtualDiskCommand cmd = new AttachVirtualDiskCommand(Program.MainWindow, vm); if (cmd.CanExecute()) cmd.Execute(); UpdateButtons(); } private void MoveVdi() { List rows = SelectedVBDRows; if (rows == null) return; List l = new List(); foreach (VBDRow r in rows) l.Add(new SelectedItem(r.VDI)); Command cmd = MoveVirtualDiskDialog.MoveMigrateCommand(Program.MainWindow, l); if (cmd.CanExecute()) cmd.Execute(); } private void DetachVdi() { List rows = SelectedVBDRows; if (rows == null) return; List l = new List(); foreach (VBDRow r in rows) l.Add(new SelectedItem(r.VDI)); DetachVirtualDiskCommand cmd = new DetachVirtualDiskCommand(Program.MainWindow, l, vm); if (cmd.CanExecute()) cmd.Execute(); } private void DeleteVdi() { List rows = SelectedVBDRows; if (rows == null) return; List l = new List(); foreach (VBDRow r in rows) l.Add(new SelectedItem(r.VDI)); DeleteVirtualDiskCommand cmd = new DeleteVirtualDiskCommand(Program.MainWindow, l); // User has visibility that this disk in use by this VM. Allow unplug + delete in single step (non default behaviour), // but only if we are the only VBD (default behaviour) cmd.AllowRunningVMDelete = true; if (cmd.CanExecute()) cmd.Execute(); } private void EditVdi() { if (vm == null) return; List rows = SelectedVBDRows; if (rows == null) return; new PropertiesDialog(rows[0].VDI).ShowDialog(this); } private void ActivateDeactivateVdi() { List rows = SelectedVBDRows; if (rows == null) return; var selection = from VBDRow r in rows select new SelectedItem(r.VBD); SelectedItemCollection col = new SelectedItemCollection(selection); Command cmd = null; if (col.AsXenObjects().Find(vbd => !vbd.currently_attached) == null) cmd = new DeactivateVBDCommand(Program.MainWindow, col); else cmd = new ActivateVBDCommand(Program.MainWindow, col); if (cmd.CanExecute()) cmd.Execute(); } #endregion #region Datagridview event handlers private void dataGridViewStorage_SortCompare(object sender, DataGridViewSortCompareEventArgs e) { if (e.Column == ColumnDevicePosition) { e.SortResult = StringUtility.NaturalCompare(e.CellValue1.ToString(), e.CellValue2.ToString()); e.Handled = true; } else { e.Handled = false; } } private void dataGridViewStorage_SelectedIndexChanged(object sender, EventArgs e) { UpdateButtons(); } private void dataGridViewStorage_CellMouseDoubleClick(object sender, DataGridViewCellMouseEventArgs e) { if (e.RowIndex >= 0 && EditButton.Enabled) { // If double-click was on data row (not including header), launch the storage edit box EditVdi(); } } private void dataGridViewStorage_MouseUp(object sender, MouseEventArgs e) { DataGridView.HitTestInfo hitTestInfo = dataGridViewStorage.HitTest(e.X, e.Y); if (hitTestInfo.Type == DataGridViewHitTestType.None) { dataGridViewStorage.ClearSelection(); } else if (hitTestInfo.Type == DataGridViewHitTestType.Cell && e.Button == MouseButtons.Right && 0 <= hitTestInfo.RowIndex && hitTestInfo.RowIndex < dataGridViewStorage.Rows.Count && !dataGridViewStorage.Rows[hitTestInfo.RowIndex].Selected) { // Select the row that the user right clicked on (similiar to outlook) if it's not already in the selection // (avoids clearing a multiselect if you right click inside it) // Check if the CurrentCell is the cell the user right clicked on (but the row is not Selected) [CA-64954] // This happens when the grid is initially shown: the current cell is the first cell in the first column, but the row is not selected if (dataGridViewStorage.CurrentCell == dataGridViewStorage[hitTestInfo.ColumnIndex, hitTestInfo.RowIndex]) dataGridViewStorage.Rows[hitTestInfo.RowIndex].Selected = true; else dataGridViewStorage.CurrentCell = dataGridViewStorage[hitTestInfo.ColumnIndex, hitTestInfo.RowIndex]; } if ((hitTestInfo.Type == DataGridViewHitTestType.None || hitTestInfo.Type == DataGridViewHitTestType.Cell) && e.Button == MouseButtons.Right) { contextMenuStrip1.Show(dataGridViewStorage, new Point(e.X, e.Y)); } } private void dataGridViewStorage_KeyUp(object sender, KeyEventArgs e) { if (e.KeyCode != Keys.Apps) return; if (dataGridViewStorage.SelectedRows.Count == 0) { // 3 is the defaul control margin contextMenuStrip1.Show(dataGridViewStorage, 3, dataGridViewStorage.ColumnHeadersHeight + 3); } else { DataGridViewRow row = dataGridViewStorage.SelectedRows[0]; contextMenuStrip1.Show(dataGridViewStorage, 3, row.Height * (row.Index + 2)); } } #endregion #region Button and Toolstrip event handlers private void contextMenuStrip1_Opening(object sender, CancelEventArgs e) { bool add = AddButton.Visible && AddButton.Enabled; bool attach = AttachButton.Visible && AttachButton.Enabled; bool deactivate = DeactivateButton.Enabled; bool move = MoveButton.Enabled; bool delete = DeleteButton.Visible && DeleteButton.Enabled; bool detach = DetachButton.Visible && DetachButton.Enabled; bool edit = EditButton.Visible && EditButton.Enabled; if (!(add || attach || deactivate || move || delete || detach || edit)) { e.Cancel = true; return; } toolStripMenuItemAdd.Visible = add; toolStripMenuItemAttach.Visible = attach; toolStripMenuItemDeactivate.Visible = deactivate; toolStripMenuItemMove.Visible = move; toolStripMenuItemDelete.Visible = delete; toolStripMenuItemDetach.Visible = detach; toolStripMenuItemProperties.Visible = edit; toolStripSeparator1.Visible = (add || attach || deactivate || move || delete || detach) && edit; } private void toolStripMenuItemAdd_Click(object sender, EventArgs e) { AddVdi(); } private void toolStripMenuItemAttach_Click(object sender, EventArgs e) { AttachVdi(); } private void toolStripMenuItemDeactivate_Click(object sender, EventArgs e) { ActivateDeactivateVdi(); } private void toolStripMenuItemMove_Click(object sender, EventArgs e) { MoveVdi(); } private void toolStripMenuItemDelete_Click(object sender, EventArgs e) { DeleteVdi(); } private void toolStripMenuItemDetach_Click(object sender, EventArgs e) { DetachVdi(); } private void toolStripMenuItemProperties_Click(object sender, EventArgs e) { EditVdi(); } private void AddButton_Click(object sender, EventArgs e) { AddVdi(); } private void AttachButton_Click(object sender, EventArgs e) { AttachVdi(); } private void DeactivateButton_Click(object sender, EventArgs e) { ActivateDeactivateVdi(); } private void MoveButton_Click(object sender, EventArgs e) { MoveVdi(); } private void DeleteDriveButton_Click(object sender, EventArgs e) { DeleteVdi(); } private void DetachButton_Click(object sender, EventArgs e) { DetachVdi(); } private void EditButton_Click(object sender, EventArgs e) { EditVdi(); } #endregion } public class VBDRow : DataGridViewRow { public readonly VBD VBD; public readonly VDI VDI; public readonly SR SR; /// /// Arguments should never be null /// public VBDRow(VBD vbd, VDI vdi, SR sr) { Debug.Assert(vbd != null && vdi != null && sr != null, "vbd, vdi and sr must be set to a non-null value"); VBD = vbd; VDI = vdi; SR = sr; for (int i = 0; i < 10; i++) { DataGridViewTextBoxCell c = new DataGridViewTextBoxCell(); c.Value = CellValue(i); Cells.Add(c); } } public int CompareTo(object o) { VBDRow r = (VBDRow)o; string c1 = this.VBD.userdevice; string c2 = r.VBD.userdevice; int i1; int i2; if (int.TryParse(c1, out i1) && int.TryParse(c2, out i2)) { return i1 - i2; } else { return c1.CompareTo(c2); } } private object CellValue(int col) { switch (col) { case 0: return VBD.userdevice; case 1: return VDI.Name(); case 2: return VDI.Description(); case 3: return SR.Name(); case 4: string name; return VDI.sm_config.TryGetValue("displayname", out name) ? name : ""; case 5: return VDI.SizeText(); case 6: return VBD.IsReadOnly() ? Messages.YES : Messages.NO; case 7: return GetPriorityString(VBD.GetIoNice()); case 8: return VBD.currently_attached ? Messages.YES : Messages.NO; case 9: return VBD.device == "" ? Messages.STORAGE_PANEL_UNKNOWN : string.Format("/dev/{0}", VBD.device); default: throw new ArgumentException(String.Format("Invalid column number {0} in VBDRenderer.CellValue()", col)); } } public void UpdateCells() { for (int i = 0; i < 10; i++) { Cells[i].Value = CellValue(i); } } private static string GetPriorityString(int p) { switch (p) { case 0: return Messages.STORAGE_WORST; case 7: return Messages.STORAGE_BEST; default: return p.ToString(); } } } }