From 6e6126cca201dae1f3a9499bb0d950fb9d797a8f Mon Sep 17 00:00:00 2001 From: Roy Ben Shabat Date: Fri, 14 Feb 2020 12:13:10 +0200 Subject: Implemented version rollback on AzureUtils. Changed GetLatestVersion on machine service to respond to any version difference instead of smaller version. --- .../Tango.AzureUtils.UI/AzureDashboardViewModel.cs | 12 ++- .../Controls/WebAppPropertiesControl.xaml | 2 + .../Controls/WebAppPropertiesControl.xaml.cs | 13 ++++ .../Tango.AzureUtils.UI/Tango.AzureUtils.UI.csproj | 8 ++ .../Azure/Tango.AzureUtils.UI/ViewModelLocator.cs | 9 +++ .../ViewModels/EnvironmentFirmwareUpgradeViewVM.cs | 14 +++- .../ViewModels/EnvironmentRemovalViewVM.cs | 9 ++- .../ViewModels/EnvironmentRollbackViewVM.cs | 86 ++++++++++++++++++++++ .../ViewModels/EnvironmentUpgradeViewVM.cs | 20 +++-- .../Views/EnvironmentRollbackView.xaml | 44 +++++++++++ .../Views/EnvironmentRollbackView.xaml.cs | 28 +++++++ .../Azure/Tango.AzureUtils.UI/Views/MainView.xaml | 3 + .../Tango.AzureUtils/Database/DatabaseManager.cs | 30 ++++++++ .../Environment/EnvironmentManager.cs | 56 ++++++++++++++ .../RollbackEnvironmentConfiguration.cs | 15 ++++ .../Tango.AzureUtils/Storage/StorageManager.cs | 34 +++++++++ .../Azure/Tango.AzureUtils/Tango.AzureUtils.csproj | 1 + 17 files changed, 370 insertions(+), 14 deletions(-) create mode 100644 Software/Visual_Studio/Azure/Tango.AzureUtils.UI/ViewModels/EnvironmentRollbackViewVM.cs create mode 100644 Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Views/EnvironmentRollbackView.xaml create mode 100644 Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Views/EnvironmentRollbackView.xaml.cs create mode 100644 Software/Visual_Studio/Azure/Tango.AzureUtils/Environment/RollbackEnvironmentConfiguration.cs (limited to 'Software/Visual_Studio/Azure') diff --git a/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/AzureDashboardViewModel.cs b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/AzureDashboardViewModel.cs index eddc7d009..9d3a07b7b 100644 --- a/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/AzureDashboardViewModel.cs +++ b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/AzureDashboardViewModel.cs @@ -27,7 +27,7 @@ namespace Tango.AzureUtils.UI public virtual void OnApplicationReady() { - + } public virtual void OnAuthenticated(IAzure azure, List apps) @@ -47,6 +47,16 @@ namespace Tango.AzureUtils.UI } } + protected void RequireRefresh() + { + TangoIOC.Default.GetAllInstancesByBase().ToList().ForEach(x => x.OnRefreshRequired()); + } + + protected virtual void OnRefreshRequired() + { + + } + public void ProgressHandler(object sender, AzureUtilsProgressEventArgs e) { StatusManager.UpdateStatus(e); diff --git a/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Controls/WebAppPropertiesControl.xaml b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Controls/WebAppPropertiesControl.xaml index 5adec916a..fe8b4bdef 100644 --- a/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Controls/WebAppPropertiesControl.xaml +++ b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Controls/WebAppPropertiesControl.xaml @@ -63,5 +63,7 @@ + + diff --git a/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Controls/WebAppPropertiesControl.xaml.cs b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Controls/WebAppPropertiesControl.xaml.cs index f71f236c7..ea7475fb1 100644 --- a/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Controls/WebAppPropertiesControl.xaml.cs +++ b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Controls/WebAppPropertiesControl.xaml.cs @@ -55,6 +55,13 @@ namespace Tango.AzureUtils.UI.Controls public static readonly DependencyProperty MachineStudioVersionProperty = DependencyProperty.Register("MachineStudioVersion", typeof(MachineStudioVersion), typeof(WebAppPropertiesControl), new PropertyMetadata(null)); + public bool IsBusy + { + get { return (bool)GetValue(IsBusyProperty); } + set { SetValue(IsBusyProperty, value); } + } + public static readonly DependencyProperty IsBusyProperty = + DependencyProperty.Register("IsBusy", typeof(bool), typeof(WebAppPropertiesControl), new PropertyMetadata(false)); public WebAppPropertiesControl() { @@ -71,6 +78,8 @@ namespace Tango.AzureUtils.UI.Controls try { + IsBusy = true; + HostNames = app.HostNames.Select(x => x).ToList(); Settings = await app.GetMachineServiceSettingsAsync(false); @@ -81,6 +90,10 @@ namespace Tango.AzureUtils.UI.Controls MachineStudioVersion = await databaseManager.GetLatestMachineStudioVersion(app); } catch { } + finally + { + IsBusy = false; + } } } } diff --git a/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Tango.AzureUtils.UI.csproj b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Tango.AzureUtils.UI.csproj index 985c54c00..b88b39c6f 100644 --- a/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Tango.AzureUtils.UI.csproj +++ b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Tango.AzureUtils.UI.csproj @@ -207,9 +207,13 @@ + + + EnvironmentRollbackView.xaml + EnvironmentCreationView.xaml @@ -252,6 +256,10 @@ Designer MSBuild:Compile + + MSBuild:Compile + Designer + Designer MSBuild:Compile diff --git a/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/ViewModelLocator.cs b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/ViewModelLocator.cs index 4fd7293c9..ef765f93a 100644 --- a/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/ViewModelLocator.cs +++ b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/ViewModelLocator.cs @@ -24,6 +24,7 @@ namespace Tango.AzureUtils.UI TangoIOC.Default.Register(); TangoIOC.Default.Register(); TangoIOC.Default.Register(); + TangoIOC.Default.Register(); TangoIOC.Default.Register(); } @@ -76,5 +77,13 @@ namespace Tango.AzureUtils.UI return TangoIOC.Default.GetInstance(); } } + + public static EnvironmentRollbackViewVM EnvironmentRollbackViewVM + { + get + { + return TangoIOC.Default.GetInstance(); + } + } } } diff --git a/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/ViewModels/EnvironmentFirmwareUpgradeViewVM.cs b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/ViewModels/EnvironmentFirmwareUpgradeViewVM.cs index 54576cda7..964febbd8 100644 --- a/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/ViewModels/EnvironmentFirmwareUpgradeViewVM.cs +++ b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/ViewModels/EnvironmentFirmwareUpgradeViewVM.cs @@ -94,10 +94,6 @@ namespace Tango.AzureUtils.UI.ViewModels IsFree = false; await _firmwareManager.InjectFirmwarePackage(SelectedDeploymentSlot, FilePath); - - var old = SelectedDeploymentSlot; - SelectedDeploymentSlot = null; - SelectedDeploymentSlot = old; } catch (Exception ex) { @@ -105,8 +101,18 @@ namespace Tango.AzureUtils.UI.ViewModels } finally { + RequireRefresh(); IsFree = true; } } + + protected override void OnRefreshRequired() + { + base.OnRefreshRequired(); + + var old = SelectedDeploymentSlot; + SelectedDeploymentSlot = null; + SelectedDeploymentSlot = old; + } } } diff --git a/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/ViewModels/EnvironmentRemovalViewVM.cs b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/ViewModels/EnvironmentRemovalViewVM.cs index 0917b012d..e296ac16e 100644 --- a/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/ViewModels/EnvironmentRemovalViewVM.cs +++ b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/ViewModels/EnvironmentRemovalViewVM.cs @@ -77,8 +77,6 @@ namespace Tango.AzureUtils.UI.ViewModels Config.Password = Settings.Password; await _environmentManager.RemoveEnvironment(SelectedDeploymentSlot, SlotName, Config); - - SelectedDeploymentSlot = null; } catch (Exception ex) { @@ -86,8 +84,15 @@ namespace Tango.AzureUtils.UI.ViewModels } finally { + RequireRefresh(); IsFree = true; } } + + protected override void OnRefreshRequired() + { + base.OnRefreshRequired(); + SelectedDeploymentSlot = null; + } } } diff --git a/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/ViewModels/EnvironmentRollbackViewVM.cs b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/ViewModels/EnvironmentRollbackViewVM.cs new file mode 100644 index 000000000..69e18b51d --- /dev/null +++ b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/ViewModels/EnvironmentRollbackViewVM.cs @@ -0,0 +1,86 @@ +using Microsoft.Azure.Management.AppService.Fluent; +using Microsoft.Azure.Management.Fluent; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Tango.AzureUtils.Environment; +using Tango.Core.Commands; + +namespace Tango.AzureUtils.UI.ViewModels +{ + public class EnvironmentRollbackViewVM : AzureDashboardViewModel + { + private IWebAppBase _machineServiceApp; + private EnvironmentManager _environmentManager; + + private List _deploymentSlots; + public List DeploymentSlots + { + get { return _deploymentSlots; } + set { _deploymentSlots = value; RaisePropertyChangedAuto(); } + } + + private IDeploymentSlot _selectedDeploymentSlot; + public IDeploymentSlot SelectedDeploymentSlot + { + get { return _selectedDeploymentSlot; } + set { _selectedDeploymentSlot = value; RaisePropertyChangedAuto(); } + } + + private RollbackEnvironmentConfiguration _config; + public RollbackEnvironmentConfiguration Config + { + get { return _config; } + set { _config = value; RaisePropertyChangedAuto(); } + } + + public RelayCommand RollbackEnvironmentCommand { get; set; } + + public EnvironmentRollbackViewVM() + { + RollbackEnvironmentCommand = new RelayCommand(RollbackEnvironment); + Config = new RollbackEnvironmentConfiguration(); + } + + public override void OnAuthenticated(IAzure azure, List apps) + { + _machineServiceApp = apps.SingleOrDefault(x => x.Name == "MachineService"); + DeploymentSlots = apps.OfType().Where(x => x.Parent == _machineServiceApp).ToList(); + SelectedDeploymentSlot = DeploymentSlots.FirstOrDefault(); + + _environmentManager = new EnvironmentManager(azure); + _environmentManager.ConfirmationRequired += ConfirmationHandler; + _environmentManager.Progress += ProgressHandler; + } + + private async void RollbackEnvironment() + { + try + { + IsFree = false; + + await _environmentManager.RollbackEnvironment(SelectedDeploymentSlot, Config); + } + catch (Exception ex) + { + StatusManager.UpdateStatus(ex); + } + finally + { + RequireRefresh(); + IsFree = true; + } + } + + protected override void OnRefreshRequired() + { + base.OnRefreshRequired(); + + var old = SelectedDeploymentSlot; + SelectedDeploymentSlot = null; + SelectedDeploymentSlot = old; + } + } +} diff --git a/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/ViewModels/EnvironmentUpgradeViewVM.cs b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/ViewModels/EnvironmentUpgradeViewVM.cs index 14e4bf196..3f353d54d 100644 --- a/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/ViewModels/EnvironmentUpgradeViewVM.cs +++ b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/ViewModels/EnvironmentUpgradeViewVM.cs @@ -112,13 +112,6 @@ namespace Tango.AzureUtils.UI.ViewModels { IsFree = false; await EnvironmentManager.UpgradeEnvironment(SelectedSourceApp, SelectedTargetApp, Config); - - var oldSource = SelectedSourceApp; - var oldTarget = SelectedTargetApp; - SelectedSourceApp = null; - SelectedTargetApp = null; - SelectedSourceApp = oldSource; - SelectedTargetApp = oldTarget; } catch (Exception ex) { @@ -126,8 +119,21 @@ namespace Tango.AzureUtils.UI.ViewModels } finally { + RequireRefresh(); IsFree = true; } } + + protected override void OnRefreshRequired() + { + base.OnRefreshRequired(); + + var oldSource = SelectedSourceApp; + var oldTarget = SelectedTargetApp; + SelectedSourceApp = null; + SelectedTargetApp = null; + SelectedSourceApp = oldSource; + SelectedTargetApp = oldTarget; + } } } diff --git a/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Views/EnvironmentRollbackView.xaml b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Views/EnvironmentRollbackView.xaml new file mode 100644 index 000000000..d3a198e21 --- /dev/null +++ b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Views/EnvironmentRollbackView.xaml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + Rollback Machine Studio Version + Rollback PPC Version + + + + + + + diff --git a/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Views/EnvironmentRollbackView.xaml.cs b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Views/EnvironmentRollbackView.xaml.cs new file mode 100644 index 000000000..aa38c19d0 --- /dev/null +++ b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Views/EnvironmentRollbackView.xaml.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace Tango.AzureUtils.UI.Views +{ + /// + /// Interaction logic for EnvironmentCreationView.xaml + /// + public partial class EnvironmentRollbackView : UserControl + { + public EnvironmentRollbackView() + { + InitializeComponent(); + } + } +} diff --git a/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Views/MainView.xaml b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Views/MainView.xaml index b79d306d1..aa3623f4b 100644 --- a/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Views/MainView.xaml +++ b/Software/Visual_Studio/Azure/Tango.AzureUtils.UI/Views/MainView.xaml @@ -55,6 +55,9 @@ + + + diff --git a/Software/Visual_Studio/Azure/Tango.AzureUtils/Database/DatabaseManager.cs b/Software/Visual_Studio/Azure/Tango.AzureUtils/Database/DatabaseManager.cs index 0c2e56edf..cb1a608a8 100644 --- a/Software/Visual_Studio/Azure/Tango.AzureUtils/Database/DatabaseManager.cs +++ b/Software/Visual_Studio/Azure/Tango.AzureUtils/Database/DatabaseManager.cs @@ -231,6 +231,36 @@ namespace Tango.AzureUtils.Database } } + public async Task DowngradeMachineStudioVersion(IWebAppBase app) + { + var latestMachineStudioVersion = await GetLatestMachineStudioVersion(app); + var dataSource = (await app.GetMachineServiceSettingsAsync()).ToDataSource(); + + OnProgress(AzureUtilsStage.Database, $"Removing machine studio database entry for version '{latestMachineStudioVersion.Version}'..."); + + using (var db = ObservablesContext.CreateDefault(dataSource)) + { + var latest = await db.MachineStudioVersions.SingleOrDefaultAsync(x => x.Guid == latestMachineStudioVersion.Guid); + db.MachineStudioVersions.Remove(latest); + await db.SaveChangesAsync(); + } + } + + public async Task DowngradePPCVersion(IWebAppBase app) + { + var latestPPCVersion = await GetLatestPPCVersion(app); + var dataSource = (await app.GetMachineServiceSettingsAsync()).ToDataSource(); + + OnProgress(AzureUtilsStage.Database, $"Removing PPC database entry for version '{latestPPCVersion.Version}'..."); + + using (var db = ObservablesContext.CreateDefault(dataSource)) + { + var latest = await db.TangoVersions.SingleOrDefaultAsync(x => x.Guid == latestPPCVersion.Guid); + db.TangoVersions.Remove(latest); + await db.SaveChangesAsync(); + } + } + public async Task GetLatestMachineStudioVersion(IWebAppBase app) { OnProgress(AzureUtilsStage.Database, $"Getting latest machine studio version on '{app.Name}'..."); diff --git a/Software/Visual_Studio/Azure/Tango.AzureUtils/Environment/EnvironmentManager.cs b/Software/Visual_Studio/Azure/Tango.AzureUtils/Environment/EnvironmentManager.cs index 83b0c29ee..53665a73d 100644 --- a/Software/Visual_Studio/Azure/Tango.AzureUtils/Environment/EnvironmentManager.cs +++ b/Software/Visual_Studio/Azure/Tango.AzureUtils/Environment/EnvironmentManager.cs @@ -453,5 +453,61 @@ namespace Tango.AzureUtils.Environment } #endregion + + #region Rollback + + public async Task RollbackEnvironment(IWebAppBase app, RollbackEnvironmentConfiguration config) + { + OnProgress(AzureUtilsStage.Environment, $"Retrieving settings for '{app.Name}'..."); + + var settings = await app.GetMachineServiceSettingsAsync(); + await _storageManager.Connect(settings.STORAGE_ACCOUNT); + + if (config.RollbackMachineStudio) + { + try + { + await _storageManager.DowngradeMachineStudioStorage(app); + } + catch (Exception ex) + { + await RequestConfirmation($"Error occurred while trying to remove machine studio storage blobs.\n{ex.FlattenMessage()}\nDo you wish to continue?"); + } + + try + { + await _databaseManager.DowngradeMachineStudioVersion(app); + } + catch (Exception ex) + { + await RequestConfirmation($"Error occurred while trying to remove machine studio database version.\n{ex.FlattenMessage()}\nDo you wish to continue?"); + } + } + + if (config.RollbackPPC) + { + try + { + await _storageManager.DowngradePPCStorage(app); + } + catch (Exception ex) + { + await RequestConfirmation($"Error occurred while trying to remove PPC storage blobs.\n{ex.FlattenMessage()}\nDo you wish to continue?"); + } + + try + { + await _databaseManager.DowngradePPCVersion(app); + } + catch (Exception ex) + { + await RequestConfirmation($"Error occurred while trying to remove PPC database version.\n{ex.FlattenMessage()}\nDo you wish to continue?"); + } + } + + OnCompleted("Environment rollback completed successfully."); + } + + #endregion } } diff --git a/Software/Visual_Studio/Azure/Tango.AzureUtils/Environment/RollbackEnvironmentConfiguration.cs b/Software/Visual_Studio/Azure/Tango.AzureUtils/Environment/RollbackEnvironmentConfiguration.cs new file mode 100644 index 000000000..da0f68aac --- /dev/null +++ b/Software/Visual_Studio/Azure/Tango.AzureUtils/Environment/RollbackEnvironmentConfiguration.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Tango.AzureUtils.Environment +{ + public class RollbackEnvironmentConfiguration + { + public bool RollbackMachineStudio { get; set; } + + public bool RollbackPPC { get; set; } + } +} diff --git a/Software/Visual_Studio/Azure/Tango.AzureUtils/Storage/StorageManager.cs b/Software/Visual_Studio/Azure/Tango.AzureUtils/Storage/StorageManager.cs index 3551bf4f4..77a0aaf6d 100644 --- a/Software/Visual_Studio/Azure/Tango.AzureUtils/Storage/StorageManager.cs +++ b/Software/Visual_Studio/Azure/Tango.AzureUtils/Storage/StorageManager.cs @@ -68,6 +68,14 @@ namespace Tango.AzureUtils.Storage await container.DeleteAsync(); } + public async Task RemoveBlob(String containerName, String blobName) + { + OnProgress(AzureUtilsStage.Storage, $"Removing blob '{blobName}'..."); + var container = _client.GetContainerReference(containerName); + var blob = container.GetBlockBlobReference(blobName); + await blob.DeleteAsync(); + } + public async Task UpgradePPCStorage(IWebAppBase sourceApp, IWebAppBase targetApp) { OnProgress(AzureUtilsStage.Storage, $"Retrieving source and target settings..."); @@ -134,6 +142,32 @@ namespace Tango.AzureUtils.Storage }); } + public async Task DowngradeMachineStudioStorage(IWebAppBase app) + { + OnProgress(AzureUtilsStage.Storage, $"Retrieving settings..."); + + var latestMachineStudioVersion = await _databaseManager.GetLatestMachineStudioVersion(app); + var settings = await app.GetMachineServiceSettingsAsync(); + + await RemoveBlob(settings.MACHINE_STUDIO_VERSIONS_CONTAINER, latestMachineStudioVersion.BlobName); + await RemoveBlob(settings.MACHINE_STUDIO_VERSIONS_CONTAINER, latestMachineStudioVersion.InstallerBlobName); + + OnCompleted("Latest Machine Studio storage blobs removed."); + } + + public async Task DowngradePPCStorage(IWebAppBase app) + { + OnProgress(AzureUtilsStage.Storage, $"Retrieving settings..."); + + var latestPPCVersion = await _databaseManager.GetLatestPPCVersion(app); + var settings = await app.GetMachineServiceSettingsAsync(); + + await RemoveBlob(settings.TANGO_VERSIONS_CONTAINER, latestPPCVersion.BlobName); + await RemoveBlob(settings.TANGO_VERSIONS_CONTAINER, latestPPCVersion.InstallerBlobName); + + OnCompleted("Latest PPC storage blobs removed."); + } + public async Task ValidatePPCStorageUpgrade(IWebAppBase sourceApp, IWebAppBase targetApp) { OnProgress(AzureUtilsStage.Validating, "Validating PPC database upgrade..."); diff --git a/Software/Visual_Studio/Azure/Tango.AzureUtils/Tango.AzureUtils.csproj b/Software/Visual_Studio/Azure/Tango.AzureUtils/Tango.AzureUtils.csproj index 4a8daf233..896c34635 100644 --- a/Software/Visual_Studio/Azure/Tango.AzureUtils/Tango.AzureUtils.csproj +++ b/Software/Visual_Studio/Azure/Tango.AzureUtils/Tango.AzureUtils.csproj @@ -241,6 +241,7 @@ + -- cgit v1.3.1