New: RemotePathMapping HealthCheck

Co-Authored-By: ta264 <ta264@users.noreply.github.com>
pull/4550/head
Qstick 4 years ago
parent ee9d35e55f
commit 79c35fabfa

@ -1,4 +1,4 @@
using FluentAssertions;
using FluentAssertions;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.HealthCheck;
@ -21,7 +21,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
}
}
public static void ShouldBeWarning(this Core.HealthCheck.HealthCheck result, string message = null)
public static void ShouldBeWarning(this Core.HealthCheck.HealthCheck result, string message = null, string wikiFragment = null)
{
result.Type.Should().Be(HealthCheckResult.Warning);
@ -29,9 +29,14 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
{
result.Message.Should().Contain(message);
}
if (wikiFragment.IsNotNullOrWhiteSpace())
{
result.WikiUrl.ToString().Should().Contain(wikiFragment);
}
}
public static void ShouldBeError(this Core.HealthCheck.HealthCheck result, string message = null)
public static void ShouldBeError(this Core.HealthCheck.HealthCheck result, string message = null, string wikiFragment = null)
{
result.Type.Should().Be(HealthCheckResult.Error);
@ -39,6 +44,11 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
{
result.Message.Should().Contain(message);
}
if (wikiFragment.IsNotNullOrWhiteSpace())
{
result.WikiUrl.ToString().Should().Contain(wikiFragment);
}
}
}
}

@ -0,0 +1,260 @@
using System;
using System.Collections.Generic;
using System.IO;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.HealthCheck.Checks
{
[TestFixture]
public class RemotePathMappingCheckFixture : CoreTest<RemotePathMappingCheck>
{
private string _downloadRootPath = @"c:\Test".AsOsAgnostic();
private string _downloadItemPath = @"c:\Test\item".AsOsAgnostic();
private DownloadClientInfo _clientStatus;
private DownloadClientItem _downloadItem;
private Mock<IDownloadClient> _downloadClient;
private static Exception[] DownloadClientExceptions =
{
new DownloadClientUnavailableException("error"),
new DownloadClientAuthenticationException("error"),
new DownloadClientException("error")
};
[SetUp]
public void Setup()
{
_downloadItem = new DownloadClientItem
{
DownloadClientInfo = new DownloadClientItemClientInfo { Name = "Test" },
DownloadId = "TestId",
OutputPath = new OsPath(_downloadItemPath)
};
_clientStatus = new DownloadClientInfo
{
IsLocalhost = true,
OutputRootFolders = new List<OsPath> { new OsPath(_downloadRootPath) }
};
_downloadClient = Mocker.GetMock<IDownloadClient>();
_downloadClient.Setup(s => s.Definition)
.Returns(new DownloadClientDefinition { Name = "Test" });
_downloadClient.Setup(s => s.GetItems())
.Returns(new List<DownloadClientItem> { _downloadItem });
_downloadClient.Setup(s => s.GetStatus())
.Returns(_clientStatus);
Mocker.GetMock<IProvideDownloadClient>()
.Setup(s => s.GetDownloadClients())
.Returns(new IDownloadClient[] { _downloadClient.Object });
Mocker.GetMock<IDiskProvider>()
.Setup(x => x.FolderExists(It.IsAny<string>()))
.Returns((string path) =>
{
Ensure.That(path, () => path).IsValidPath();
return false;
});
Mocker.GetMock<IDiskProvider>()
.Setup(x => x.FileExists(It.IsAny<string>()))
.Returns((string path) =>
{
Ensure.That(path, () => path).IsValidPath();
return false;
});
}
private void GivenFolderExists(string folder)
{
Mocker.GetMock<IDiskProvider>()
.Setup(x => x.FolderExists(folder))
.Returns(true);
}
private void GivenFileExists(string file)
{
Mocker.GetMock<IDiskProvider>()
.Setup(x => x.FileExists(file))
.Returns(true);
}
private void GivenDocker()
{
Mocker.GetMock<IOsInfo>()
.Setup(x => x.IsDocker)
.Returns(true);
}
[Test]
public void should_return_ok_if_setup_correctly()
{
GivenFolderExists(_downloadRootPath);
Subject.Check().ShouldBeOk();
}
[Test]
public void should_return_permissions_error_if_local_client_download_root_missing()
{
Subject.Check().ShouldBeError(wikiFragment: "permissions-error");
}
[Test]
public void should_return_mapping_error_if_remote_client_root_path_invalid()
{
_clientStatus.IsLocalhost = false;
_clientStatus.OutputRootFolders = new List<OsPath> { new OsPath("An invalid path") };
Subject.Check().ShouldBeError(wikiFragment: "bad-remote-path-mapping");
}
[Test]
public void should_return_download_client_error_if_local_client_root_path_invalid()
{
_clientStatus.IsLocalhost = true;
_clientStatus.OutputRootFolders = new List<OsPath> { new OsPath("An invalid path") };
Subject.Check().ShouldBeError(wikiFragment: "bad-download-client-settings");
}
[Test]
public void should_return_path_mapping_error_if_remote_client_download_root_missing()
{
_clientStatus.IsLocalhost = false;
Subject.Check().ShouldBeError(wikiFragment: "bad-remote-path-mapping");
}
[Test]
[TestCaseSource("DownloadClientExceptions")]
public void should_return_ok_if_client_throws_downloadclientexception(Exception ex)
{
_downloadClient.Setup(s => s.GetStatus())
.Throws(ex);
Subject.Check().ShouldBeOk();
ExceptionVerification.ExpectedErrors(0);
}
[Test]
public void should_return_docker_path_mapping_error_if_on_docker_and_root_missing()
{
GivenDocker();
Subject.Check().ShouldBeError(wikiFragment: "docker-bad-remote-path-mapping");
}
[Test]
public void should_return_ok_on_movie_imported_event()
{
GivenFolderExists(_downloadRootPath);
var importEvent = new MovieImportedEvent(new LocalMovie(), new MovieFile(), true, new DownloadClientItem(), _downloadItem.DownloadId);
Subject.Check(importEvent).ShouldBeOk();
}
[Test]
public void should_return_permissions_error_on_movie_import_failed_event_if_file_exists()
{
var localMovie = new LocalMovie
{
Path = Path.Combine(_downloadItemPath, "file.mkv")
};
GivenFileExists(localMovie.Path);
var importEvent = new MovieImportFailedEvent(new Exception(), localMovie, true, new DownloadClientItem());
Subject.Check(importEvent).ShouldBeError(wikiFragment: "permissions-error");
}
[Test]
public void should_return_permissions_error_on_movie_import_failed_event_if_folder_exists()
{
GivenFolderExists(_downloadItemPath);
var importEvent = new MovieImportFailedEvent(null, null, true, _downloadItem);
Subject.Check(importEvent).ShouldBeError(wikiFragment: "permissions-error");
}
[Test]
public void should_return_permissions_error_on_movie_import_failed_event_for_local_client_if_folder_does_not_exist()
{
var importEvent = new MovieImportFailedEvent(null, null, true, _downloadItem);
Subject.Check(importEvent).ShouldBeError(wikiFragment: "permissions-error");
}
[Test]
public void should_return_mapping_error_on_movie_import_failed_event_for_remote_client_if_folder_does_not_exist()
{
_clientStatus.IsLocalhost = false;
var importEvent = new MovieImportFailedEvent(null, null, true, _downloadItem);
Subject.Check(importEvent).ShouldBeError(wikiFragment: "bad-remote-path-mapping");
}
[Test]
public void should_return_mapping_error_on_movie_import_failed_event_for_remote_client_if_path_invalid()
{
_clientStatus.IsLocalhost = false;
_downloadItem.OutputPath = new OsPath("an invalid path");
var importEvent = new MovieImportFailedEvent(null, null, true, _downloadItem);
Subject.Check(importEvent).ShouldBeError(wikiFragment: "bad-remote-path-mapping");
}
[Test]
public void should_return_download_client_error_on_movie_import_failed_event_for_remote_client_if_path_invalid()
{
_clientStatus.IsLocalhost = true;
_downloadItem.OutputPath = new OsPath("an invalid path");
var importEvent = new MovieImportFailedEvent(null, null, true, _downloadItem);
Subject.Check(importEvent).ShouldBeError(wikiFragment: "bad-download-client-settings");
}
[Test]
public void should_return_docker_mapping_error_on_movie_import_failed_event_inside_docker_if_folder_does_not_exist()
{
GivenDocker();
_clientStatus.IsLocalhost = false;
var importEvent = new MovieImportFailedEvent(null, null, true, _downloadItem);
Subject.Check(importEvent).ShouldBeError(wikiFragment: "docker-bad-remote-path-mapping");
}
[Test]
[TestCaseSource("DownloadClientExceptions")]
public void should_return_ok_on_import_failed_event_if_client_throws_downloadclientexception(Exception ex)
{
_downloadClient.Setup(s => s.GetStatus())
.Throws(ex);
var importEvent = new MovieImportFailedEvent(null, null, true, _downloadItem);
Subject.Check(importEvent).ShouldBeOk();
ExceptionVerification.ExpectedErrors(0);
}
}
}

@ -0,0 +1,192 @@
using System;
using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Messaging;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(ProviderAddedEvent<IDownloadClient>))]
[CheckOn(typeof(ProviderUpdatedEvent<IDownloadClient>))]
[CheckOn(typeof(ProviderDeletedEvent<IDownloadClient>))]
[CheckOn(typeof(ModelEvent<RemotePathMapping>))]
[CheckOn(typeof(MovieImportedEvent), CheckOnCondition.FailedOnly)]
[CheckOn(typeof(MovieImportFailedEvent), CheckOnCondition.SuccessfulOnly)]
public class RemotePathMappingCheck : HealthCheckBase, IProvideHealthCheck
{
private readonly IDiskProvider _diskProvider;
private readonly IProvideDownloadClient _downloadClientProvider;
private readonly Logger _logger;
private readonly IOsInfo _osInfo;
public RemotePathMappingCheck(IDiskProvider diskProvider,
IProvideDownloadClient downloadClientProvider,
IOsInfo osInfo,
Logger logger)
{
_diskProvider = diskProvider;
_downloadClientProvider = downloadClientProvider;
_logger = logger;
_osInfo = osInfo;
}
public override HealthCheck Check()
{
var clients = _downloadClientProvider.GetDownloadClients();
foreach (var client in clients)
{
try
{
var status = client.GetStatus();
var folders = status.OutputRootFolders;
if (folders != null)
{
foreach (var folder in folders)
{
if (!folder.IsValid)
{
if (!status.IsLocalhost)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Remote download client {client.Definition.Name} places downloads in {folder.FullPath} but this is not a valid {_osInfo.Name} path. Review your remote path mappings and download client settings.", "#bad-remote-path-mapping");
}
else if (_osInfo.IsDocker)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, $"You are using docker; download client {client.Definition.Name} places downloads in {folder.FullPath} but this is not a valid {_osInfo.Name} path. Review your remote path mappings and download client settings.", "#docker-bad-remote-path-mapping");
}
else
{
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Local download client {client.Definition.Name} places downloads in {folder.FullPath} but this is not a valid {_osInfo.Name} path. Review your download client settings.", "#bad-download-client-settings");
}
}
if (!_diskProvider.FolderExists(folder.FullPath))
{
if (_osInfo.IsDocker)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, $"You are using docker; download client {client.Definition.Name} places downloads in {folder.FullPath} but this directory does not appear to exist inside the container. Review your remote path mappings and container volume settings.", "#docker-bad-remote-path-mapping");
}
else if (!status.IsLocalhost)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Remote download client {client.Definition.Name} places downloads in {folder.FullPath} but this directory does not appear to exist. Likely missing or incorrect remote path mapping.", "#bad-remote-path-mapping");
}
else
{
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Download client {client.Definition.Name} places downloads in {folder.FullPath} but Radarr cannot see this directory. You may need to adjust the folder's permissions.", "#permissions-error");
}
}
}
}
}
catch (DownloadClientException ex)
{
_logger.Debug(ex, "Unable to communicate with {0}", client.Definition.Name);
}
catch (Exception ex)
{
_logger.Error(ex, "Unknown error occured in RemotePathMapping HealthCheck");
}
}
return new HealthCheck(GetType());
}
public HealthCheck Check(IEvent message)
{
if (typeof(MovieImportFailedEvent).IsAssignableFrom(message.GetType()))
{
var failureMessage = (MovieImportFailedEvent)message;
// if we can see the file exists but the import failed then likely a permissions issue
if (failureMessage.MovieInfo != null)
{
var moviePath = failureMessage.MovieInfo.Path;
if (_diskProvider.FileExists(moviePath))
{
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Radarr can see but not access downloaded movie {moviePath}. Likely permissions error.", "#permissions-error");
}
else
{
// If the file doesn't exist but TrackInfo is not null then the message is coming from
// ImportApprovedTracks and the file must have been removed part way through processing
return new HealthCheck(GetType(), HealthCheckResult.Error, $"File {moviePath} was removed part way though procesing.");
}
}
// If the previous case did not match then the failure occured in DownloadedTracksImportService,
// while trying to locate the files reported by the download client
var client = _downloadClientProvider.GetDownloadClients().FirstOrDefault(x => x.Definition.Name == failureMessage.DownloadClientInfo.Name);
try
{
var status = client.GetStatus();
var dlpath = client?.GetItems().FirstOrDefault(x => x.DownloadId == failureMessage.DownloadId)?.OutputPath.FullPath;
// If dlpath is null then there's not much useful we can report. Give a generic message so
// that the user realises something is wrong.
if (dlpath.IsNullOrWhiteSpace())
{
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Radarr failed to import a movie. Check your logs for details.");
}
if (!dlpath.IsPathValid())
{
if (!status.IsLocalhost)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Remote download client {client.Definition.Name} reported files in {dlpath} but this is not a valid {_osInfo.Name} path. Review your remote path mappings and download client settings.", "#bad-remote-path-mapping");
}
else if (_osInfo.IsDocker)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, $"You are using docker; download client {client.Definition.Name} reported files in {dlpath} but this is not a valid {_osInfo.Name} path. Review your remote path mappings and download client settings.", "#docker-bad-remote-path-mapping");
}
else
{
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Local download client {client.Definition.Name} reported files in {dlpath} but this is not a valid {_osInfo.Name} path. Review your download client settings.", "#bad-download-client-settings");
}
}
if (_diskProvider.FolderExists(dlpath))
{
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Radarr can see but not access download directory {dlpath}. Likely permissions error.", "#permissions-error");
}
// if it's a remote client/docker, likely missing path mappings
if (_osInfo.IsDocker)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, $"You are using docker; download client {client.Definition.Name} reported files in {dlpath} but this directory does not appear to exist inside the container. Review your remote path mappings and container volume settings.", "#docker-bad-remote-path-mapping");
}
else if (!status.IsLocalhost)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Remote download client {client.Definition.Name} reported files in {dlpath} but this directory does not appear to exist. Likely missing remote path mapping.", "#bad-remote-path-mapping");
}
else
{
// path mappings shouldn't be needed locally so probably a permissions issue
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Download client {client.Definition.Name} reported files in {dlpath} but Radarr cannot see this directory. You may need to adjust the folder's permissions.", "#permissions-error");
}
}
catch (DownloadClientException ex)
{
_logger.Debug(ex, "Unable to communicate with {0}", client.Definition.Name);
}
catch (Exception ex)
{
_logger.Error(ex, "Unknown error occured in RemotePathMapping HealthCheck");
}
return new HealthCheck(GetType());
}
else
{
return Check();
}
}
}
}
Loading…
Cancel
Save