Publish-Deployment.ps1
function Publish-Deployment { <# .Synopsis Publishes a deployment .Description Generates an Azure deployment package from a set of modules that will be deployed. OR Uploads a set of modules in a deployment to Blob Storage .Link Get-Deployment .Link Add-Deployment .Link Remove-Deployment .Link Import-Deployment .Example Publish-Deployment #> [CmdletBinding(DefaultParameterSetName="PublishAzureDeployment")] [OutputType([IO.FileInfo], [Nullable])] param( # The directory the deployment will be placed in. [Parameter(ParameterSetName='PublishAzureDeployment', ValueFromPipelineByPropertyName=$true)] [string] $DeploymentDirectory = "$home\Documents\Deployments", # The directory the deployment will be placed in. [Parameter(ParameterSetName='PublishAzureDeployment', ValueFromPipelineByPropertyName=$true)] [string] $DeploymentName, # The name of the modules to publish [Parameter(Position=0)] [string[]] $Name, # The group of modules to publish [Parameter(Position=1)] [string[]] $Group, # A list of groups to exclude. [Parameter(Position=2)] [string[]] $ExcludeGroup, # The VMSize of the deployment [Parameter(ParameterSetName='PublishAzureDeployment', ValueFromPipelineByPropertyName=$true)] [ValidateSet('ExtraSmall','Small','Medium', 'Large', 'Extra-Large', 'XS', 'XL', 'S', 'M', 'L')] [string] $VMSize = 'ExtraSmall', # The instance count [Parameter(ParameterSetName='PublishAzureDeployment', ValueFromPipelineByPropertyName=$true)] [Uint32] $InstanceCount = 1, # If set, will publish items in a background job [Switch] $AsJob, # If set, will wait for all jobs to complete [Switch] $Wait, # The throttle for background jobs. By default, 10 [Uint32] $Throttle, # The buffer between jobs. By default, 3 seconds [Timespan] $Buffer = $([Timespan]::FromSeconds(3)), # The operating system family [Parameter(ParameterSetName='PublishAzureDeployment', ValueFromPipelineByPropertyName=$true)] [ValidateSet("2K8R2","2012")] [string] $Os = "2012", # The Azure storage account [Parameter(Mandatory=$true,ParameterSetName='PublishToBlobStorage', ValueFromPipelineByPropertyName=$true)] [string] $StorageAccount, # The Azure storage key [Parameter(Mandatory=$true,ParameterSetName='PublishToBlobStorage', ValueFromPipelineByPropertyName=$true)] [string] $StorageKey, # If set, will push a deployment to a list of computers via a LAN. [Parameter(Mandatory=$true,ParameterSetName='PublishToLan', ValueFromPipelineByPropertyName=$true)] [Parameter(ParameterSetName='PublishToAzureVM', ValueFromPipelineByPropertyName=$true)] [string[]] $ComputerName, # If set, will publish a deployment to AzureVMs [Parameter(Mandatory=$true,ParameterSetName='PublishToAzureVM')] [Switch] $ToAzureVM, # The name of the computers that will receive the deployment [Parameter(Mandatory=$true,ParameterSetName='PublishToAzureVM', ValueFromPipelineByPropertyName=$true)] [Parameter(Mandatory=$true,ParameterSetName='PublishToLan', ValueFromPipelineByPropertyName=$true)] [Management.Automation.PSCredential] $Credential, # The number of concurrent batches of remote jobs to run. This should approximately be the number of remote shells allowed on the destination machines. [Parameter(ParameterSetName='PublishToAzureVM')] [Uint32] $BatchSize = 5, # The secure settings to copy to the remote machine. Wildcards are permitted. [Parameter(ParameterSetName='PublishToAzureVM')] [Parameter(ParameterSetName='PublishToLan')] [string[]] $SecureSetting ) begin { $deployments = Get-Deployment #region RunAs Job or Elevated $asJobOrElevate = { param($CommandInfo, [switch]$OnlyCommand, [string[]]$AdditionalModules, [Hashtable]$Parameter, [Switch]$RequireAdmin) # Find the current user and see if they're admin $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() $isAdmin = (New-Object Security.Principal.WindowsPrincipal $currentUser).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) # If we need to run in the background... if ($AsJob -or $Throttle -or (-not $isAdmin) -and $requireAdmin) { # Then pack things up... if ($onlyCommand) { # Just drop in the command if it's simple $AdditionalModules = $AdditionalModules | Select-Object -Unique $myDefinition = [ScriptBLock]::Create(" $(if ($AdditionalModules) { " Import-Module '$($AdditionalModules -join ("','"))' "}) function $commandInfo { $($commandInfo | Select-Object -ExpandProperty Definition) } ") } else { # Otherwise, drop in the module import $myModule = $CommandInfo.ScriptBlock.Module $AdditionalModules += $myModule | Split-Path $AdditionalModules += $myModule.RequiredModules | Split-Path $AdditionalModules = $AdditionalModules | Select-Object -Unique $myDefinition = [ScriptBLock]::Create(" $(if ($AdditionalModules) { " Import-Module '$($AdditionalModules -join ("','"))' "}) ") } # Remove AsJob, Throttle, and RequireAdmin (to avoid endless loops) $null = $Parameter.Remove('AsJob') $null = $Parameter.Remove('Throttle') $null = $Parameter.Remove('RequireAdmin') # Create the full command. Use Splatting to provide the parameters $myJob= [ScriptBLock]::Create("" + { param([Hashtable]$parameter) } + $myDefinition + " $commandInfo `@parameter ") if ($Throttle) { # Throttle as needed $jobLaunched= $false do { if ($myJobs) { $myJobs | Receive-Job } $runningJobs = $myJobs | Where-Object { $_.State -ne 'Running' } if ($runningJobs) { $runningJobs | Remove-Job -Force } if ($myJobs.Count -lt $throttle) { $null = Start-Job -Name "${MyCmd}_Background_Job" -ScriptBlock $myJob -ArgumentList $Parameter $JobLaunched = $true } $myJobs = Get-Job -Name "${MyCmd}_Background_Job" -ErrorAction SilentlyContinue Write-Progress "Waiting for Jobs to Complete" "$($myJobs.Count) Running" -Id $ProgressId } until ($jobLaunched) $myJobs = Get-Job -Name "${MyCmd}_Background_Job" -ErrorAction SilentlyContinue $myJobs | Wait-Job | Receive-Job return } elseif ($asJob) { # Just kick off the background job return Start-Job -ScriptBlock $myJob -ArgumentList $Parameter -Name "${CommandInfo}_Background_Job" } elseif ((-not $isAdmin) -and $RequireAdmin) { # Create a new process to run the job $fullCommand = " `$parameter = $(Write-PowerShellHashtable -InputObject $parameter) & { $myJob } `$parameter " $encodedCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($fullCommand)) return Start-Process powershell -Verb Runas -ArgumentList '-encodedCommand', $encodedCommand -PassThru } } } #endregion RunAs Job or Elevated } process { $deploymentsToPublish = $deployments | Where-Object { if ($name) { $_.Name -like $name } elseif ($Group) { if ($ExcludeGroup) { foreach ($g in $Group) { if ($_.Group -contains $g) { $excluded = foreach ($ex in $ExcludeGroup) { if ($_.Group -contains $ex) { $true break } } if (-not $excluded) { $_ } } } } else { foreach ($g in $Group) { if ($_.Group -contains $g) { $_ } } } } elseif ($ExcludedGroup) { $excluded = foreach ($ex in $ExcludeGroup) { if ($_.Group -contains $ex) { $true break } } if (-not $excluded) { $true } } else { $true } } if ($PSCmdlet.ParameterSetName -eq 'PublishAzureDeployment') { $launched = . $asJobOrElevate $MyInvocation.MyCommand -additionalModules $theModulePaths -Parameter $psBoundParameters -RequireAdmin $null = $launched if (-not $psBoundParameters.DeploymentName) { $DeploymentName = if ($Name) { $name } elseif ($Group) { $group } else { "MyPipeworksDeployment" } } $deploymentsToPublish | Import-Deployment | Publish-AzureService -DeploymentName $deploymentName -DeploymentDirectory $DeploymentDirectory -VMSize $vmSize -InstanceCount $InstanceCount -Os $os -AsJob -Wait } elseif ($PSCmdlet.ParameterSetName -eq 'PublishToBlobStorage') { $jobs = @() $deploymentsToPublish | Import-Deployment | ForEach-Object { $item = $_ if ($asjob) { $jobs += Start-Job -ArgumentList $item, $storageAccount, $storageKey -ScriptBlock { param($item, $storageAccount, $storageKey) Import-Module Pipeworks -Force $item | Split-Path | Get-ChildItem -Recurse | Out-Zip -ZipFile $home\Documents\WindowsPowerShell\LatestDeployment\$($item.Name).zip $zipFile = Get-Item "$home\Documents\WindowsPowerShell\LatestDeployment\$($item.Name).zip" Get-Item $zipFile | Export-Blob -StorageAccount $StorageAccount -StorageKey $StorageKey -Container "$($zipFile.Name.Replace(' ', '').Replace('.zip', '').Replace('.', ''))-Source" -Name "$([DateTime]::Now.ToShortDateString().Replace('/', '-')).zip" } #Start-Sleep -Milliseconds 100 } else { $item | Split-Path | Get-ChildItem -Recurse | Out-Zip -ZipFile $home\Documents\WindowsPowerShell\LatestDeployment\$($item.Name).zip $zipFile = Get-Item "$home\Documents\WindowsPowerShell\LatestDeployment\$($item.Name).zip" Get-Item $zipFile | Export-Blob -StorageAccount $StorageAccount -StorageKey $StorageKey -Container "$($zipFile.Name.Replace(' ', '').Replace('.zip', '').Replace('.', ''))-Source" -Name "$([DateTime]::Now.ToShortDateString().Replace('/', '-')).zip" } } if ($Wait) { $runningJobs = @($jobs) while ($runningJobs.Count) { if ($jobs) { $jobs | Receive-Job } $runningJobs = @($jobs | Where-Object { $_.State -eq 'Running' }) Write-Progress "Waiting for Jobs to Complete" "$($runningJobs.Count) Running" } } else { $jobs } } elseif ($PSCmdlet.ParameterSetName -eq 'PublishToAzureVM' -or $PSCmdlet.ParameterSetName -eq 'PublishToLan') { Import-Module Azure -Global if ($PSCmdlet.ParameterSetName -eq 'PublishToAzureVM') { $vmList = @(Get-AzureVM | ForEach-Object { $_.Name + ".CloudApp.net" } ) if ($ComputerName) { $computerName = foreach ($v in $vmList) { foreach ($cn in $ComputerName) { if ($v -like $cn) { $v } } } $ComputerName = $ComputerName | Select-Object -Unique } else { $computerName = $vmList } } $deploymentsToPublish | Import-Deployment | ForEach-Object -Begin { $sb = { param([string]$moduleName, [byte[]]$moduleZipBytes) Add-Type -AssemblyName System.Web $destModuleDir = "$env:UserProfile\Documents\WindowsPowerShell\Modules\$($moduleName)" $tempDir = [IO.Path]::GetTempPath() $theFile = Join-Path $tempDir "$($moduleName)$(Get-Random).zip" [IO.FILE]::WriteAllBytes("$theFile", $moduleZipBytes) }.ToString() + @" function Expand-Zip { $((Get-Command Expand-Zip).Definition) } "@ + { if (Test-Path $destModuleDir) { $destModuleDir | Remove-Item -Recurse -Force } $null = New-Item -ItemType Directory -path $destModuleDir -ErrorAction SilentlyContinue Expand-Zip -ZipPath "$theFile" -OutputPath $destModuleDir Remove-Item -Path "$theFile" -Force } $syncScript = [ScriptBlock]::Create($sb) $jobs = @() } -Process { $tempFile = Join-Path $env:TEMP "$($_.Name)$(Get-Random).zip" $module = $_ $module | Split-Path | Get-ChildItem -Recurse -Force | Out-Zip -ZipFile $tempFile $zipBytes = [IO.File]::ReadAllBytes($tempFile) $jobs += foreach ($cn in $ComputerName) { Invoke-Command -ComputerName $cn -Credential $Credential -Authentication Credssp -ScriptBlock $syncScript -AsJob -JobName $_.Name -ArgumentList $module.Name, $zipBytes } $runningJobs = @($jobs | Where-Object {$_.State -eq 'Running' }) $maxRunning = $runningJobs | Group-Object ComputerName -NoElement | Sort-Object Count -Descending | Select-Object -First 1 -ExpandProperty Count if ($maxRunning -ge $BatchSize) { while ($jobs | Where-Object { $_.State -eq 'Running'}) { $jobs | Receive-Job Start-Sleep -milliseconds 250 } } Remove-Item -Path $tempFile -Force } -End { while ($jobs | Where-Object { $_.State -eq 'Running'}) { $jobs | Receive-Job Start-Sleep -milliseconds 250 } } if ($SecureSetting) { $settings = @(foreach ($s in $SecureSetting) { Get-SecureSetting $s -Decrypted }) $settings | ForEach-Object -Begin { $setecAstronomy = "" } { $s = $_ if (-not $s.Name) { continue } if ($s.Type -eq [string]) { $setecAstronomy += "Add-SecureSetting -Name '$($s.Name)' -String '$($s.DecryptedData.Replace("'", "''"))' " } elseif ($s.Type -eq [Hashtable]) { $setecAstronomy += " `$data = $($s.DecryptedData | Write-PowershellHashtable) Add-SecureSetting -Name '$($s.Name)' -Hashtable `$data " } elseif ($s.Type -eq [Management.Automation.PSCredential]) { $setecAstronomy += " `$p = ConvertTo-SecureString -String '$($s.DecryptedData.GetNetworkCredential().Password.Replace("'", "''"))' -AsPlainText -Force Add-SecureSetting -Name '$($s.Name.Replace("_Password", ''))' -Credential (New-Object Management.Automation.PSCredential '$($s.DecryptedData.UserName.Replace("'", "''"))', `$p) " } } -End { } $setecAstronomy = [ScriptBlock]::Create(" Import-Module Pipeworks $setecAstronomy ") $jobs += foreach ($cn in $ComputerName) { Invoke-Command -ComputerName $cn -Credential $Credential -Authentication Credssp -ScriptBlock $setecAstronomy -AsJob -JobName "SetecAstronomy" } while ($jobs | Where-Object { $_.State -eq 'Running'}) { $jobs | Receive-Job Start-Sleep -milliseconds 250 } } } } } |