Exemple #1
0
        public Task SuspendObserver()
        {
            SetupCORSHeaders();

            if (ServerStore.IsLeader())
            {
                var suspend = GetBoolValueQueryString("value");
                if (suspend.HasValue)
                {
                    Server.ServerStore.Observer.Suspended = suspend.Value;
                }

                NoContentStatus();
                return(Task.CompletedTask);
            }

            RedirectToLeader();

            return(Task.CompletedTask);
        }
Exemple #2
0
        public async Task DemoteNode()
        {
            if (ServerStore.LeaderTag == null)
            {
                NoContentStatus();
                return;
            }

            SetupCORSHeaders();

            if (ServerStore.IsLeader() == false)
            {
                RedirectToLeader();
                return;
            }

            var nodeTag = GetStringQueryString("nodeTag");

            if (nodeTag == ServerStore.LeaderTag)
            {
                throw new InvalidOperationException(
                          $"Failed to demote node {nodeTag} beacuse {nodeTag} is the current leader in the cluster topology. In order to demote {nodeTag} perform a Step-Down first");
            }

            using (ServerStore.ContextPool.AllocateOperationContext(out TransactionOperationContext context))
                using (context.OpenReadTransaction())
                {
                    var topology = ServerStore.GetClusterTopology(context);
                    if (topology.Promotables.ContainsKey(nodeTag) == false && topology.Members.ContainsKey(nodeTag) == false)
                    {
                        throw new InvalidOperationException(
                                  $"Failed to demote node {nodeTag} beacuse {nodeTag} is not a voter in the cluster topology");
                    }

                    var url = topology.GetUrlFromTag(nodeTag);
                    await ServerStore.Engine.ModifyTopologyAsync(nodeTag, url, Leader.TopologyModification.NonVoter);

                    NoContentStatus();
                }
        }
        public async Task SetLicenseLimit()
        {
            var nodeTag          = GetStringQueryString("nodeTag");
            var newAssignedCores = GetIntValueQueryString("newAssignedCores");

            Debug.Assert(newAssignedCores != null);

            if (newAssignedCores <= 0)
            {
                throw new ArgumentException("The new assigned cores value must be larger than 0");
            }

            if (ServerStore.IsLeader())
            {
                await ServerStore.LicenseManager.ChangeLicenseLimits(nodeTag, newAssignedCores.Value, GetRaftRequestIdFromQuery());

                NoContentStatus();
                return;
            }

            RedirectToLeader();
        }
Exemple #4
0
        public async Task GetObserverDecisions()
        {
            if (ServerStore.IsLeader())
            {
                using (ServerStore.ContextPool.AllocateOperationContext(out TransactionOperationContext context))
                    await using (var writer = new AsyncBlittableJsonTextWriter(context, ResponseBodyStream()))
                    {
                        var res  = ServerStore.Observer.ReadDecisionsForDatabase();
                        var json = new DynamicJsonValue
                        {
                            [nameof(ClusterObserverDecisions.LeaderNode)]  = Server.ServerStore.NodeTag,
                            [nameof(ClusterObserverDecisions.Term)]        = Server.ServerStore.Engine.CurrentLeader?.Term,
                            [nameof(ClusterObserverDecisions.Suspended)]   = Server.ServerStore.Observer.Suspended,
                            [nameof(ClusterObserverDecisions.Iteration)]   = res.Iteration,
                            [nameof(ClusterObserverDecisions.ObserverLog)] = new DynamicJsonArray(res.List)
                        };

                        context.Write(writer, json);
                        return;
                    }
            }
            RedirectToLeader();
        }
Exemple #5
0
        public async Task ApplyCommand()
        {
            using (ServerStore.ContextPool.AllocateOperationContext(out TransactionOperationContext context))
            {
                if (ServerStore.IsLeader() == false)
                {
                    throw new NoLeaderException("Not a leader, cannot accept commands.");
                }

                HttpContext.Response.Headers["Reached-Leader"] = "true";

                var commandJson = await context.ReadForMemoryAsync(RequestBodyStream(), "external/rachis/command");

                CommandBase command;
                try
                {
                    command = CommandBase.CreateFrom(commandJson);
                }
                catch (InvalidOperationException e)
                {
                    RequestRouter.AssertClientVersion(HttpContext, e);
                    throw;
                }

                switch (command)
                {
                case AddDatabaseCommand addDatabase:
                    if (addDatabase.Record.Topology.Count == 0)
                    {
                        ServerStore.AssignNodesToDatabase(ServerStore.GetClusterTopology(), addDatabase.Record);
                    }
                    break;

                case AddOrUpdateCompareExchangeBatchCommand batchCmpExchange:
                    batchCmpExchange.ContextToWriteResult = context;
                    break;

                case CompareExchangeCommandBase cmpExchange:
                    cmpExchange.ContextToWriteResult = context;
                    break;
                }

                var isClusterAdmin = IsClusterAdmin();
                command.VerifyCanExecuteCommand(ServerStore, context, isClusterAdmin);

                var(etag, result) = await ServerStore.Engine.PutAsync(command);

                HttpContext.Response.StatusCode = (int)HttpStatusCode.OK;
                var ms = context.CheckoutMemoryStream();
                try
                {
                    using (var writer = new BlittableJsonTextWriter(context, ms))
                    {
                        context.Write(writer, new DynamicJsonValue
                        {
                            [nameof(ServerStore.PutRaftCommandResult.RaftCommandIndex)] = etag,
                            [nameof(ServerStore.PutRaftCommandResult.Data)]             = result
                        });
                        writer.Flush();
                    }
                    // now that we know that we properly serialized it
                    ms.Position = 0;
                    await ms.CopyToAsync(ResponseBodyStream());
                }
                finally
                {
                    context.ReturnMemoryStream(ms);
                }
            }
        }
Exemple #6
0
        public async Task AddNode()
        {
            SetupCORSHeaders();

            var nodeUrl       = GetQueryStringValueAndAssertIfSingleAndNotEmpty("url");
            var watcher       = GetBoolValueQueryString("watcher", false);
            var assignedCores = GetIntValueQueryString("assignedCores", false);

            if (assignedCores <= 0)
            {
                throw new ArgumentException("Assigned cores must be greater than 0!");
            }

            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.");
            }

            NodeInfo nodeInfo;

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

                    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 (ServerStore.IsPassive() && nodeInfo.TopologyId != null)
                    {
                        throw new TopologyMismatchException("You can't add new node to an already existing cluster");
                    }
                }

            if (assignedCores != null && assignedCores > nodeInfo.NumberOfCores)
            {
                throw new ArgumentException("Cannot add node because the assigned cores is larger " +
                                            $"than the available cores on that machine: {nodeInfo.NumberOfCores}");
            }

            ServerStore.EnsureNotPassive();

            if (assignedCores == null)
            {
                assignedCores = ServerStore.LicenseManager.GetCoresToAssign(nodeInfo.NumberOfCores);
            }

            Debug.Assert(assignedCores <= nodeInfo.NumberOfCores);

            ServerStore.LicenseManager.AssertCanAddNode(nodeUrl, assignedCores.Value);

            if (ServerStore.IsLeader())
            {
                using (ServerStore.ContextPool.AllocateOperationContext(out TransactionOperationContext ctx))
                {
                    string          topologyId;
                    ClusterTopology clusterTopology;
                    using (ctx.OpenReadTransaction())
                    {
                        clusterTopology = ServerStore.GetClusterTopology(ctx);
                        topologyId      = clusterTopology.TopologyId;
                    }

                    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)
                    {
                        if (topologyId != nodeInfo.TopologyId)
                        {
                            throw new TopologyMismatchException(
                                      $"Adding a new node to cluster failed. The new node is already in another cluster. " +
                                      $"Expected topology id: {topologyId}, but we get {nodeInfo.TopologyId}");
                        }

                        if (nodeInfo.CurrentState != RachisState.Passive)
                        {
                            throw new InvalidOperationException($"Can't add a new node on {nodeUrl} to cluster " +
                                                                $"because it's already in the cluster under tag :{nodeInfo.NodeTag} " +
                                                                $"and URL: {clusterTopology.GetUrlFromTag(nodeInfo.NodeTag)}");
                        }
                    }

                    var nodeTag = nodeInfo.NodeTag == RachisConsensus.InitialTag ? null : 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}'.");
                            }
                        }

                        using (ctx.OpenReadTransaction())
                        {
                            var key      = Constants.Certificates.Prefix + certificate.Thumbprint;
                            var readCert = ServerStore.Cluster.Read(ctx, key);
                            if (readCert != null)
                            {
                                oldServerCert = JsonDeserializationServer.CertificateDefinition(readCert);
                            }
                        }

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

                            var res = await ServerStore.PutValueInClusterAsync(new PutCertificateCommand(Constants.Certificates.Prefix + certificate.Thumbprint, certificateDefinition));

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

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

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

                        if (certificate != null)
                        {
                            var key = Constants.Certificates.Prefix + certificate.Thumbprint;

                            var modifiedServerCert = JsonDeserializationServer.CertificateDefinition(ServerStore.Cluster.Read(ctx, key));

                            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(key, modifiedServerCert));

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

                        var nodeDetails = new NodeDetails
                        {
                            NodeTag             = nodeTag,
                            AssignedCores       = assignedCores.Value,
                            NumberOfCores       = nodeInfo.NumberOfCores,
                            InstalledMemoryInGb = nodeInfo.InstalledMemoryInGb,
                            UsableMemoryInGb    = nodeInfo.UsableMemoryInGb,
                            BuildInfo           = nodeInfo.BuildInfo
                        };
                        await ServerStore.LicenseManager.CalculateLicenseLimits(nodeDetails, forceFetchingNodeInfo : true, waitToUpdate : true);
                    }

                    NoContentStatus();
                    return;
                }
            }
            RedirectToLeader();
        }
Exemple #7
0
        public async Task AddNode()
        {
            SetupCORSHeaders();

            var nodeUrl = GetStringQueryString("url").TrimEnd('/');
            var watcher = GetBoolValueQueryString("watcher", false);

            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.");
            }

            NodeInfo nodeInfo;

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

                    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 (ServerStore.IsPassive() && nodeInfo.TopologyId != null)
                    {
                        throw new TopologyMismatchException("You can't add new node to an already existing cluster");
                    }
                }

            ServerStore.EnsureNotPassive();
            if (ServerStore.IsLeader())
            {
                using (ServerStore.ContextPool.AllocateOperationContext(out TransactionOperationContext ctx))
                {
                    string topologyId;
                    using (ctx.OpenReadTransaction())
                    {
                        var clusterTopology = ServerStore.GetClusterTopology(ctx);
                        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}");
                        }
                        topologyId = clusterTopology.TopologyId;
                    }

                    if (nodeInfo.TopologyId != null && topologyId != nodeInfo.TopologyId)
                    {
                        throw new TopologyMismatchException(
                                  $"Adding a new node to cluster failed. The new node is already in another cluster. Expected topology id: {topologyId}, but we get {nodeInfo.TopologyId}");
                    }

                    var nodeTag = nodeInfo.NodeTag == RachisConsensus.InitialTag
                        ? null : nodeInfo.NodeTag;

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

                        var certificate = new X509Certificate2(Convert.FromBase64String(nodeInfo.Certificate));

                        if (certificate.NotBefore > DateTime.UtcNow)
                        {
                            throw new InvalidOperationException($"Cannot add node {nodeTag} to cluster because its certificate '{certificate.FriendlyName}' is not yet valid. It starts on {certificate.NotBefore}");
                        }

                        if (certificate.NotAfter < DateTime.UtcNow)
                        {
                            throw new InvalidOperationException($"Cannot add node {nodeTag} to cluster because its certificate '{certificate.FriendlyName}' expired on {certificate.NotAfter}");
                        }

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

                        var certificateDefinition = new CertificateDefinition
                        {
                            Certificate = nodeInfo.Certificate,
                            Thumbprint  = certificate.Thumbprint
                        };

                        var res = await ServerStore.PutValueInClusterAsync(new PutCertificateCommand(Constants.Certificates.Prefix + certificate.Thumbprint, certificateDefinition));

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

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

                    NoContentStatus();
                    return;
                }
            }
            RedirectToLeader();
        }
Exemple #8
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();
        }
        public async Task AddNode()
        {
            SetupCORSHeaders();

            var nodeUrl       = GetStringQueryString("url").TrimEnd('/');
            var watcher       = GetBoolValueQueryString("watcher", false);
            var assignedCores = GetIntValueQueryString("assignedCores", false);

            if (assignedCores <= 0)
            {
                throw new ArgumentException("Assigned cores must be greater than 0!");
            }

            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.");
            }

            NodeInfo nodeInfo;

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

                    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 (ServerStore.IsPassive() && nodeInfo.TopologyId != null)
                    {
                        throw new TopologyMismatchException("You can't add new node to an already existing cluster");
                    }
                }

            if (assignedCores == null)
            {
                assignedCores = ServerStore.LicenseManager.GetCoresToAssign(nodeInfo.NumberOfCores);
            }

            if (assignedCores != null && assignedCores > nodeInfo.NumberOfCores)
            {
                throw new ArgumentException("Cannot add node because the assigned cores is larger " +
                                            $"than the available cores on that machine: {nodeInfo.NumberOfCores}");
            }

            ServerStore.EnsureNotPassive();

            if (ServerStore.LicenseManager.CanAddNode(nodeUrl, assignedCores, out var licenseLimit) == false)
            {
                SetLicenseLimitResponse(licenseLimit);
                return;
            }

            if (ServerStore.IsLeader())
            {
                using (ServerStore.ContextPool.AllocateOperationContext(out TransactionOperationContext ctx))
                {
                    string topologyId;
                    using (ctx.OpenReadTransaction())
                    {
                        var clusterTopology = ServerStore.GetClusterTopology(ctx);
                        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}");
                        }
                        topologyId = clusterTopology.TopologyId;
                    }

                    if (nodeInfo.TopologyId != null && topologyId != nodeInfo.TopologyId)
                    {
                        throw new TopologyMismatchException(
                                  $"Adding a new node to cluster failed. The new node is already in another cluster. Expected topology id: {topologyId}, but we get {nodeInfo.TopologyId}");
                    }

                    var nodeTag = nodeInfo.NodeTag == RachisConsensus.InitialTag ? null : 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));

                        if (certificate.NotBefore > DateTime.UtcNow)
                        {
                            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 < DateTime.UtcNow)
                        {
                            throw new InvalidOperationException($"Cannot add node {nodeTag} with url {nodeUrl} to cluster because its certificate '{certificate.FriendlyName}' expired on {certificate.NotAfter}");
                        }

                        var expected = GetStringQueryString("expectedThumbrpint", 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}'.");
                            }
                        }

                        using (ctx.OpenReadTransaction())
                        {
                            var key      = Constants.Certificates.Prefix + certificate.Thumbprint;
                            var readCert = ServerStore.Cluster.Read(ctx, key);
                            if (readCert != null)
                            {
                                oldServerCert = JsonDeserializationServer.CertificateDefinition(readCert);
                            }
                        }

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

                            var res = await ServerStore.PutValueInClusterAsync(new PutCertificateCommand(Constants.Certificates.Prefix + certificate.Thumbprint, certificateDefinition));

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

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

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

                        if (certificate != null)
                        {
                            var key = Constants.Certificates.Prefix + certificate.Thumbprint;

                            var modifiedServerCert = JsonDeserializationServer.CertificateDefinition(ServerStore.Cluster.Read(ctx, key));

                            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(key, modifiedServerCert));

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

                        ServerStore.LicenseManager.CalculateLicenseLimits(nodeTag, assignedCores, nodeInfo, forceFetchingNodeInfo: true);
                    }

                    NoContentStatus();
                    return;
                }
            }
            RedirectToLeader();
        }
Exemple #10
0
        private async Task HandleUnusedAutoIndexes()
        {
            if (_serverStore.IsLeader() == false)
            {
                return;
            }

            var timeToWaitBeforeMarkingAutoIndexAsIdle        = _documentDatabase.Configuration.Indexing.TimeToWaitBeforeMarkingAutoIndexAsIdle;
            var timeToWaitBeforeDeletingAutoIndexMarkedAsIdle = _documentDatabase.Configuration.Indexing.TimeToWaitBeforeDeletingAutoIndexMarkedAsIdle;
            var ageThreshold = timeToWaitBeforeMarkingAutoIndexAsIdle.AsTimeSpan.Add(timeToWaitBeforeMarkingAutoIndexAsIdle.AsTimeSpan); // idle * 2

            var indexesSortedByLastQueryTime = (from index in _indexes
                                                where index.State != IndexState.Disabled && index.State != IndexState.Error
                                                let stats = index.GetStats()
                                                            let lastQueryingTime = stats.LastQueryingTime ?? DateTime.MinValue
                                                                                   orderby lastQueryingTime
                                                                                   select new UnusedIndexState
            {
                LastQueryingTime = lastQueryingTime,
                Index = index,
                State = stats.State,
                CreationDate = stats.CreatedTimestamp
            }).ToList();

            for (var i = 0; i < indexesSortedByLastQueryTime.Count; i++)
            {
                var item = indexesSortedByLastQueryTime[i];

                if (item.Index.Type != IndexType.AutoMap && item.Index.Type != IndexType.AutoMapReduce)
                {
                    continue;
                }

                var now       = _documentDatabase.Time.GetUtcNow();
                var age       = now - item.CreationDate;
                var lastQuery = now - item.LastQueryingTime;

                if (item.State == IndexState.Normal)
                {
                    TimeSpan differenceBetweenNewestAndCurrentQueryingTime;
                    if (i < indexesSortedByLastQueryTime.Count - 1)
                    {
                        var lastItem = indexesSortedByLastQueryTime[indexesSortedByLastQueryTime.Count - 1];
                        differenceBetweenNewestAndCurrentQueryingTime = lastItem.LastQueryingTime - item.LastQueryingTime;
                    }
                    else
                    {
                        differenceBetweenNewestAndCurrentQueryingTime = TimeSpan.Zero;
                    }

                    if (differenceBetweenNewestAndCurrentQueryingTime >= timeToWaitBeforeMarkingAutoIndexAsIdle.AsTimeSpan)
                    {
                        if (lastQuery >= timeToWaitBeforeMarkingAutoIndexAsIdle.AsTimeSpan)
                        {
                            item.Index.SetState(IndexState.Idle);
                            if (_logger.IsInfoEnabled)
                            {
                                _logger.Info($"Changed index '{item.Index.Name}' priority to idle. Age: {age}. Last query: {lastQuery}. Query difference: {differenceBetweenNewestAndCurrentQueryingTime}.");
                            }
                        }
                    }

                    continue;
                }

                if (item.State == IndexState.Idle)
                {
                    if (age <= ageThreshold || lastQuery >= timeToWaitBeforeDeletingAutoIndexMarkedAsIdle.AsTimeSpan)
                    {
                        await TryDeleteIndexIfExists(item.Index.Name);

                        if (_logger.IsInfoEnabled)
                        {
                            _logger.Info($"Deleted index '{item.Index.Name}' due to idleness. Age: {age}. Last query: {lastQuery}.");
                        }
                    }
                }
            }
        }