public async Task PutSettings()
        {
            await ServerStore.EnsureNotPassiveAsync();

            using (ServerStore.ContextPool.AllocateOperationContext(out TransactionOperationContext context))
            {
                var databaseSettingsJson = context.ReadForDisk(RequestBodyStream(), Constants.DatabaseSettings.StudioId);

                Dictionary <string, string> settings = new Dictionary <string, string>();
                var prop = new BlittableJsonReaderObject.PropertyDetails();

                for (int i = 0; i < databaseSettingsJson.Count; i++)
                {
                    databaseSettingsJson.GetPropertyByIndex(i, ref prop);
                    settings.Add(prop.Name, prop.Value?.ToString() ?? null);
                }

                await UpdateDatabaseRecord(context, (record, _) =>
                {
                    record.Settings = settings;
                }, GetRaftRequestIdFromQuery());
            }

            NoContentStatus();
            HttpContext.Response.StatusCode = (int)HttpStatusCode.Created;
        }
Exemple #2
0
        public async Task MigrateDatabases(List <DatabaseMigrationSettings> databases)
        {
            await UpdateBuildInfoIfNeeded();

            if (databases == null || databases.Count == 0)
            {
                // migrate all databases
                var authorized    = new Reference <bool>();
                var databaseNames = await GetDatabaseNames(_buildMajorVersion, authorized);

                if (authorized.Value == false)
                {
                    throw new InvalidOperationException($"You are not authorized to fetch " +
                                                        $"the database names for server: {_serverUrl}");
                }

                if (databases == null)
                {
                    databases = new List <DatabaseMigrationSettings>();
                }

                var operateOnTypes = DatabaseSmugglerOptions.DefaultOperateOnTypes;
                if (_buildMajorVersion != MajorVersion.V4 && _buildMajorVersion != MajorVersion.V5 && _buildMajorVersion != MajorVersion.GreaterThanCurrent)
                {
                    operateOnTypes |= DatabaseItemType.LegacyAttachments;
                }

                foreach (var databaseName in databaseNames)
                {
                    databases.Add(new DatabaseMigrationSettings
                    {
                        DatabaseName    = databaseName,
                        OperateOnTypes  = operateOnTypes,
                        RemoveAnalyzers = false
                    });
                }
            }

            if (databases.Count == 0)
            {
                throw new InvalidOperationException("Found no databases to migrate");
            }

            await _serverStore.EnsureNotPassiveAsync();

            foreach (var databaseToMigrate in databases)
            {
                await CreateDatabaseIfNeeded(databaseToMigrate.DatabaseName);

                var database = await GetDatabase(databaseToMigrate.DatabaseName);

                if (database == null)
                {
                    // database doesn't exist
                    continue;
                }

                StartMigratingSingleDatabase(databaseToMigrate, database);
            }
        }
Exemple #3
0
        public async Task DeleteNode()
        {
            var nodeTag = GetStringQueryString("nodeTag");
            await ServerStore.EnsureNotPassiveAsync();

            if (ServerStore.IsLeader())
            {
                if (nodeTag == ServerStore.Engine.Tag)
                {
                    // cannot remove the leader, let's change the leader
                    ServerStore.Engine.CurrentLeader?.StepDown();
                    await ServerStore.Engine.WaitForState(RachisState.Follower, HttpContext.RequestAborted);

                    RedirectToLeader();
                    return;
                }

                await ServerStore.RemoveFromClusterAsync(nodeTag);

                NoContentStatus();
                return;
            }

            RedirectToLeader();
        }
Exemple #4
0
        public async Task DeleteCompareExchangeValue()
        {
            var key           = GetStringQueryString("key");
            var raftRequestId = GetRaftRequestIdFromQuery();

            // ReSharper disable once PossibleInvalidOperationException
            var index = GetLongQueryString("index", true).Value;

            await ServerStore.EnsureNotPassiveAsync();

            using (ServerStore.ContextPool.AllocateOperationContext(out TransactionOperationContext context))
            {
                var command = new RemoveCompareExchangeCommand(Database.Name, key, index, context, raftRequestId);
                using (var writer = new BlittableJsonTextWriter(context, ResponseBodyStream()))
                {
                    (var raftIndex, var response) = await ServerStore.SendToLeaderAsync(context, command);

                    await ServerStore.Cluster.WaitForIndexNotification(raftIndex);

                    var result = (CompareExchangeCommandBase.CompareExchangeResult)response;
                    context.Write(writer, new DynamicJsonValue
                    {
                        [nameof(CompareExchangeResult <object> .Index)]      = result.Index,
                        [nameof(CompareExchangeResult <object> .Value)]      = result.Value,
                        [nameof(CompareExchangeResult <object> .Successful)] = result.Index == raftIndex
                    });
                }
            }
        }
        public async Task DistributeKeyInCluster()
        {
            await ServerStore.EnsureNotPassiveAsync();

            var name  = GetStringQueryString("name");
            var nodes = GetStringValuesQueryString("node");

            using (var reader = new StreamReader(HttpContext.Request.Body))
            {
                var base64 = await reader.ReadToEndAsync();

                using (Server.ServerStore.ContextPool.AllocateOperationContext(out TransactionOperationContext ctx))
                {
                    ClusterTopology clusterTopology;
                    using (ctx.OpenReadTransaction())
                        clusterTopology = ServerStore.GetClusterTopology(ctx);

                    foreach (var node in nodes)
                    {
                        if (string.IsNullOrEmpty(node))
                        {
                            continue;
                        }

                        if (string.Equals(node, "?") || string.Equals(node, ServerStore.NodeTag, StringComparison.OrdinalIgnoreCase))
                        {
                            var key = Convert.FromBase64String(base64);

                            if (key.Length != 256 / 8)
                            {
                                throw new ArgumentException($"Key size must be 256 bits, but was {key.Length * 8}", nameof(key));
                            }

                            StoreKeyLocally(name, key, ctx);
                        }
                        else
                        {
                            var url = clusterTopology.GetUrlFromTag(node);
                            if (url == null)
                            {
                                throw new InvalidOperationException($"Node {node} is not a part of the cluster, cannot send secret key.");
                            }

                            if (url.StartsWith("https:", StringComparison.OrdinalIgnoreCase) == false)
                            {
                                throw new InvalidOperationException($"Cannot put secret key for {name} on node {node} with url {url} because it is not using HTTPS");
                            }

                            await SendKeyToNodeAsync(name, base64, ctx, ServerStore, node, url).ConfigureAwait(false);
                        }
                    }
                }
            }

            HttpContext.Response.StatusCode = (int)HttpStatusCode.Created;
        }
        public async Task AddTimeSeriesPolicy()
        {
            await ServerStore.EnsureNotPassiveAsync();

            var collection = GetStringQueryString("collection", required: true);

            using (ServerStore.ContextPool.AllocateOperationContext(out TransactionOperationContext context))
                using (var json = context.ReadForDisk(RequestBodyStream(), "time-series policy config"))
                {
                    var policy = JsonDeserializationCluster.TimeSeriesPolicy(json);

                    TimeSeriesConfiguration current;
                    using (context.OpenReadTransaction())
                    {
                        current = ServerStore.Cluster.ReadRawDatabaseRecord(context, Database.Name).TimeSeriesConfiguration ?? new TimeSeriesConfiguration();
                    }

                    current.Collections ??= new Dictionary <string, TimeSeriesCollectionConfiguration>(StringComparer.OrdinalIgnoreCase);

                    if (current.Collections.ContainsKey(collection) == false)
                    {
                        current.Collections[collection] = new TimeSeriesCollectionConfiguration();
                    }

                    if (RawTimeSeriesPolicy.IsRaw(policy))
                    {
                        current.Collections[collection].RawPolicy = new RawTimeSeriesPolicy(policy.RetentionTime);
                    }
                    else
                    {
                        current.Collections[collection].Policies ??= new List <TimeSeriesPolicy>();
                        var existing = current.Collections[collection].GetPolicyByName(policy.Name, out _);
                        if (existing != null)
                        {
                            current.Collections[collection].Policies.Remove(existing);
                        }

                        current.Collections[collection].Policies.Add(policy);
                    }

                    current.InitializeRollupAndRetention();

                    ServerStore.LicenseManager.AssertCanAddTimeSeriesRollupsAndRetention(current);

                    var editTimeSeries = new EditTimeSeriesConfigurationCommand(current, Database.Name, GetRaftRequestIdFromQuery());
                    var(index, _) = await ServerStore.SendToLeaderAsync(editTimeSeries);

                    await WaitForIndexToBeApplied(context, index);

                    SendConfigurationResponse(context, index);
                }
        }
        public async Task PutClientConfiguration()
        {
            await ServerStore.EnsureNotPassiveAsync();

            using (ServerStore.ContextPool.AllocateOperationContext(out TransactionOperationContext ctx))
            {
                var clientConfigurationJson = await ctx.ReadForMemoryAsync(RequestBodyStream(), Constants.Configuration.ClientId);

                var clientConfiguration = JsonDeserializationServer.ClientConfiguration(clientConfigurationJson);
                var res = await ServerStore.PutValueInClusterAsync(new PutClientConfigurationCommand(clientConfiguration, GetRaftRequestIdFromQuery()));

                await ServerStore.Cluster.WaitForIndexNotification(res.Index);

                NoContentStatus(HttpStatusCode.Created);
            }
        }
        public async Task ConfigTimeSeriesNames()
        {
            await ServerStore.EnsureNotPassiveAsync();

            using (ServerStore.ContextPool.AllocateOperationContext(out TransactionOperationContext context))
                using (var json = context.ReadForDisk(RequestBodyStream(), "time-series value names"))
                {
                    var parameters = JsonDeserializationServer.Parameters.TimeSeriesValueNamesParameters(json);
                    parameters.Validate();

                    TimeSeriesConfiguration current;
                    using (context.OpenReadTransaction())
                    {
                        current = ServerStore.Cluster.ReadRawDatabaseRecord(context, Database.Name).TimeSeriesConfiguration ?? new TimeSeriesConfiguration();
                    }

                    if (current.NamedValues == null)
                    {
                        current.AddValueName(parameters.Collection, parameters.TimeSeries, parameters.ValueNames);
                    }
                    else
                    {
                        var currentNames = current.GetNames(parameters.Collection, parameters.TimeSeries);
                        if (currentNames?.SequenceEqual(parameters.ValueNames, StringComparer.Ordinal) == true)
                        {
                            return; // no need to update, they identical
                        }
                        if (parameters.Update == false)
                        {
                            if (current.TryAddValueName(parameters.Collection, parameters.TimeSeries, parameters.ValueNames) == false)
                            {
                                throw new InvalidOperationException(
                                          $"Failed to update the names for time-series '{parameters.TimeSeries}' in collection '{parameters.Collection}', they already exists.");
                            }
                        }
                        current.AddValueName(parameters.Collection, parameters.TimeSeries, parameters.ValueNames);
                    }
                    var editTimeSeries = new EditTimeSeriesConfigurationCommand(current, Database.Name, GetRaftRequestIdFromQuery());
                    var(index, _) = await ServerStore.SendToLeaderAsync(editTimeSeries);

                    await WaitForIndexToBeApplied(context, index);

                    SendConfigurationResponse(context, index);
                }
        }
        public async Task RemoveTimeSeriesPolicy()
        {
            await ServerStore.EnsureNotPassiveAsync();

            var collection = GetStringQueryString("collection", required: true);
            var name       = GetStringQueryString("name", required: true);

            using (ServerStore.ContextPool.AllocateOperationContext(out TransactionOperationContext context))
            {
                TimeSeriesConfiguration current;
                using (context.OpenReadTransaction())
                {
                    current = ServerStore.Cluster.ReadRawDatabaseRecord(context, Database.Name).TimeSeriesConfiguration;
                }

                if (current?.Collections?.ContainsKey(collection) == true)
                {
                    var p = current.Collections[collection].GetPolicyByName(name, out _);
                    if (p == null)
                    {
                        return;
                    }

                    if (ReferenceEquals(p, current.Collections[collection].RawPolicy))
                    {
                        current.Collections[collection].RawPolicy = RawTimeSeriesPolicy.Default;
                    }
                    else
                    {
                        current.Collections[collection].Policies.Remove(p);
                    }

                    current.InitializeRollupAndRetention();

                    ServerStore.LicenseManager.AssertCanAddTimeSeriesRollupsAndRetention(current);

                    var editTimeSeries = new EditTimeSeriesConfigurationCommand(current, Database.Name, GetRaftRequestIdFromQuery());
                    var(index, _) = await ServerStore.SendToLeaderAsync(editTimeSeries);

                    await WaitForIndexToBeApplied(context, index);

                    SendConfigurationResponse(context, index);
                }
            }
        }
        public async Task PutStudioConfiguration()
        {
            await ServerStore.EnsureNotPassiveAsync();

            using (ServerStore.ContextPool.AllocateOperationContext(out TransactionOperationContext context))
            {
                var studioConfigurationJson = context.ReadForDisk(RequestBodyStream(), Constants.Configuration.StudioId);
                var studioConfiguration     = JsonDeserializationServer.StudioConfiguration(studioConfigurationJson);

                await UpdateDatabaseRecord(context, (record, _) =>
                {
                    record.Studio = studioConfiguration;
                }, GetRaftRequestIdFromQuery());
            }

            NoContentStatus();
            HttpContext.Response.StatusCode = (int)HttpStatusCode.Created;
        }
        public async Task PutStudioConfiguration()
        {
            await ServerStore.EnsureNotPassiveAsync();

            using (ServerStore.ContextPool.AllocateOperationContext(out TransactionOperationContext ctx))
            {
                var studioConfigurationJson = await ctx.ReadForDiskAsync(RequestBodyStream(), Constants.Configuration.StudioId);

                var studioConfiguration = JsonDeserializationServer.ServerWideStudioConfiguration(studioConfigurationJson);

                var res = await ServerStore.PutValueInClusterAsync(new PutServerWideStudioConfigurationCommand(studioConfiguration, GetRaftRequestIdFromQuery()));

                await ServerStore.Cluster.WaitForIndexNotification(res.Index);

                NoContentStatus();

                HttpContext.Response.StatusCode = (int)HttpStatusCode.Created;
            }
        }
Exemple #12
0
        protected async Task DatabaseConfigurations(SetupFunc setupConfigurationFunc,
                                                    string debug,
                                                    string raftRequestId,
                                                    RefAction beforeSetupConfiguration = null,
                                                    Action <DynamicJsonValue, BlittableJsonReaderObject, long> fillJson = null,
                                                    HttpStatusCode statusCode = HttpStatusCode.OK)
        {
            if (TryGetAllowedDbs(Database.Name, out var _, requireAdmin: true) == false)
            {
                return;
            }

            if (ResourceNameValidator.IsValidResourceName(Database.Name, ServerStore.Configuration.Core.DataDirectory.FullPath, out string errorMessage) == false)
            {
                throw new BadRequestException(errorMessage);
            }

            await ServerStore.EnsureNotPassiveAsync();

            using (ServerStore.ContextPool.AllocateOperationContext(out TransactionOperationContext context))
            {
                var configurationJson = await context.ReadForMemoryAsync(RequestBodyStream(), debug);

                beforeSetupConfiguration?.Invoke(Database.Name, ref configurationJson, context);

                var(index, _) = await setupConfigurationFunc(context, Database.Name, configurationJson, raftRequestId);
                await WaitForIndexToBeApplied(context, index);

                HttpContext.Response.StatusCode = (int)statusCode;

                using (var writer = new BlittableJsonTextWriter(context, ResponseBodyStream()))
                {
                    var json = new DynamicJsonValue
                    {
                        ["RaftCommandIndex"] = index,
                    };
                    fillJson?.Invoke(json, configurationJson, index);
                    context.Write(writer, json);
                    writer.Flush();
                }
            }
        }
        public async Task PutClientConfiguration()
        {
            await ServerStore.EnsureNotPassiveAsync();

            using (ServerStore.ContextPool.AllocateOperationContext(out TransactionOperationContext context))
            {
                var clientConfigurationJson = context.ReadForDisk(RequestBodyStream(), Constants.Configuration.ClientId);
                var clientConfiguration     = JsonDeserializationServer.ClientConfiguration(clientConfigurationJson);

                await UpdateDatabaseRecord(context, (record, index) =>
                {
                    record.Client      = clientConfiguration;
                    record.Client.Etag = index;
                }, GetRaftRequestIdFromQuery());
            }

            NoContentStatus();
            HttpContext.Response.Headers[Constants.Headers.RefreshClientConfiguration] = "true";
            HttpContext.Response.StatusCode = (int)HttpStatusCode.Created;
        }
Exemple #14
0
        public async Task Activate()
        {
            if (ServerStore.Configuration.Licensing.CanActivate == false)
            {
                HttpContext.Response.StatusCode = (int)HttpStatusCode.MethodNotAllowed;
                return;
            }

            License license;

            using (ServerStore.ContextPool.AllocateOperationContext(out TransactionOperationContext context))
            {
                var json = context.Read(RequestBodyStream(), "license activation");
                license = JsonDeserializationServer.License(json);
            }

            await ServerStore.EnsureNotPassiveAsync(skipLicenseActivation : true);

            await ServerStore.LicenseManager.Activate(license, GetRaftRequestIdFromQuery());

            NoContentStatus();
        }
Exemple #15
0
        protected RestoreBackupTaskBase(ServerStore serverStore,
                                        RestoreBackupConfigurationBase restoreFromConfiguration,
                                        string nodeTag,
                                        OperationCancelToken operationCancelToken)
        {
            _serverStore             = serverStore;
            RestoreFromConfiguration = restoreFromConfiguration;
            _nodeTag = nodeTag;
            _operationCancelToken = operationCancelToken;

            var dataDirectoryThatWillBeUsed = string.IsNullOrWhiteSpace(RestoreFromConfiguration.DataDirectory) ?
                                              _serverStore.Configuration.Core.DataDirectory.FullPath :
                                              new PathSetting(RestoreFromConfiguration.DataDirectory, _serverStore.Configuration.Core.DataDirectory.FullPath).FullPath;

            if (ResourceNameValidator.IsValidResourceName(RestoreFromConfiguration.DatabaseName, dataDirectoryThatWillBeUsed, out string errorMessage) == false)
            {
                throw new InvalidOperationException(errorMessage);
            }

            _serverStore.EnsureNotPassiveAsync().Wait(_operationCancelToken.Token);

            ClusterTopology clusterTopology;

            using (_serverStore.ContextPool.AllocateOperationContext(out TransactionOperationContext context))
                using (context.OpenReadTransaction())
                {
                    if (_serverStore.Cluster.DatabaseExists(context, RestoreFromConfiguration.DatabaseName))
                    {
                        throw new ArgumentException($"Cannot restore data to an existing database named {RestoreFromConfiguration.DatabaseName}");
                    }

                    clusterTopology = _serverStore.GetClusterTopology(context);
                }

            _hasEncryptionKey = string.IsNullOrWhiteSpace(RestoreFromConfiguration.EncryptionKey) == false;
            if (_hasEncryptionKey)
            {
                var key = Convert.FromBase64String(RestoreFromConfiguration.EncryptionKey);
                if (key.Length != 256 / 8)
                {
                    throw new InvalidOperationException($"The size of the key must be 256 bits, but was {key.Length * 8} bits.");
                }

                if (AdminDatabasesHandler.NotUsingHttps(clusterTopology.GetUrlFromTag(_serverStore.NodeTag)))
                {
                    throw new InvalidOperationException("Cannot restore an encrypted database to a node which doesn't support SSL!");
                }
            }

            var backupEncryptionSettings = RestoreFromConfiguration.BackupEncryptionSettings;

            if (backupEncryptionSettings != null)
            {
                if (backupEncryptionSettings.EncryptionMode == EncryptionMode.UseProvidedKey &&
                    backupEncryptionSettings.Key == null)
                {
                    throw new InvalidOperationException($"{nameof(BackupEncryptionSettings.EncryptionMode)} is set to {nameof(EncryptionMode.UseProvidedKey)} but an encryption key wasn't provided");
                }

                if (backupEncryptionSettings.EncryptionMode != EncryptionMode.UseProvidedKey &&
                    backupEncryptionSettings.Key != null)
                {
                    throw new InvalidOperationException($"{nameof(BackupEncryptionSettings.EncryptionMode)} is set to {backupEncryptionSettings.EncryptionMode} but an encryption key was provided");
                }
            }

            var hasRestoreDataDirectory = string.IsNullOrWhiteSpace(RestoreFromConfiguration.DataDirectory) == false;

            if (hasRestoreDataDirectory &&
                HasFilesOrDirectories(dataDirectoryThatWillBeUsed))
            {
                throw new ArgumentException("New data directory must be empty of any files or folders, " +
                                            $"path: {dataDirectoryThatWillBeUsed}");
            }

            if (hasRestoreDataDirectory == false)
            {
                RestoreFromConfiguration.DataDirectory = GetDataDirectory();
            }

            _restoringToDefaultDataDirectory = IsDefaultDataDirectory(RestoreFromConfiguration.DataDirectory, RestoreFromConfiguration.DatabaseName);
        }
Exemple #16
0
        public async Task Bootstrap()
        {
            await ServerStore.EnsureNotPassiveAsync();

            NoContentStatus();
        }
Exemple #17
0
        public async Task SetupUnsecured()
        {
            AssertOnlyInSetupMode();

            using (ServerStore.Engine.ContextPool.AllocateOperationContext(out ClusterOperationContext context))
            using (var setupInfoJson = context.ReadForMemory(RequestBodyStream(), "setup-unsecured"))
            {
                // Making sure we don't have leftovers from previous setup
                try
                {
                    using (var tx = context.OpenWriteTransaction())
                    {
                        ServerStore.Engine.DeleteTopology(context);
                        tx.Commit();
                    }
                }
                catch (Exception)
                {
                    // ignored
                }

                var setupInfo = JsonDeserializationServer.UnsecuredSetupInfo(setupInfoJson);

                BlittableJsonReaderObject settingsJson;
                using (var fs = new FileStream(ServerStore.Configuration.ConfigPath, FileMode.Open, FileAccess.Read))
                {
                    settingsJson = context.ReadForMemory(fs, "settings-json");
                }

                settingsJson.Modifications = new DynamicJsonValue(settingsJson)
                {
                    [RavenConfiguration.GetKey(x => x.Licensing.EulaAccepted)] = true,
                    [RavenConfiguration.GetKey(x => x.Core.SetupMode)] = nameof(SetupMode.Unsecured),
                    [RavenConfiguration.GetKey(x => x.Security.UnsecuredAccessAllowed)] = nameof(UnsecuredAccessAddressRange.PublicNetwork)
                };

                if (setupInfo.Port == 0)
                    setupInfo.Port = 8080;

                settingsJson.Modifications[RavenConfiguration.GetKey(x => x.Core.ServerUrls)] = string.Join(";", setupInfo.Addresses.Select(ip => IpAddressToUrl(ip, setupInfo.Port)));

                if (setupInfo.TcpPort == 0)
                    setupInfo.TcpPort = 38888;

                settingsJson.Modifications[RavenConfiguration.GetKey(x => x.Core.TcpServerUrls)] = string.Join(";", setupInfo.Addresses.Select(ip => IpAddressToUrl(ip, setupInfo.TcpPort, "tcp")));

                if (setupInfo.EnableExperimentalFeatures)
                {
                    settingsJson.Modifications[RavenConfiguration.GetKey(x => x.Core.FeaturesAvailability)] = FeaturesAvailability.Experimental;
                }

                if (!string.IsNullOrEmpty(setupInfo.LocalNodeTag))
                {
                    await ServerStore.EnsureNotPassiveAsync(nodeTag: setupInfo.LocalNodeTag);
                }

                if (setupInfo.Environment != StudioConfiguration.StudioEnvironment.None)
                {
                    var res = await ServerStore.PutValueInClusterAsync(
                        new PutServerWideStudioConfigurationCommand(new ServerWideStudioConfiguration { Disabled = false, Environment = setupInfo.Environment },
                            RaftIdGenerator.DontCareId));
                    await ServerStore.Cluster.WaitForIndexNotification(res.Index);
                }

                var modifiedJsonObj = context.ReadObject(settingsJson, "modified-settings-json");

                var indentedJson = SetupManager.IndentJsonString(modifiedJsonObj.ToString());
                SetupManager.WriteSettingsJsonLocally(ServerStore.Configuration.ConfigPath, indentedJson);
            }

            NoContentStatus();
        }
Exemple #18
0
        public async Task AddNode()
        {
            var nodeUrl       = GetQueryStringValueAndAssertIfSingleAndNotEmpty("url");
            var tag           = GetStringQueryString("tag", false);
            var watcher       = GetBoolValueQueryString("watcher", false);
            var raftRequestId = GetRaftRequestIdFromQuery();

            var maxUtilizedCores = GetIntValueQueryString("maxUtilizedCores", false);

            if (maxUtilizedCores != null && maxUtilizedCores <= 0)
            {
                throw new ArgumentException("Max utilized cores cores must be greater than 0");
            }

            nodeUrl = nodeUrl.Trim();
            if (Uri.IsWellFormedUriString(nodeUrl, UriKind.Absolute) == false)
            {
                throw new InvalidOperationException($"Given node URL '{nodeUrl}' is not in a correct format.");
            }

            nodeUrl = UrlHelper.TryGetLeftPart(nodeUrl);
            var remoteIsHttps = nodeUrl.StartsWith("https:", StringComparison.OrdinalIgnoreCase);

            if (HttpContext.Request.IsHttps != remoteIsHttps)
            {
                throw new InvalidOperationException($"Cannot add node '{nodeUrl}' to cluster because it will create invalid mix of HTTPS & HTTP endpoints. A cluster must be only HTTPS or only HTTP.");
            }

            tag = tag?.Trim();

            NodeInfo nodeInfo;

            using (ServerStore.ContextPool.AllocateOperationContext(out TransactionOperationContext ctx))
                using (var requestExecutor = ClusterRequestExecutor.CreateForSingleNode(nodeUrl, Server.Certificate.Certificate))
                {
                    requestExecutor.DefaultTimeout = ServerStore.Engine.OperationTimeout;

                    // test connection to remote.
                    var result = await ServerStore.TestConnectionToRemote(nodeUrl, database : null);

                    if (result.Success == false)
                    {
                        throw new InvalidOperationException(result.Error);
                    }

                    // test connection from remote to destination
                    result = await ServerStore.TestConnectionFromRemote(requestExecutor, ctx, nodeUrl);

                    if (result.Success == false)
                    {
                        throw new InvalidOperationException(result.Error);
                    }

                    var infoCmd = new GetNodeInfoCommand();
                    try
                    {
                        await requestExecutor.ExecuteAsync(infoCmd, ctx);
                    }
                    catch (AllTopologyNodesDownException e)
                    {
                        throw new InvalidOperationException($"Couldn't contact node at {nodeUrl}", e);
                    }

                    nodeInfo = infoCmd.Result;

                    if (SchemaUpgrader.CurrentVersion.ServerVersion != nodeInfo.ServerSchemaVersion)
                    {
                        var nodesVersion = nodeInfo.ServerSchemaVersion == 0 ? "Pre 4.2 version" : nodeInfo.ServerSchemaVersion.ToString();
                        throw new InvalidOperationException($"Can't add node with mismatched storage schema version.{Environment.NewLine}" +
                                                            $"My version is {SchemaUpgrader.CurrentVersion.ServerVersion}, while node's version is {nodesVersion}");
                    }

                    if (ServerStore.IsPassive() && nodeInfo.TopologyId != null)
                    {
                        throw new TopologyMismatchException("You can't add new node to an already existing cluster");
                    }
                }

            if (ServerStore.ValidateFixedPort && nodeInfo.HasFixedPort == false)
            {
                throw new InvalidOperationException($"Failed to add node '{nodeUrl}' to cluster. " +
                                                    $"Node '{nodeUrl}' has port '0' in 'Configuration.Core.ServerUrls' setting. " +
                                                    "Adding a node with non fixed port is forbidden. Define a fixed port for the node to enable cluster creation.");
            }

            await ServerStore.EnsureNotPassiveAsync();

            ServerStore.LicenseManager.AssertCanAddNode();

            if (ServerStore.IsLeader())
            {
                using (ServerStore.ContextPool.AllocateOperationContext(out TransactionOperationContext ctx))
                {
                    var clusterTopology = ServerStore.GetClusterTopology();

                    var possibleNode = clusterTopology.TryGetNodeTagByUrl(nodeUrl);
                    if (possibleNode.HasUrl)
                    {
                        throw new InvalidOperationException($"Can't add a new node on {nodeUrl} to cluster because this url is already used by node {possibleNode.NodeTag}");
                    }

                    if (nodeInfo.ServerId == ServerStore.GetServerId())
                    {
                        throw new InvalidOperationException($"Can't add a new node on {nodeUrl} to cluster because it's a synonym of the current node URL:{ServerStore.GetNodeHttpServerUrl()}");
                    }

                    if (nodeInfo.TopologyId != null)
                    {
                        AssertCanAddNodeWithTopologyId(clusterTopology, nodeInfo, nodeUrl);
                    }

                    var nodeTag = nodeInfo.NodeTag == RachisConsensus.InitialTag ? tag : nodeInfo.NodeTag;
                    CertificateDefinition oldServerCert = null;
                    X509Certificate2      certificate   = null;

                    if (remoteIsHttps)
                    {
                        if (nodeInfo.Certificate == null)
                        {
                            throw new InvalidOperationException($"Cannot add node {nodeTag} with url {nodeUrl} to cluster because it has no certificate while trying to use HTTPS");
                        }

                        certificate = new X509Certificate2(Convert.FromBase64String(nodeInfo.Certificate), (string)null, X509KeyStorageFlags.MachineKeySet);

                        var now = DateTime.UtcNow;
                        if (certificate.NotBefore.ToUniversalTime() > now)
                        {
                            // Because of time zone and time drift issues, we can't assume that the certificate generation will be
                            // proper. Because of that, we allow tolerance of the NotBefore to be a bit earlier / later than the
                            // current time. Clients may still fail to work with our certificate because of timing issues,
                            // but the admin needs to setup time sync properly and there isn't much we can do at that point
                            if ((certificate.NotBefore.ToUniversalTime() - now).TotalDays > 1)
                            {
                                throw new InvalidOperationException(
                                          $"Cannot add node {nodeTag} with url {nodeUrl} to cluster because its certificate '{certificate.FriendlyName}' is not yet valid. It starts on {certificate.NotBefore}");
                            }
                        }

                        if (certificate.NotAfter.ToUniversalTime() < now)
                        {
                            throw new InvalidOperationException($"Cannot add node {nodeTag} with url {nodeUrl} to cluster because its certificate '{certificate.FriendlyName}' expired on {certificate.NotAfter}");
                        }

                        var expected = GetStringQueryString("expectedThumbprint", required: false);
                        if (expected != null)
                        {
                            if (certificate.Thumbprint != expected)
                            {
                                throw new InvalidOperationException($"Cannot add node {nodeTag} with url {nodeUrl} to cluster because its certificate thumbprint '{certificate.Thumbprint}' doesn't match the expected thumbprint '{expected}'.");
                            }
                        }

                        // if it's the same server certificate as our own, we don't want to add it to the cluster
                        if (certificate.Thumbprint != Server.Certificate.Certificate.Thumbprint)
                        {
                            using (ctx.OpenReadTransaction())
                            {
                                var readCert = ServerStore.Cluster.GetCertificateByThumbprint(ctx, certificate.Thumbprint);
                                if (readCert != null)
                                {
                                    oldServerCert = JsonDeserializationServer.CertificateDefinition(readCert);
                                }
                            }

                            if (oldServerCert == null)
                            {
                                var certificateDefinition = new CertificateDefinition
                                {
                                    Certificate          = nodeInfo.Certificate,
                                    Thumbprint           = certificate.Thumbprint,
                                    PublicKeyPinningHash = certificate.GetPublicKeyPinningHash(),
                                    NotAfter             = certificate.NotAfter,
                                    Name = "Server Certificate for " + nodeUrl,
                                    SecurityClearance = SecurityClearance.ClusterNode
                                };

                                var res = await ServerStore.PutValueInClusterAsync(new PutCertificateCommand(certificate.Thumbprint, certificateDefinition,
                                                                                                             $"{raftRequestId}/put-new-certificate"));

                                await ServerStore.Cluster.WaitForIndexNotification(res.Index);
                            }
                        }
                    }

                    await ServerStore.AddNodeToClusterAsync(nodeUrl, nodeTag, validateNotInTopology : true, asWatcher : watcher ?? false);

                    using (ctx.OpenReadTransaction())
                    {
                        clusterTopology = ServerStore.GetClusterTopology(ctx);
                        possibleNode    = clusterTopology.TryGetNodeTagByUrl(nodeUrl);
                        nodeTag         = possibleNode.HasUrl ? possibleNode.NodeTag : null;

                        if (certificate != null && certificate.Thumbprint != Server.Certificate.Certificate.Thumbprint)
                        {
                            var modifiedServerCert = JsonDeserializationServer.CertificateDefinition(ServerStore.Cluster.GetCertificateByThumbprint(ctx, certificate.Thumbprint));

                            if (modifiedServerCert == null)
                            {
                                throw new ConcurrencyException("After adding the certificate, it was removed, shouldn't happen unless another admin removed it midway through.");
                            }

                            if (oldServerCert == null)
                            {
                                modifiedServerCert.Name = "Server certificate for Node " + nodeTag;
                            }
                            else
                            {
                                var value = "Node " + nodeTag;
                                if (modifiedServerCert.Name.Contains(value) == false)
                                {
                                    modifiedServerCert.Name += ", " + value;
                                }
                            }

                            var res = await ServerStore.PutValueInClusterAsync(new PutCertificateCommand(certificate.Thumbprint, modifiedServerCert, $"{raftRequestId}/put-modified-certificate"));

                            await ServerStore.Cluster.WaitForIndexNotification(res.Index);
                        }

                        var detailsPerNode = new DetailsPerNode
                        {
                            MaxUtilizedCores    = maxUtilizedCores,
                            NumberOfCores       = nodeInfo.NumberOfCores,
                            InstalledMemoryInGb = nodeInfo.InstalledMemoryInGb,
                            UsableMemoryInGb    = nodeInfo.UsableMemoryInGb,
                            BuildInfo           = nodeInfo.BuildInfo,
                            OsInfo = nodeInfo.OsInfo
                        };

                        var maxCores = ServerStore.LicenseManager.LicenseStatus.MaxCores;

                        try
                        {
                            await ServerStore.PutNodeLicenseLimitsAsync(nodeTag, detailsPerNode, maxCores, $"{raftRequestId}/put-license-limits");
                        }
                        catch
                        {
                            // we'll retry this again later
                        }
                    }

                    NoContentStatus();
                    return;
                }
            }
            RedirectToLeader();
        }