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; }
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); } }
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(); }
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; } }
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; }
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(); }
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); }
public async Task Bootstrap() { await ServerStore.EnsureNotPassiveAsync(); NoContentStatus(); }
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(); }
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(); }