From 5984bc93c9a8f7cfa5030361cb3e27485ebe04ec Mon Sep 17 00:00:00 2001 From: Wes Carroll Date: Sat, 12 Oct 2019 22:26:13 -0400 Subject: [PATCH 1/7] Create Copy-ZertoVpg Function --- ZertoApiWrapper/Public/Copy-ZertoVpg.ps1 | 49 ++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 ZertoApiWrapper/Public/Copy-ZertoVpg.ps1 diff --git a/ZertoApiWrapper/Public/Copy-ZertoVpg.ps1 b/ZertoApiWrapper/Public/Copy-ZertoVpg.ps1 new file mode 100644 index 0000000..67ef11c --- /dev/null +++ b/ZertoApiWrapper/Public/Copy-ZertoVpg.ps1 @@ -0,0 +1,49 @@ +function Copy-ZertoVpg { + [CmdletBinding()] + param ( + # VPG Name to Clone + [Parameter(Mandatory, + HelpMessage = "Name of the VPG to clone")] + [ValidateNotNullOrEmpty()] + [String]$VpgName, + # Name of VMs to add to the VPG + [Parameter(Mandatory, + HelpMessage = "Name(s) of the Virtual Machine(s) to add to the VPG")] + [ValidateNotNullOrEmpty()] + [String[]]$VMs + ) + + begin { + + } + + process { + $VpgIdToCopy = @{ VpgIdentifier = (Get-ZertoVpg -vpgName $VpgName).vpgIdentifier } + if ( $null -eq $VpgIdToCopy.VpgIdentifier ) { + Write-Error -Message "Unable to find a VPG with the name: $VpgName. Please check the name and try again." + Break + } elseif ($VpgIdToCopy.VpgIdentifier.Count -gt 1) { + Write-Error -Message "More than one VPG was returned when searching for the VPG name: $VpgName. Please try again." + Break + } + $BaseUri = "vpgSettings/copyVpgSettings" + $UnprotectedVms = Get-ZertoUnprotectedVm + $ProtectedVms = Get-ZertoProtectedVm + $VMsToAdd = foreach ($VM in $VMs) { + if ($UnprotectedVms.VmName -contains $VM) { + $UnprotectedVms | Where-Object { $_.VmName -like $VM } | Select-Object -ExpandProperty VmIdentifier + } elseif ($ProtectedVms.VmName -contains $VM) { + $ProtectedVms | Where-Object { $_.VmName -like $VM } | Select-Object -ExpandProperty VmIdentifier + } else { + Write-Warning -Message "Unable to find VM with Name $VM. Skipping." + } + } + $NewVpgId = Invoke-ZertoRestRequest -Uri $BaseUri -body ($VpgIdToCopy | ConvertTo-Json) -Method "POST" + $Uri = "{0}/{1}/vms" -f $BaseUri, $NewVpgId.VpgIdentifier + Invoke-ZertoRestRequest -Uri $Uri -Body ($VMsToAdd | ConvertTo-Json) -Method "PUT" + } + + end { + + } +} From 10785df8a99ef7cea480d156452949f4e28f92c6 Mon Sep 17 00:00:00 2001 From: Wes Carroll Date: Sat, 12 Oct 2019 22:26:13 -0400 Subject: [PATCH 2/7] Create Copy-ZertoVpg Function --- ZertoApiWrapper/Public/Copy-ZertoVpg.ps1 | 49 ++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 ZertoApiWrapper/Public/Copy-ZertoVpg.ps1 diff --git a/ZertoApiWrapper/Public/Copy-ZertoVpg.ps1 b/ZertoApiWrapper/Public/Copy-ZertoVpg.ps1 new file mode 100644 index 0000000..67ef11c --- /dev/null +++ b/ZertoApiWrapper/Public/Copy-ZertoVpg.ps1 @@ -0,0 +1,49 @@ +function Copy-ZertoVpg { + [CmdletBinding()] + param ( + # VPG Name to Clone + [Parameter(Mandatory, + HelpMessage = "Name of the VPG to clone")] + [ValidateNotNullOrEmpty()] + [String]$VpgName, + # Name of VMs to add to the VPG + [Parameter(Mandatory, + HelpMessage = "Name(s) of the Virtual Machine(s) to add to the VPG")] + [ValidateNotNullOrEmpty()] + [String[]]$VMs + ) + + begin { + + } + + process { + $VpgIdToCopy = @{ VpgIdentifier = (Get-ZertoVpg -vpgName $VpgName).vpgIdentifier } + if ( $null -eq $VpgIdToCopy.VpgIdentifier ) { + Write-Error -Message "Unable to find a VPG with the name: $VpgName. Please check the name and try again." + Break + } elseif ($VpgIdToCopy.VpgIdentifier.Count -gt 1) { + Write-Error -Message "More than one VPG was returned when searching for the VPG name: $VpgName. Please try again." + Break + } + $BaseUri = "vpgSettings/copyVpgSettings" + $UnprotectedVms = Get-ZertoUnprotectedVm + $ProtectedVms = Get-ZertoProtectedVm + $VMsToAdd = foreach ($VM in $VMs) { + if ($UnprotectedVms.VmName -contains $VM) { + $UnprotectedVms | Where-Object { $_.VmName -like $VM } | Select-Object -ExpandProperty VmIdentifier + } elseif ($ProtectedVms.VmName -contains $VM) { + $ProtectedVms | Where-Object { $_.VmName -like $VM } | Select-Object -ExpandProperty VmIdentifier + } else { + Write-Warning -Message "Unable to find VM with Name $VM. Skipping." + } + } + $NewVpgId = Invoke-ZertoRestRequest -Uri $BaseUri -body ($VpgIdToCopy | ConvertTo-Json) -Method "POST" + $Uri = "{0}/{1}/vms" -f $BaseUri, $NewVpgId.VpgIdentifier + Invoke-ZertoRestRequest -Uri $Uri -Body ($VMsToAdd | ConvertTo-Json) -Method "PUT" + } + + end { + + } +} From 5f978d7b1a686864779fb4fa03464817a40ee1c8 Mon Sep 17 00:00:00 2001 From: Wes Carroll Date: Thu, 7 Nov 2019 17:43:18 -0500 Subject: [PATCH 3/7] Fix AddVM Logic --- ZertoApiWrapper/Public/Copy-ZertoVpg.ps1 | 38 +++++++++++++++++------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/ZertoApiWrapper/Public/Copy-ZertoVpg.ps1 b/ZertoApiWrapper/Public/Copy-ZertoVpg.ps1 index 67ef11c..3a22c37 100644 --- a/ZertoApiWrapper/Public/Copy-ZertoVpg.ps1 +++ b/ZertoApiWrapper/Public/Copy-ZertoVpg.ps1 @@ -1,12 +1,17 @@ +<# .ExternalHelp ./en-us/ZertoApiWrapper-help.xml #> function Copy-ZertoVpg { - [CmdletBinding()] + [CmdletBinding(SupportsShouldProcess)] param ( # VPG Name to Clone [Parameter(Mandatory, HelpMessage = "Name of the VPG to clone")] [ValidateNotNullOrEmpty()] - [String]$VpgName, - # Name of VMs to add to the VPG + [String]$SourceVpgName, + # New VPG Name + [Parameter(Mandatory, + HelpMessage = "Name to assign the newly created VPG")] + [ValidateNotNullOrEmpty()] + [String]$NewVpgName, # Name of VMs to add to the VPG [Parameter(Mandatory, HelpMessage = "Name(s) of the Virtual Machine(s) to add to the VPG")] [ValidateNotNullOrEmpty()] @@ -18,12 +23,12 @@ function Copy-ZertoVpg { } process { - $VpgIdToCopy = @{ VpgIdentifier = (Get-ZertoVpg -vpgName $VpgName).vpgIdentifier } + $VpgIdToCopy = @{ VpgIdentifier = (Get-ZertoVpg -vpgName $SourceVpgName).vpgIdentifier } if ( $null -eq $VpgIdToCopy.VpgIdentifier ) { - Write-Error -Message "Unable to find a VPG with the name: $VpgName. Please check the name and try again." + Write-Error -Message "Unable to find a VPG with the name: $SourceVpgName. Please check the name and try again." Break } elseif ($VpgIdToCopy.VpgIdentifier.Count -gt 1) { - Write-Error -Message "More than one VPG was returned when searching for the VPG name: $VpgName. Please try again." + Write-Error -Message "More than one VPG was returned when searching for the VPG name: $SourceVpgName. Please try again." Break } $BaseUri = "vpgSettings/copyVpgSettings" @@ -31,16 +36,27 @@ function Copy-ZertoVpg { $ProtectedVms = Get-ZertoProtectedVm $VMsToAdd = foreach ($VM in $VMs) { if ($UnprotectedVms.VmName -contains $VM) { - $UnprotectedVms | Where-Object { $_.VmName -like $VM } | Select-Object -ExpandProperty VmIdentifier + $VmId = $UnprotectedVms | Where-Object { $_.VmName -like $VM } | Select-Object -ExpandProperty VmIdentifier } elseif ($ProtectedVms.VmName -contains $VM) { - $ProtectedVms | Where-Object { $_.VmName -like $VM } | Select-Object -ExpandProperty VmIdentifier + $VmId = $ProtectedVms | Where-Object { $_.VmName -like $VM } | Select-Object -ExpandProperty VmIdentifier } else { Write-Warning -Message "Unable to find VM with Name $VM. Skipping." } + $returnObject = New-Object PSObject + $returnObject | Add-Member -MemberType NoteProperty -Name "VmIdentifier" -Value $VmId + $returnObject } - $NewVpgId = Invoke-ZertoRestRequest -Uri $BaseUri -body ($VpgIdToCopy | ConvertTo-Json) -Method "POST" - $Uri = "{0}/{1}/vms" -f $BaseUri, $NewVpgId.VpgIdentifier - Invoke-ZertoRestRequest -Uri $Uri -Body ($VMsToAdd | ConvertTo-Json) -Method "PUT" + if ($PSCmdlet.ShouldProcess("$VMsToAdd", "Copying $SourceVpgName to $NewVpgName with Settings")) { + $NewVpgId = Invoke-ZertoRestRequest -Uri $BaseUri -Body ($VpgIdToCopy | ConvertTo-Json) -Method "POST" + $Uri = "{0}/{1}/vms" -f "vpgSettings", $NewVpgId + $null = Invoke-ZertoRestRequest -Uri $Uri -Body ($VMsToAdd | ConvertTo-Json) -Method "POST" + $Uri = "vpgSettings/{0}" -f $NewVpgId + $CurrentSettings = Invoke-ZertoRestRequest -Uri $Uri + $CurrentSettings.Basic.Name = $NewVpgName + $Null = Invoke-ZertoRestRequest -Uri $Uri -Method "Put" -Body $($CurrentSettings | ConvertTo-Json -Depth 20) + Save-ZertoVpgSetting -vpgSettingsIdentifier $NewVpgId + } + } end { From 11ab03be0ae568e60ea092566ae0a14d49b0ece1 Mon Sep 17 00:00:00 2001 From: Wes Carroll Date: Thu, 7 Nov 2019 17:43:29 -0500 Subject: [PATCH 4/7] Create Tests --- Tests/Public/Copy-ZertoVpg.Tests.ps1 | 56 ++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 Tests/Public/Copy-ZertoVpg.Tests.ps1 diff --git a/Tests/Public/Copy-ZertoVpg.Tests.ps1 b/Tests/Public/Copy-ZertoVpg.Tests.ps1 new file mode 100644 index 0000000..cd53274 --- /dev/null +++ b/Tests/Public/Copy-ZertoVpg.Tests.ps1 @@ -0,0 +1,56 @@ +#Requires -Modules Pester +$global:here = (Split-Path -Parent $MyInvocation.MyCommand.Path) +$global:function = ((Split-Path -leaf $MyInvocation.MyCommand.Path).Split('.'))[0] + +Describe $global:function -Tag 'Unit', 'Source', 'Built' { + BeforeAll { + $script:ScriptBlock = (Get-Command $global:function).ScriptBlock + } + + Context "$($global:function)::Parameter Unit Tests" { + + It "$global:function should have exactly 16 parameters defined" { + (Get-Command $global:function).Parameters.Count | Should -Be 16 + } + + $ParameterTestCases = @( + @{ParameterName = 'SourceVpgName'; Type = 'String'; Mandatory = $true; Validation = 'NotNullOrEmpty' } + @{ParameterName = 'NewVpgName'; Type = 'String'; Mandatory = $true; Validation = 'NotNullOrEmpty' } + @{ParameterName = 'VMs'; Type = 'String[]'; Mandatory = $true; Validation = 'NotNullOrEmpty' } + ) + + It " parameter is of type" -TestCases $ParameterTestCases { + param($ParameterName, $Type, $Mandatory) + Get-Command $global:function | Should -HaveParameter $ParameterName -Mandatory:$Mandatory -Type $Type + } + + It " parameter has correct validation setting" -TestCases $ParameterTestCases { + param($ParameterName, $Validation) + Switch ($Validation) { + 'NotNullOrEmpty' { + $attrs = (Get-Command $global:function).Parameters[$ParameterName].Attributes + $attrs.Where{ $_ -is [ValidateNotNullOrEmpty] }.Count | Should -Be 1 + } + + $null { + $attrs = (Get-Command $global:function).Parameters[$ParameterName].Attributes + $attrs.TypeId.Count | Should -Be 2 + } + } + } + + It "Supports 'SupportsShouldProcess'" { + Get-Command $global:function | Should -HaveParameter WhatIf + Get-Command $global:function | Should -HaveParameter Confirm + $script:ScriptBlock | Should -match 'SupportsShouldProcess' + $script:ScriptBlock | Should -match '\$PSCmdlet\.ShouldProcess\(.+\)' + } + } + + Context "$($global:function)::Function Unit Tests" { + + } +} + +Remove-Variable -Name function -Scope Global +Remove-Variable -Name here -Scope Global From 213b6029e4bc31b0404bc739cd21284dbad1772c Mon Sep 17 00:00:00 2001 From: Wes Carroll Date: Thu, 7 Nov 2019 17:43:40 -0500 Subject: [PATCH 5/7] Create Documentation --- docs/Copy-ZertoVpg.md | 127 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 docs/Copy-ZertoVpg.md diff --git a/docs/Copy-ZertoVpg.md b/docs/Copy-ZertoVpg.md new file mode 100644 index 0000000..6e5436b --- /dev/null +++ b/docs/Copy-ZertoVpg.md @@ -0,0 +1,127 @@ +--- +external help file: ZertoApiWrapper-help.xml +Module Name: ZertoApiWrapper +online version: https://github.com/ZertoPublic/ZertoApiWrapper/blob/master/docs/Copy-ZertoVpg.md +schema: 2.0.0 +--- + +# Copy-ZertoVpg + +## SYNOPSIS +Copy an existing VPG settings object to create a new VPG with the same settings. New VMs must be added to the copied VPG. + +## SYNTAX + +``` +Copy-ZertoVpg -SourceVpgName -NewVpgName [-VMs] [-WhatIf] [-Confirm] + [] +``` + +## DESCRIPTION +Copy an existing VPG settings object to create a new VPG with the same settings. New VMs must be added to the copied VPG. + +## EXAMPLES + +### Example 1 +```powershell +PS C:\> Copy-ZertoVpg -SourceVpgName 'MyVpg' -NewVpgName 'MyNewVpg' -VMs 'VmToAdd01' +``` + +Copies the settings of 'MyVpg' into a new VPG 'MyNewVpg' adding one VM, 'VmToAdd01' + +### Example 2 +```powershell +PS C:\> Copy-ZertoVpg -SourceVpgName 'MyVpg' -NewVpgName 'MyNewVpg' -VMs 'VmToAdd01', 'VmToAdd02' +``` + +Copies the settings of 'MyVpg' into a new VPG 'MyNewVpg' adding two VMs, 'VmToAdd01' and 'VmToAdd02' + +## PARAMETERS + +### -NewVpgName +Name to assign the newly created VPG + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -SourceVpgName +Name of the VPG to clone + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -VMs +Name of VMs to add to the VPG + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: True +Position: 2 +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) +Aliases: cf + +Required: False +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) +Aliases: wi + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES + +## RELATED LINKS + +[Zerto Clone VPG API Endpoint Documentation](http://s3.amazonaws.com/zertodownload_docs/Latest/Zerto%20Virtual%20Replication%20Zerto%20Virtual%20Manager%20%28ZVM%29%20-%20vSphere%20Online%20Help/index.html#page/RestfulAPIs%2FStatusAPIs.5.119.html%23) From cdae2039935bc5b21edac07f9378fcf1bd56c1f5 Mon Sep 17 00:00:00 2001 From: Wes Carroll Date: Thu, 7 Nov 2019 17:44:06 -0500 Subject: [PATCH 6/7] Fix Documentation Formatting --- docs/Invoke-ZertoEvacuateVra.md | 1 + docs/New-ZertoPairingToken.md | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/Invoke-ZertoEvacuateVra.md b/docs/Invoke-ZertoEvacuateVra.md index bea1ad3..aa0b2e9 100644 --- a/docs/Invoke-ZertoEvacuateVra.md +++ b/docs/Invoke-ZertoEvacuateVra.md @@ -143,5 +143,6 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable ## NOTES ## RELATED LINKS + [Zerto REST API Evacuate VRA End Point Documentation](http://s3.amazonaws.com/zertodownload_docs/Latest/Zerto%20Virtual%20Replication%20Zerto%20Virtual%20Manager%20%28ZVM%29%20-%20vSphere%20Online%20Help/index.html#page/RestfulAPIs%2FStatusAPIs.5.129.html%23) diff --git a/docs/New-ZertoPairingToken.md b/docs/New-ZertoPairingToken.md index ee5f55a..09987b9 100644 --- a/docs/New-ZertoPairingToken.md +++ b/docs/New-ZertoPairingToken.md @@ -74,4 +74,5 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable ## NOTES ## RELATED LINKS + [Zerto REST API Peer Sites End Point Documentation](http://s3.amazonaws.com/zertodownload_docs/Latest/Zerto%20Virtual%20Replication%20Zerto%20Virtual%20Manager%20%28ZVM%29%20-%20vSphere%20Online%20Help/index.html#page/RestfulAPIs%2FStatusAPIs.5.046.html%23) From d6562de9bffd244802c1bf6f2164d875b2d13e54 Mon Sep 17 00:00:00 2001 From: Wes Carroll Date: Thu, 7 Nov 2019 21:37:03 -0500 Subject: [PATCH 7/7] Update for Copy-ZertoVpg --- RELEASENOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 88b7fb5..b97d8b5 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,7 @@ * A token is now required to pair two sites together. The need is discussed in [Issue 46](https://github.com/ZertoPublic/ZertoApiWrapper/issues/46). To implement this change a `-token` parameter has been added to the `Add-ZertoPeerSite` function. * A new function has been added; `New-ZertoPairingToken`. This function will allow users to generate a pairing authentication token from the target ZVM to be used in the pairing process. [Issue 47](https://github.com/ZertoPublic/ZertoApiWrapper/issues/47) covers additional details. * A new function has been added; `Invoke-ZertoEvacuateVra`. This function will allow users to evacuate a target VRA by specifying a Host Name, VRA Name, or VRA Identifier. All VMs currently replicating to the specified location will be migrated to different targets. [Issue 51](https://github.com/ZertoPublic/ZertoApiWrapper/issues/51) + * A function has been added; `Copy-ZertoVpg`. This function will allow users to copy the settings of a single VPG and add new VMs to it. There is currently no customization beyond specifying the VMs to be placed in the newly created VPG. Should additional edits \ updates be required, they should be done post creation. [Issue 54](https://github.com/ZertoPublic/ZertoApiWrapper/issues/54) ### Zerto Analytics