From c9a0a0df49365ccba1c49f3028763b33d76cc4d6 Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Sat, 25 Apr 2026 22:24:36 +0300 Subject: [PATCH 1/2] Add Start-PnPTenantRename cmdlet and related documentation for tenant domain renaming Co-authored-by: Copilot --- CHANGELOG.md | 1 + documentation/Start-PnPTenantRename.md | 123 +++++++++ src/Commands/Admin/StartTenantRename.cs | 80 ++++++ src/Commands/Model/TenantRenameJob.cs | 76 ++++++ .../Model/TenantRenameJobEntityData.cs | 27 ++ .../MultiGeo/MultiGeoRestApiClient.cs | 244 ++++++++++++++++++ 6 files changed, 551 insertions(+) create mode 100644 documentation/Start-PnPTenantRename.md create mode 100644 src/Commands/Admin/StartTenantRename.cs create mode 100644 src/Commands/Model/TenantRenameJob.cs create mode 100644 src/Commands/Model/TenantRenameJobEntityData.cs create mode 100644 src/Commands/Utilities/MultiGeo/MultiGeoRestApiClient.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2acaec08a..91c3f519e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). ## [Current nightly] ### Added +- Added `Start-PnPTenantRename` cmdlet to schedule SharePoint Online tenant domain rename jobs. - Added `Get-PnPListVersionPolicy` and `Set-PnPListVersionPolicy` cmdlets to inspect and manage SharePoint Online document library version policies. [#5300](https://github.com/pnp/powershell/pull/5300) - Added `New-PnPSiteManageVersionPolicyJob` cmdlet to queue site-level list version policy trim and sync jobs. [#5300](https://github.com/pnp/powershell/pull/5300) - Added site version policy parameters to `Set-PnPSite` and `Set-PnPTenantSite`, including support for inheriting tenant defaults, targeting new or existing document libraries, managing file type overrides, and bypassing confirmation with `-Force`. [#5300](https://github.com/pnp/powershell/pull/5300) diff --git a/documentation/Start-PnPTenantRename.md b/documentation/Start-PnPTenantRename.md new file mode 100644 index 000000000..13f791457 --- /dev/null +++ b/documentation/Start-PnPTenantRename.md @@ -0,0 +1,123 @@ +--- +Module Name: PnP.PowerShell +title: Start-PnPTenantRename +schema: 2.0.0 +applicable: SharePoint Online +external help file: PnP.PowerShell.dll-Help.xml +online version: https://pnp.github.io/powershell/cmdlets/Start-PnPTenantRename.html +--- + +# Start-PnPTenantRename + +## SYNOPSIS +Schedules a rename of the SharePoint Online tenant domain name. + +## SYNTAX + +```powershell +Start-PnPTenantRename -DomainName -ScheduledDateTime [-Connection ] [-WhatIf] [-Confirm] +``` + +## DESCRIPTION +Schedules a tenant rename job to change the SharePoint Online domain name for the organization. For example, this can change `contoso.sharepoint.com` to `fabrikam.sharepoint.com`. + +The new domain name must already have been added successfully to Microsoft Entra ID. Specify only the domain prefix, without `.sharepoint.com` or `.onmicrosoft.com`. + +The scheduled date and time must be in the future. SharePoint Online requires the selected time to be at least 24 hours in the future and no more than 30 days in the future. Tenant rename can take several hours to days depending on the number of SharePoint sites and OneDrive accounts in the tenant. + +## EXAMPLES + +### EXAMPLE 1 +```powershell +Start-PnPTenantRename -DomainName "fabrikam" -ScheduledDateTime "2026-05-30T22:00:00" +``` + +Schedules the SharePoint Online tenant domain rename to `fabrikam.sharepoint.com` for May 30, 2026 at 22:00. + +### EXAMPLE 2 +```powershell +Start-PnPTenantRename -DomainName "fabrikam" -ScheduledDateTime (Get-Date).AddDays(7) -WhatIf +``` + +Shows what would happen if the tenant rename were scheduled one week from now. + +## PARAMETERS + +### -Connection +Optional connection to be used by the cmdlet. Retrieve the value for this parameter by specifying `-ReturnConnection` on `Connect-PnPOnline` or by executing `Get-PnPConnection`. + +```yaml +Type: PnPConnection +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Confirm +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -DomainName +Specifies the new SharePoint Online domain prefix. Do not include `.sharepoint.com` or `.onmicrosoft.com`. + +```yaml +Type: String +Parameter Sets: (All) + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ScheduledDateTime +Specifies the date and time at which the tenant rename job should start. SharePoint Online requires this to be at least 24 hours in the future and no more than 30 days in the future. + +```yaml +Type: DateTime +Parameter Sets: (All) + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -WhatIf +Shows what would happen if the cmdlet runs. The cmdlet is not run. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +## OUTPUTS + +### System.String +Returns tenant rename warning and scheduling messages from SharePoint Online. + +## RELATED LINKS + +[Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) diff --git a/src/Commands/Admin/StartTenantRename.cs b/src/Commands/Admin/StartTenantRename.cs new file mode 100644 index 000000000..146450256 --- /dev/null +++ b/src/Commands/Admin/StartTenantRename.cs @@ -0,0 +1,80 @@ +using PnP.PowerShell.Commands.Base; +using PnP.PowerShell.Commands.Model; +using PnP.PowerShell.Commands.Utilities.MultiGeo; +using System; +using System.Collections.Generic; +using System.Management.Automation; + +namespace PnP.PowerShell.Commands.Admin +{ + [Cmdlet(VerbsLifecycle.Start, "PnPTenantRename", DefaultParameterSetName = ParameterSetFullRename, SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(string))] + public class StartTenantRename : PnPSharePointOnlineAdminCmdlet + { + private const string ParameterSetFullRename = "FullRename"; + + [Parameter(Mandatory = true, ParameterSetName = ParameterSetFullRename)] + [ValidateNotNullOrEmpty] + public string DomainName { get; set; } + + [Parameter(Mandatory = true, ParameterSetName = ParameterSetFullRename)] + [ValidateNotNullOrEmpty] + public DateTime ScheduledDateTime { get; set; } + + protected override void ExecuteCmdlet() + { + var targetDomainPrefix = DomainName.Trim(); + var scheduledDateTimeInUtc = GetValidatedScheduledDateTimeInUtc(ScheduledDateTime); + var multiGeoRestApiClient = new MultiGeoRestApiClient(AdminContext); + + WriteResponse(multiGeoRestApiClient.GetTenantRenameWarningMessages()); + + if (!ShouldProcess(targetDomainPrefix, $"Schedule SharePoint tenant rename for {scheduledDateTimeInUtc:u}")) + { + return; + } + + var tenantRenameJob = new TenantRenameJobEntityData + { + TargetDomainPrefix = targetDomainPrefix, + ScheduledDateTimeInUtc = scheduledDateTimeInUtc + }; + + var tenantRenameJobResponse = multiGeoRestApiClient.CreateTenantRenameJob(tenantRenameJob); + if (tenantRenameJobResponse?.ResponseMessages == null) + { + throw new PSInvalidOperationException("The tenant rename job could not be created. SharePoint Online did not return a response message."); + } + + WriteResponse(tenantRenameJobResponse.ResponseMessages); + } + + private static DateTime GetValidatedScheduledDateTimeInUtc(DateTime scheduledDateTime) + { + var scheduledDateTimeInUtc = scheduledDateTime.ToUniversalTime(); + + if (scheduledDateTimeInUtc < DateTime.UtcNow) + { + throw new PSArgumentException("ScheduledDateTime must be in the future.", nameof(ScheduledDateTime)); + } + + return scheduledDateTimeInUtc; + } + + private void WriteResponse(IEnumerable messages) + { + if (messages == null) + { + return; + } + + foreach (var message in messages) + { + if (!string.IsNullOrWhiteSpace(message)) + { + WriteObject(message); + } + } + } + } +} diff --git a/src/Commands/Model/TenantRenameJob.cs b/src/Commands/Model/TenantRenameJob.cs new file mode 100644 index 000000000..1084c1f2a --- /dev/null +++ b/src/Commands/Model/TenantRenameJob.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; + +namespace PnP.PowerShell.Commands.Model +{ + /// + /// Contains information about a SharePoint Online tenant rename job. + /// + public class TenantRenameJob + { + /// + /// Current state of the tenant rename job. + /// + public string JobState { get; set; } + + /// + /// User or process that initiated the tenant rename job. + /// + public string TriggeredBy { get; set; } + + /// + /// Number of site rename jobs queued for the tenant rename. + /// + public int QueuedSitesCount { get; set; } + + /// + /// Number of site rename jobs currently in progress for the tenant rename. + /// + public int InprogressSitesCount { get; set; } + + /// + /// Number of suspended site rename jobs for the tenant rename. + /// + public int SuspendedSitesCount { get; set; } + + /// + /// Number of successfully completed site rename jobs for the tenant rename. + /// + public int SuccessSitesCount { get; set; } + + /// + /// Number of failed site rename jobs for the tenant rename. + /// + public int FailedSitesCount { get; set; } + + /// + /// Total number of site rename jobs for the tenant rename. + /// + public int TotalSitesCount { get; set; } + + /// + /// Indicates whether the V2 tenant rename status endpoint should be used. + /// + public bool UseGetSpoTenantRenameStatusV2 { get; set; } + + /// + /// Scheduled start time for the tenant rename in UTC. + /// + public DateTime ScheduledDateTimeInUtc { get; set; } + + /// + /// Time at which the tenant rename was requested. + /// + public DateTime RequestedAt { get; set; } + + /// + /// Messages returned by the tenant rename service. + /// + public List ResponseMessages { get; set; } + + /// + /// Date and time format used by the tenant rename service messages. + /// + public string DateTimeFormat { get; set; } + } +} diff --git a/src/Commands/Model/TenantRenameJobEntityData.cs b/src/Commands/Model/TenantRenameJobEntityData.cs new file mode 100644 index 000000000..d28fe8d37 --- /dev/null +++ b/src/Commands/Model/TenantRenameJobEntityData.cs @@ -0,0 +1,27 @@ +using System; + +namespace PnP.PowerShell.Commands.Model +{ + internal class TenantRenameJobEntityData + { + public Guid JobId { get; set; } + + public string TargetDomainPrefix { get; set; } + + public bool SkipDomainCheck { get; set; } + + public int Option { get; set; } + + public string Reserve { get; set; } + + public bool UseV2TenantRename { get; set; } + + public bool UseV3TenantRename { get; set; } + + public string IncludeGestures { get; set; } + + public DateTime ScheduledDateTimeInUtc { get; set; } + + public DateTime RequestedAt { get; set; } + } +} diff --git a/src/Commands/Utilities/MultiGeo/MultiGeoRestApiClient.cs b/src/Commands/Utilities/MultiGeo/MultiGeoRestApiClient.cs new file mode 100644 index 000000000..5796018f8 --- /dev/null +++ b/src/Commands/Utilities/MultiGeo/MultiGeoRestApiClient.cs @@ -0,0 +1,244 @@ +using Microsoft.SharePoint.Client; +using PnP.Framework.Http; +using PnP.PowerShell.Commands.Model; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace PnP.PowerShell.Commands.Utilities.MultiGeo +{ + internal class MultiGeoRestApiClient + { + private const string TenantRenameApiVersion = "1.5.3"; + private const string TenantRenameStatusV2ApiVersion = "1.5.18"; + private const string TenantRenameJobsPath = "TenantRenameJobs"; + private const string TenantRenameJobsPathToGetWarningMessages = "TenantRenameJobs/GetWarningMessages"; + private const string TenantRenameJobsPathToGetStatus = "TenantRenameJobs/Get"; + private const string TenantRenameJobsPathToGetStatusV2 = "TenantRenameJobs/GetV2"; + private const string TenantRenameJobsPathToCancelAJob = "TenantRenameJobs/Cancel"; + private static readonly TimeSpan CreateTenantRenameJobTimeout = TimeSpan.FromSeconds(300); + private static readonly JsonSerializerOptions SerializerOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + private readonly ClientContext adminContext; + private readonly HttpClient httpClient; + + internal MultiGeoRestApiClient(ClientContext adminContext) + { + this.adminContext = adminContext ?? throw new ArgumentNullException(nameof(adminContext)); + httpClient = PnPHttpClient.Instance.GetHttpClient(adminContext); + } + + internal TenantRenameJob CreateTenantRenameJob(TenantRenameJobEntityData job) + { + return Post(TenantRenameJobsPath, job, CreateTenantRenameJobTimeout); + } + + internal TenantRenameJob GetTenantRenameJob() + { + return Get(TenantRenameJobsPathToGetStatus); + } + + internal TenantRenameJob GetTenantRenameJobV2() + { + return Get(TenantRenameJobsPathToGetStatusV2, TenantRenameStatusV2ApiVersion); + } + + internal IEnumerable GetTenantRenameWarningMessages() + { + return Get>(TenantRenameJobsPathToGetWarningMessages); + } + + internal void CancelTenantRenameJob() + { + Post(TenantRenameJobsPathToCancelAJob, payload: null); + } + + private T Get(string path, string apiVersion = TenantRenameApiVersion) + { + var responseText = Send(() => CreateRequest(HttpMethod.Get, path, apiVersion), timeout: null, allowRetries: true); + return DeserializeResponse(responseText); + } + + private T Post(string path, object payload, TimeSpan? timeout = null, string apiVersion = TenantRenameApiVersion) + { + var jsonPayload = payload == null ? null : JsonSerializer.Serialize(payload, SerializerOptions); + var responseText = Send(() => CreateRequest(HttpMethod.Post, path, apiVersion, jsonPayload), timeout, allowRetries: false); + return DeserializeResponse(responseText); + } + + private HttpRequestMessage CreateRequest(HttpMethod method, string path, string apiVersion, string jsonPayload = null) + { + var request = new HttpRequestMessage(method, CreateApiUri(path, apiVersion)) + { + Version = new Version(2, 0) + }; + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata=nometadata")); + PnPHttpClient.AuthenticateRequestAsync(request, adminContext).GetAwaiter().GetResult(); + + if (method == HttpMethod.Post) + { + request.Headers.TryAddWithoutValidation("X-RequestDigest", adminContext.GetRequestDigestAsync().GetAwaiter().GetResult()); + if (jsonPayload != null) + { + request.Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); + } + } + + return request; + } + + private Uri CreateApiUri(string path, string apiVersion) + { + var normalizedPath = path.TrimStart('/'); + var separator = normalizedPath.Contains('?') ? "&" : "?"; + return new Uri($"{adminContext.Url.TrimEnd('/')}/_api/{normalizedPath}{separator}api-version={apiVersion}"); + } + + private string Send(Func requestFactory, TimeSpan? timeout, bool allowRetries) + { + var retryAttempt = 0; + while (true) + { + using var request = requestFactory(); + using var cancellationTokenSource = timeout.HasValue ? new CancellationTokenSource(timeout.Value) : null; + using var response = httpClient.SendAsync(request, cancellationTokenSource?.Token ?? CancellationToken.None).GetAwaiter().GetResult(); + var responseText = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + + if (allowRetries && IsTransient(response.StatusCode) && retryAttempt < 10) + { + retryAttempt++; + Task.Delay(GetRetryDelay(response, retryAttempt)).GetAwaiter().GetResult(); + continue; + } + + if (!response.IsSuccessStatusCode) + { + throw new InvalidOperationException(GetErrorMessage(response.StatusCode, responseText)); + } + + return responseText; + } + } + + private static bool IsTransient(HttpStatusCode statusCode) + { + return statusCode == (HttpStatusCode)429 || statusCode == HttpStatusCode.ServiceUnavailable || statusCode == HttpStatusCode.GatewayTimeout; + } + + private static TimeSpan GetRetryDelay(HttpResponseMessage response, int retryAttempt) + { + if (response.Headers.RetryAfter?.Delta != null) + { + return response.Headers.RetryAfter.Delta.Value; + } + + if (response.Headers.RetryAfter?.Date != null) + { + var retryAfter = response.Headers.RetryAfter.Date.Value - DateTimeOffset.UtcNow; + if (retryAfter > TimeSpan.Zero) + { + return retryAfter; + } + } + + return TimeSpan.FromSeconds(Math.Min(Math.Pow(2, retryAttempt), 30)); + } + + private static T DeserializeResponse(string responseText) + { + if (string.IsNullOrWhiteSpace(responseText)) + { + return default; + } + + if (typeof(T) == typeof(string)) + { + return (T)(object)responseText; + } + + using var jsonDocument = JsonDocument.Parse(responseText); + var responseElement = UnwrapODataResponse(jsonDocument.RootElement); + return JsonSerializer.Deserialize(responseElement.GetRawText(), SerializerOptions); + } + + private static JsonElement UnwrapODataResponse(JsonElement responseElement) + { + if (responseElement.ValueKind != JsonValueKind.Object) + { + return responseElement; + } + + if (responseElement.TryGetProperty("d", out var dElement)) + { + return dElement.TryGetProperty("results", out var resultsElement) ? resultsElement : dElement; + } + + return responseElement.TryGetProperty("value", out var valueElement) ? valueElement : responseElement; + } + + private static string GetErrorMessage(HttpStatusCode statusCode, string responseText) + { + var statusMessage = $"SharePoint Online multi-geo REST API request failed with status {(int)statusCode} ({statusCode})."; + if (string.IsNullOrWhiteSpace(responseText)) + { + return statusMessage; + } + + try + { + using var jsonDocument = JsonDocument.Parse(responseText); + var rootElement = jsonDocument.RootElement; + if (TryGetODataErrorMessage(rootElement, out var errorMessage)) + { + return $"{statusMessage} {errorMessage}"; + } + } + catch (JsonException) + { + } + + return $"{statusMessage} {responseText}"; + } + + private static bool TryGetODataErrorMessage(JsonElement rootElement, out string errorMessage) + { + errorMessage = null; + if (!rootElement.TryGetProperty("error", out var errorElement) && !rootElement.TryGetProperty("odata.error", out errorElement)) + { + return false; + } + + if (errorElement.TryGetProperty("message", out var messageElement)) + { + if (messageElement.ValueKind == JsonValueKind.String) + { + errorMessage = messageElement.GetString(); + return !string.IsNullOrWhiteSpace(errorMessage); + } + + if (messageElement.ValueKind == JsonValueKind.Object && messageElement.TryGetProperty("value", out var valueElement)) + { + errorMessage = valueElement.GetString(); + return !string.IsNullOrWhiteSpace(errorMessage); + } + } + + if (errorElement.TryGetProperty("code", out var codeElement)) + { + errorMessage = codeElement.GetString(); + return !string.IsNullOrWhiteSpace(errorMessage); + } + + return false; + } + } +} From d891faf3f9a74a03574022e59c8b4a2e4f7341b9 Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Sat, 25 Apr 2026 22:37:46 +0300 Subject: [PATCH 2/2] Add Start-PnPTenantRename cmdlet and update documentation; refine scheduled date validation Co-authored-by: Copilot --- CHANGELOG.md | 2 +- documentation/Start-PnPTenantRename.md | 2 +- src/Commands/Admin/StartTenantRename.cs | 15 +++++++++++++-- src/Commands/Model/TenantRenameJobEntityData.cs | 16 ---------------- .../Utilities/MultiGeo/MultiGeoRestApiClient.cs | 2 +- 5 files changed, 16 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91c3f519e..cc7879217 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). ## [Current nightly] ### Added -- Added `Start-PnPTenantRename` cmdlet to schedule SharePoint Online tenant domain rename jobs. +- Added `Start-PnPTenantRename` cmdlet to schedule SharePoint Online tenant domain rename jobs. [#5304](https://github.com/pnp/powershell/pull/5304) - Added `Get-PnPListVersionPolicy` and `Set-PnPListVersionPolicy` cmdlets to inspect and manage SharePoint Online document library version policies. [#5300](https://github.com/pnp/powershell/pull/5300) - Added `New-PnPSiteManageVersionPolicyJob` cmdlet to queue site-level list version policy trim and sync jobs. [#5300](https://github.com/pnp/powershell/pull/5300) - Added site version policy parameters to `Set-PnPSite` and `Set-PnPTenantSite`, including support for inheriting tenant defaults, targeting new or existing document libraries, managing file type overrides, and bypassing confirmation with `-Force`. [#5300](https://github.com/pnp/powershell/pull/5300) diff --git a/documentation/Start-PnPTenantRename.md b/documentation/Start-PnPTenantRename.md index 13f791457..aced366ab 100644 --- a/documentation/Start-PnPTenantRename.md +++ b/documentation/Start-PnPTenantRename.md @@ -23,7 +23,7 @@ Schedules a tenant rename job to change the SharePoint Online domain name for th The new domain name must already have been added successfully to Microsoft Entra ID. Specify only the domain prefix, without `.sharepoint.com` or `.onmicrosoft.com`. -The scheduled date and time must be in the future. SharePoint Online requires the selected time to be at least 24 hours in the future and no more than 30 days in the future. Tenant rename can take several hours to days depending on the number of SharePoint sites and OneDrive accounts in the tenant. +The scheduled date and time must be at least 24 hours in the future and no more than 30 days in the future. Tenant rename can take several hours to days depending on the number of SharePoint sites and OneDrive accounts in the tenant. ## EXAMPLES diff --git a/src/Commands/Admin/StartTenantRename.cs b/src/Commands/Admin/StartTenantRename.cs index 146450256..598dd618b 100644 --- a/src/Commands/Admin/StartTenantRename.cs +++ b/src/Commands/Admin/StartTenantRename.cs @@ -1,6 +1,7 @@ using PnP.PowerShell.Commands.Base; using PnP.PowerShell.Commands.Model; using PnP.PowerShell.Commands.Utilities.MultiGeo; +using PnP.PowerShell.Commands.Attributes; using System; using System.Collections.Generic; using System.Management.Automation; @@ -8,6 +9,8 @@ namespace PnP.PowerShell.Commands.Admin { [Cmdlet(VerbsLifecycle.Start, "PnPTenantRename", DefaultParameterSetName = ParameterSetFullRename, SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + [RequiredApiApplicationPermissions("sharepoint/Sites.FullControl.All")] + [RequiredApiDelegatedPermissions("sharepoint/AllSites.FullControl")] [OutputType(typeof(string))] public class StartTenantRename : PnPSharePointOnlineAdminCmdlet { @@ -52,10 +55,18 @@ protected override void ExecuteCmdlet() private static DateTime GetValidatedScheduledDateTimeInUtc(DateTime scheduledDateTime) { var scheduledDateTimeInUtc = scheduledDateTime.ToUniversalTime(); + var utcNow = DateTime.UtcNow; + var minimumScheduledDateTimeInUtc = utcNow.AddHours(24); + var maximumScheduledDateTimeInUtc = utcNow.AddDays(30); - if (scheduledDateTimeInUtc < DateTime.UtcNow) + if (scheduledDateTimeInUtc < minimumScheduledDateTimeInUtc) { - throw new PSArgumentException("ScheduledDateTime must be in the future.", nameof(ScheduledDateTime)); + throw new PSArgumentException("ScheduledDateTime must be at least 24 hours in the future.", nameof(ScheduledDateTime)); + } + + if (scheduledDateTimeInUtc > maximumScheduledDateTimeInUtc) + { + throw new PSArgumentException("ScheduledDateTime must be no more than 30 days in the future.", nameof(ScheduledDateTime)); } return scheduledDateTimeInUtc; diff --git a/src/Commands/Model/TenantRenameJobEntityData.cs b/src/Commands/Model/TenantRenameJobEntityData.cs index d28fe8d37..82dfc5abd 100644 --- a/src/Commands/Model/TenantRenameJobEntityData.cs +++ b/src/Commands/Model/TenantRenameJobEntityData.cs @@ -4,24 +4,8 @@ namespace PnP.PowerShell.Commands.Model { internal class TenantRenameJobEntityData { - public Guid JobId { get; set; } - public string TargetDomainPrefix { get; set; } - public bool SkipDomainCheck { get; set; } - - public int Option { get; set; } - - public string Reserve { get; set; } - - public bool UseV2TenantRename { get; set; } - - public bool UseV3TenantRename { get; set; } - - public string IncludeGestures { get; set; } - public DateTime ScheduledDateTimeInUtc { get; set; } - - public DateTime RequestedAt { get; set; } } } diff --git a/src/Commands/Utilities/MultiGeo/MultiGeoRestApiClient.cs b/src/Commands/Utilities/MultiGeo/MultiGeoRestApiClient.cs index 5796018f8..a016b662c 100644 --- a/src/Commands/Utilities/MultiGeo/MultiGeoRestApiClient.cs +++ b/src/Commands/Utilities/MultiGeo/MultiGeoRestApiClient.cs @@ -187,7 +187,7 @@ private static JsonElement UnwrapODataResponse(JsonElement responseElement) private static string GetErrorMessage(HttpStatusCode statusCode, string responseText) { - var statusMessage = $"SharePoint Online multi-geo REST API request failed with status {(int)statusCode} ({statusCode})."; + var statusMessage = $"SharePoint Online REST request failed with status {(int)statusCode} ({statusCode})."; if (string.IsNullOrWhiteSpace(responseText)) { return statusMessage;