Backing up TFS Build and Release Definitions

We’ve chosen to use TFS 2015update3 to release and build our code.  As such I’ve found that I have a great many build and releases that are configured in TFS that I often make changes to.  So I’d like to keep a running set of configurations so that I can restore from a previous copy.  This post is about how I did this backup of build and release definitions with Powershell.

To begin with I need to figure out how I call TFS with it’s api.  Since all of these calls are made via rest we can use PowerShell’s cmdlet Invoke-RestMethod.  So the first thing I need to do is get all my projects so I can loop through them.

function Get-TFSProjects
{
 Param($tfsUrl,$apiversion = '3.0-preview')
 Invoke-RestMethod -method get -UseDefaultCredentials -uri "$tfsurl/_apis/projects?api-version=$apiversion"
}  

So to call this all I need to do is call my function:

 get-TFSProjects -tfsurl "http://mytfsinstance.com:8080/tfs/defaultcollection"  

Which then returns me an object with the projects:

count value 
----- ----- 
 18 {@{id=[guid]; name=Marketing; url=http://mytfsinstance.com:8080/tfs/defaultcollection/_apis/projects/[guid]; state=wellFormed; revision=3887163}, @{id=83ed4f89- 

This indicates a guid that is specific to my instance of tfs.[guid].

Now that I have the projects for my collection/instance of tfs. I can now loop through them and get each build definition, all my function expects is my $tfsprojects that  I gathered from the first function:


function Backup-TFSbuildDefinitions
{
 param([object]$tfsprojs,$tfsurl = 'mytfsinstance.com', $tfscollection = 'Defaultcollection', $apiversion = '3.0-preview',[string]$Path = 'c:\temp\tfsbuilds')
 #"$Uri/$DefaultCollection/$TeamProject/_apis/build/definitions?api-version=2.0&name=$buildName"
 $tfsInstance = "http://$($tfsurl):8080/tfs/$tfsCollection"

foreach($tfsproj in $tfsprojs.value)
 { 
 $tfsProjName = $tfsproj.name
 $tfsdev = "$tfsInstance/$tfsProjName"
 $projectIds = Invoke-RestMethod -Method Get -UseDefaultCredentials -uri "$tfsdev/_apis/build/definitions?api-version=$apiversion" -ContentType application/json 
 foreach($projectid in $projectIds)
 {
 $relnumber = $projectid.value.id 
 foreach($rel in $relnumber)
 {
 $relDef = invoke-restmethod -method get -UseDefaultCredentials -uri "$tfsdev/_apis/build/definitions/$($rel)?api-version=$apiversion" -ContentType application/json
 $exportPath = "$path/$tfsProjName"
 if(-not (test-path $exportPath))
 {
 mkdir $exportPath
 }
 $jsonDoc = $reldef | convertto-json -Depth 100
 $jsonDoc | out-file -FilePath "$exportPath\$($reldef.name).json"
 }

 }
 }

}

Now all I need to do is look at the api reference and use the appropriate call to get the release configuration.


function Backup-TFSReleaseDefinitions
{
 param([object]$tfsprojs,$tfsurl = 'mytfsinstance.com', $tfscollection = 'Defaultcollection', $apiversion = '3.0-preview',[string]$Path = 'c:\temp\tfsprojects')
 $tfsInstance = "http://$($tfsurl):8080/tfs/$tfsCollection"

foreach($tfsproj in $tfsprojs.value)
 { 
 $tfsProjName = $tfsproj.name
 $tfsdev = "$tfsInstance/$tfsProjName"
 $projectIds = Invoke-RestMethod -Method Get -UseDefaultCredentials -uri "$tfsdev/_apis/release/definitions?api-version=$apiversion" -ContentType application/json 
 foreach($projectid in $projectIds)
 {
 $relnumber = $projectid.value.id 
 foreach($rel in $relnumber)
 {
 $relDef = invoke-restmethod -method get -UseDefaultCredentials -uri "$tfsdev/_apis/release/definitions/$($rel)?api-version=$apiversion" -ContentType application/json
 $exportPath = "$path/$tfsProjName"
 if(-not (test-path $exportPath))
 {
 mkdir $exportPath
 }
 $jsonDoc = $reldef | convertto-json -Depth 100
 $jsonDoc | out-file -FilePath "$exportPath\$($reldef.name).json"
 }

 }
 }

}

Now if I stitch it all together I can call my tfs instance and save the configurations all to my local disk structure.  From where the script is called.

Here is the full script on my gist:


function Backup-TFSReleaseDefinitions
{
param([object]$tfsprojs,$tfsurl = 'mytfsinstance.com', $tfscollection = 'Defaultcollection', $apiversion = '3.0-preview',[string]$Path = 'c:\temp\tfsprojects')
$tfsInstance = "http://$($tfsurl):8080/tfs/$tfsCollection"
foreach($tfsproj in $tfsprojs.value)
{
$tfsProjName = $tfsproj.name
$tfsdev = "$tfsInstance/$tfsProjName"
$projectIds = Invoke-RestMethod Method Get UseDefaultCredentials uri "$tfsdev/_apis/release/definitions?api-version=$apiversion" ContentType application/json
foreach($projectid in $projectIds)
{
$relnumber = $projectid.value.id
foreach($rel in $relnumber)
{
$relDef = invoke-restmethod method get UseDefaultCredentials uri "$tfsdev/_apis/release/definitions/$($rel)?api-version=$apiversion" ContentType application/json
$exportPath = "$path/$tfsProjName"
if(-not (test-path $exportPath))
{
mkdir $exportPath
}
$jsonDoc = $reldef | convertto-json Depth 100
$jsonDoc | out-file FilePath "$exportPath\$($reldef.name).json"
}
}
}
}
<#
.Synopsis
Gets all the TFS projects for a given instance
.EXAMPLE
$tfsProjects = get-TFSProjects -tfsurl 'http://yourinstance.com:8080/tfs/Defaultcollection&#39;
.OUTPUTS
Returns an object of projects found in tfs.
count value
—– —–
19 {@{id=f7039238-b489-45d1-b9d1-c4c9222497c3; name=Marketing; url=http://yourinstance.com:8080/tfs/DefaultCollection/_apis/projects/[guid]; state=wellFormed; revisi…
#>
function Get-TFSProjects
{
Param($tfsUrl,$apiversion = '3.0-preview')
Invoke-RestMethod method get UseDefaultCredentials uri "$tfsurl/_apis/projects?api-version=$apiversion"
}
function Backup-TFSbuildDefinitions
{
param([object]$tfsprojs,$tfsurl = 'mytfsinstance.com', $tfscollection = 'Defaultcollection', $apiversion = '3.0-preview',[string]$Path = 'c:\temp\tfsbuilds')
#"$Uri/$DefaultCollection/$TeamProject/_apis/build/definitions?api-version=2.0&name=$buildName"
$tfsInstance = "http://$($tfsurl):8080/tfs/$tfsCollection"
foreach($tfsproj in $tfsprojs.value)
{
$tfsProjName = $tfsproj.name
$tfsdev = "$tfsInstance/$tfsProjName"
$projectIds = Invoke-RestMethod Method Get UseDefaultCredentials uri "$tfsdev/_apis/build/definitions?api-version=$apiversion" ContentType application/json
foreach($projectid in $projectIds)
{
$relnumber = $projectid.value.id
foreach($rel in $relnumber)
{
$relDef = invoke-restmethod method get UseDefaultCredentials uri "$tfsdev/_apis/build/definitions/$($rel)?api-version=$apiversion" ContentType application/json
$exportPath = "$path/$tfsProjName"
if(-not (test-path $exportPath))
{
mkdir $exportPath
}
$jsonDoc = $reldef | convertto-json Depth 100
$jsonDoc | out-file FilePath "$exportPath\$($reldef.name).json"
}
}
}
}

I hope this helps someone

 

Until then

Keep scripting

 

thom

Advertisement

Adding a Retention Tag / Custom Folder / Exchange

At the company I work for we have begun the task of moving users to Exchange online.  As such we discovered we needed to add a policy that sets the retention policy on a folder to some value specified by the online exchange administrator.  This Post is about how I was able to piece together some scripts  from this post and come up with something where I could apply this policy on any folder I found with a specific name. I by no means am an Exchange expert so bear with me as I do my best to explain.

To start with if we browse to my Exchange and look at compliance management then retention policy’s I’ve set a test retention policy as I want the contents of a folder to be held for X time period.

This is my Retention tag and what I called it TestRetention

2017-02-08 15_12_13-retention tags - Microsoft Exchange.png

Here I’ve associated my tag with my Policy:

2017-02-08-14_56_43-retention-policies-microsoft-exchange

Here I’m showing that my user has the retention policy set that has my tag in it.

2017-02-08 15_11_04-mailboxes - Microsoft Exchange.png

Now onto the scripts that I started Stamping Retention Policy Tag and Script to recreate “managed folders”.

In the example they show you how to connect to the on premise exchange server.  To connect to an exchange online instance just had to modify the code to this:

$ImpersonationCreds = Get-Credential -Message "Enter Credentials for Account with Impersonation Role..."
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri $connectionUri -Authentication Basic -Credential $ImpersonationCreds
Import-PSSession $Session

Where the connection uri is to exchange online: ‘https://outlook.office365.com/powershell-liveid/&#8217;

This session brings in all the cmdlets that I’ll need to use for configuring using what is called PowerShell Implicit remoting. Now since I have the cmdlets for Exchange online I can now work on the mailbox I need to make this change on.

$mailboxes = get-content $TargetMailboxes
   $Version = "Exchange2013_SP1"
    $returnStatus =@()
    Add-Type -Path $ApiPath
    $ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::$Version
    $Service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion)
    $Creds = New-Object System.Net.NetworkCredential($ImpersonationCreds.UserName, $ImpersonationCreds.Password)
    $RetentionPeriod = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x301A,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer)
    $RetentionFlags = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x301D,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer)
    $PolicyTag = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x3019,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary) 

The $ExchangeVersion sets my version that I’m going to use.   In order to get to the Retention flag and policy I need to declare objects that contain those items shown in the pictures above.  Another good post on how we are connecting and looking to accomplish is posted here. Now onto the meat of the post.  The function I wrote to search for folders in TargetMailbox.

I chose to call this function Get-Mailbox folders. The function expects a Exchange service object, a Valid SMTP Mail box, and a Folder2Find.

The assumption is that whomever is running this script has the proper credentials to get to this mailbox.   To be able to find objects in the mailbox we must get an object that allows us to see the Folderviews and set a value for how many we wish to find. In addition we need to tell the Exchange dll how far to traverse the mailbox this is done by setting an enum value on the FolderView object. Now that we have told the dll that we want a folder vview and we want to traverse the folder view a 1000 deep.  We need to tell the Dll what the folder we want to start with. This is done by creating the folder ID object with the folder root, again using an enum value, this enum value we’ve chosen is the Root folder.

$fvFolderView = new-object Microsoft.Exchange.WebServices.Data.FolderView(1000)
  $fvFolderView.Traversal = [Microsoft.Exchange.WebServices.Data.FolderTraversal]::Deep
  $folderid = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$targetMailbox)

Now that we have the object created we need to bind to the folder that was created. This is so that we can call the search method for that folder. This is done by calling the class [Microsoft.Exchange.WebServices.Data.Folder] and the corresponding method Bind.

 $tfTargetFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$folderid) 

This binding allows us to call the method to find the folders that we wish to find. The find folders method expects an object that specifies the folder view which we defined earlier.

 $findFolderResults = $tfTargetFolder.FindFolders($fvFolderView) 

Now all we need to do is go thru each one of the folders  with the $findFolderResults. I chose to retrieve the parent folder and the folder id and the type for the folder of ‘IPF.Note’.

function Get-MailBoxfolders
{
  [CmdletBinding()]
  param
  (
    [Parameter(Mandatory=$true, Position=0, HelpMessage='A service that points to exchange instance you wish to query')]
    [Microsoft.Exchange.WebServices.Data.ExchangeService]$Service,
    [Parameter(Mandatory=$true, Position=1, HelpMessage='A mailbox (smtp) that the service has access to')]
    [string]$targetMailbox,
    [string]$Folder2Find
  )
Write-Verbose -Message "create an object that gets the root folder for the mailbox"
  $fvFolderView = new-object Microsoft.Exchange.WebServices.Data.FolderView(1000)
  $fvFolderView.Traversal = [Microsoft.Exchange.WebServices.Data.FolderTraversal]::Deep
  $folderid = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$targetMailbox) 

  $tfTargetFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$folderid)

  $findFolderResults = $tfTargetFolder.FindFolders($fvFolderView)

  foreach($folder in $findFolderResults.Folders){
    if($folder.FolderClass -eq 'IPF.Note')
    {
      $parentfolder = ($findFolderResults.Folders |?{$_.id.uniqueid -eq $folder.ParentFolderId.UniqueId}).displayname
      if(-not $parentfolder)
      {$parentfolder = 'Root'}
      if($Folder2Find)
      {
        if($folder.DisplayName -eq $folder2find)
        {
              [pscustomobject] @{
            'name'= $folder.DisplayName
            'folderid' = $folder.Id.UniqueId
            'ParentFolderName' = $parentfolder
            'ParentFolderId' = $folder.ParentFolderId.UniqueId
            'folderclass' = $folder.FolderClass
          }
        }

      }
      else
      {
        [pscustomobject] @{
          'name'= $folder.DisplayName
          'folderid' = $folder.Id.UniqueId
          'ParentFolderName' = $parentfolder
          'ParentFolderId' = $folder.ParentFolderId.UniqueId
          'folderclass' = $folder.FolderClass
        }
    }
  }
  }
  } 

For the full script source see this Gist 

At the end of the script run this is how my folders look in Outlook based on my tagging.

2017-02-09-09_52_52-unread-mail-outlook

I Hope this helps someone.

Until then

Keep Scripting

thom

WMF 5.1 now available

Richard Siddaway's Blog

The download for WMF 5.1 for down level operating systems is now available:

https://blogs.msdn.microsoft.com/powershell/2017/01/19/windows-management-framework-wmf-5-1-released/

WMF 5.1 can be installed on Windows 7 and 8.1 plus Windows Server 2008 R2, 2012, 2012 R2

Windows 10 and Server 2016 already have PowerShell 5.1 and don’t need this install.

if installing on Windows 7 or Server 2008 R2 the installation process has changed – READ THE RELEASE NOTES OR BE PREPARED FOR A LOT OF EXTRA EFFORT

View original post

Copying PowerShell object

Lately I’ve needed to take a PowerShell object and use it in several places in a JSON document that PowerShell nicely put in a custom object for me.  What I needed this object to do was to allow for a set of each one of the properties and they needed to be different for each time I added it to the JSON object.   To get this to work I tried several different means. This post is about how I  worked to solve this issue.

First we’ll start with a customobject that comes from JSON

$tasks2add = $tasks = $null
$taskjson = @'
[
 {
 "taskId": "1",
 "name": "Server-Scommaintenance",
 "enabled": false,
 "inputs": {
 "servers": "$(serverMonitors) ",
 "webMonitors": "$(webMonitors)",
 "MinuteValue": "2000",
 "maintValue": "inMaint"
 }
 },
 {
 "taskId": "2",
 "name": "Server-Scommaintenance",
 "enabled": false,
 "inputs": {
 "servers": "$(serverMonitors) ",
 "webMonitors": "$(webMonitors)",
 "emailusers": "$(ScomNotify)",
 "MinuteValue": "2000",
 "maintValue": "RemoveMaint"
 }
 }
]
'@
$tasks2add = $taskjson|convertfrom-json

Now if  look at my variable $tasks2Add we’ll see that it has all the items in the custom json above:

$tasks2add = $taskjson|convertfrom-json 

PS PS:\> $tasks2add

taskId name                   enabled inputs                                                                                                                       
------ ----                   ------- ------                                                                                                                       
1      Server-Scommaintenance   False @{servers=$(serverMonitors) ; webMonitors=$(webMonitors); MinuteValue=2000; maintValue=inMaint}                              
2      Server-Scommaintenance   False @{servers=$(serverMonitors) ; webMonitors=$(webMonitors); emailusers=$(ScomNotify); MinuteValue=2000; maintValue=RemoveMaint}

Now if I take that same set of objects and add it to another variable and then set each one. Lets see what the output looks like:

$newArraylist = new-object System.Collections.Generic.List[system.object]
$newArraylist.Add((New-object pscustomobject ($tasks2add[0])))
$newArraylist.Add((New-object pscustomobject ($tasks2add[1])))
$newArraylist.Add((New-object pscustomobject ($tasks2add[0])))
$newArraylist.Add((New-object pscustomobject ($tasks2add[1])))
#$newArraylist.count

$newArraylist[0].enabled = $true
$newArraylist[1].enabled = $false
$newArraylist[2].enabled = $false
$newArraylist[3].enabled = $true
$newArraylist

Here is what my output looks like:

2017-01-24-08_26_03-clipboard

You would expect that the first and second tasks would be set to $true and $false respectively as I set them with the $newArraylist[x].enabled = $true / $false.

So what happened here.  PowerShell takes the array object and points (references) the values in the object to the first created object.  So we aren’t really getting a copy we are getting a reference to the first created object.  After much gnashing of teeth and trying several different methods I finally came to a solution that is described in this PowerShell QA post.

To get this to work in the fashion I wanted which is I want each one of the copy’s of the new object to be settable independently I had to use the psobject property of my custom object. I’ll do this with the method called copy on the psobject property.

$tasks2add[1].PSObject.copy()

This makes the code much shorter and solves my issue where I can now set my custom objects like I’d like them to be.

$newArraylist = new-object System.Collections.Generic.List[system.object]
$newArraylist.Add($tasks2add[0].PSObject.Copy())
$newArraylist.Add($tasks2add[1].PSObject.Copy())
$newArraylist.Add($tasks2add[0].PSObject.Copy())
$newArraylist.Add($tasks2add[1].PSObject.Copy())
#$newArraylist.count

$newArraylist[0].enabled = $true
$newArraylist[1].enabled = $false
$newArraylist[2].enabled = $false
$newArraylist[3].enabled = $true
$newArraylist

Now if I look at my object it is now in the condition I want where I can set each item I add to my array list.

2017-01-24 08_41_04-Clipboard.png

I Hope this helps someone.

Until then

Keep Scripting

thom

Deploying a Sharepoint App to Sharepoint Online

This article is about how I was able to use the SharePoint Modules to successfully deploy an application to SharePoint Online.

I’ve used some scripting before to update items in Sharepoint.  This blog article is how I took the build that IOZ tools creates and deploy it to Sharepoint Online.

First I needed to start with downloading the latest copy of SharePointPnPPowerShellOnline.

I discovered while using this module that there is the capability to add a PSDrive to my session.  This means I should be able to upload files to sharepoint as if it were a drive on my local machine.  Here is how you connect to sharpeoint online:

PS> Install-Module -Name SharePointPnPPowerShellOnline
$adminpassword = 'password'
$adminUserName = 'mysharepoint@onmicrosoft.com'
$creds = $AdminPassword | ConvertTo-SecureString -AsPlainText -Force
$SPdevcredentials = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $AdminUserName, $creds
connect-pnponline -Url $url -Credentials $SPdevcredentials -CreateDrive

Now that I have a connection to the SharePoint online instance I can see that I have a new powershell drive through the get-psdrive cmdlet:
get-psdrive

2017-01-10-07_33_05-clipboard

As you can see I have a new drive that is configured for use in my session, directorying the SPO: drive will get the contents of the SharePoint Site:

2017-01-10-07_49_31

My Applications are in my AppCatalog folder to get to that folder all I need to do is issue a CD to that directory.   To upload my App to this folder all I need to do is add it with Add-pnpFile.

2017-01-10 07_56_30.png

I seemed to have the best success when I used Get-Item (gi) and then used the fullname property for the file that I was sending to SharePoint. In addition one other gotcha was that the folder to upload to is a subfolder of the site you are connected to. In my case \sites\apps was my site I was connected to so specifying appcatalog was all I needed.

Now all that I needed to do was to put this in a script that  I could call from my CI automation and put some Error logic.  Now I have a full fledged script called deployspapp.ps1.   Full Source is found on my Gist:


param
(
[String]
[Parameter(Mandatory)]
$SourceFolder,
[String]
$Destination ,
[string]
$AdminUserName,
[string]
$AdminPassword
)
$creds = $AdminPassword | ConvertTo-SecureString -AsPlainText -Force
$SPdevcredentials = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $AdminUserName, $creds
#invoke-webrequest -uri $url -Credential $SPdevcredentials
if($MyInvocation.MyCommand.Path)
{
$scriptpath = $MyInvocation.MyCommand.Path
$d = Split-Path $scriptpath
write-output -InputObject "Script Directory -->$D"
pushd
cd $d
import-module .\SharePointPnPPowerShellOnline -DisableNameChecking -Force
popd
}
else{import-module .\SharePointPnPPowerShellOnline -DisableNameChecking -Force}
$u = $null
$isurl = [system.uri]::TryCreate($Destination,[system.urikind]::RelativeOrAbsolute,[ref]$u)
if(($U.Scheme -eq 'http' ) -or ($u.Scheme -eq 'https'))
{
if(get-command connect-pnponline -erroraction ignore)
{
$url = $u.AbsoluteUri
do #for($i = 1; $i -le $u.Segments.count; $i++)
{
try
{
connect-pnponline -Url $url -Credentials $SPdevcredentials -CreateDrive -ErrorAction Ignore
}
catch
{
"Error was $_"
$line = $_.InvocationInfo.ScriptLineNumber
"Error was in Line $line"
}
$i++
$url = $U.AbsoluteUri -replace (($u.segments | select -last $i ) -join ''), ''
}While((-not (get-psdrive -name spo -ErrorAction Ignore)) -and ($i -ne $u.segments.count) )
if(Get-PSDrive -Name spo -ErrorAction Ignore)
{
Write-Output "Drives = $((Get-PSDrive).name -join ',')"
Write-Output "Site root = $((get-pnpsite).url)"
if ($sourcefolder.EndsWith('\')){$sourcefolder = $sourcefolder.trimend('\')}
$apps= Get-ChildItem -Path "$SourceFolder"
Write-Output "Application to Deploy Filename: $($apps.fullname)"
foreach($app in $apps)
{
pushd
cd ($app ).directory
Add-PnPFile -Path $app.fullname -Folder ($Destination -replace (get-pnpsite).url, '')
write-output "uploaded $($app.fullname) to $destination"
popd
}
}
else
{ Throw "Cannot Connect to Sharepoint Online - $destination check username / password / url"}
}
}
else
{
throw "$destination url must be an http or https address"
}

view raw

deployspapp.ps1

hosted with ❤ by GitHub

I hope this helped someone

Until then keep scripting

Thom

Checking for valid Url using TryCreate

I’ve been working with Sharepoint online and needed to validate that a user is passing me a valid url.  So after doing some searching i found that [system.uri] had some methods that were useful.  In particular there is a method called TryCreate. This will take a url that you send to the method and let you know if it is a valid uri or not.

So first lets start with a url that we know is valid: Bing.com

TryCreate Expects Three values to be passed to it.

The first is a String or uri as you can see here.

The second is a System.urikind.   As you can see this is an enumeration that has three possible values, Absolute, Relative, and RelativeOrAbsolute

The third is a [ref]erence.  What this means is I must declare a variable for the return result to be put into after the method evaluates what was passed to it.

trycreate

Here is what that looks like in practice. Note I must use [ref] for my return result as it is used as a reference to get the results into.

$url = 'http:\\bing.com'
$kind = 'RelativeOrAbsolute'
$return = $null

[system.uri]::TryCreate($url,$kind,[ref]$return)

Now if I run this I’ll get the following output:

 $kind = 'RelativeOrAbsolute'
$return = $null

[system.uri]::TryCreate($url,$kind,[ref]$return)
True

Now if I look at my return variable we’ll notice that it has a full object with values.

 PS Z:\> $return


AbsolutePath : /
AbsoluteUri : http://bing.com/
LocalPath : /
Authority : bing.com
HostNameType : Dns
IsDefaultPort : True
IsFile : False
IsLoopback : False
PathAndQuery : /
Segments : {/}
IsUnc : False
Host : bing.com
Port : 80
Query : 
Fragment : 
Scheme : http
OriginalString : http:\\bing.com
DnsSafeHost : bing.com
IdnHost : bing.com
IsAbsoluteUri : True
UserEscaped : False
UserInfo :

Now I can use my return value and test to see if the value returned is of type HTTP or HTTPS.

if($return -like 'http*')
{
    write-output 'This is a http or https address'
}

This is a http or https address

You could write several other test’s to figure out what Scheme of uri the user passed. I’ll leave that up to your scripting.  Here is a link to the rest of the uri Schemes

 

 

I hope this helped someone

Until then keep scripting

Thom

Using PowerShell Class to Deploy Zip files

Recently I have been working with @developermj on a class that he wrote for deploying code to a server from a zip file.  This blog article is about how that code works.

To Start this off we need to gain access to the dot net classes that have the features for zipping and unzipping files in them:

System.IO.Compression & System.IO.Compression.FileSystem

These will get added with two statements using and Add-type

#requires -version 5.0
using namespace System.IO
using namespace System.IO.Compression
param(
 [Parameter(Mandatory=$true)][string]$sourceZip, 
 [Parameter(Mandatory=$true)][string]$destPath
)

add-type -assemblyname 'System.IO.Compression'
add-type -assemblyname 'System.IO.Compression.FileSystem'

Then we’ll build the first part of our utility which is our function to deploy the files. This function is where all the magic is:

function Deploy-Files {
 param(
 [ValidateNotNullOrEmpty()][FileInfo]$sourceZip,
 [ValidateNotNullOrEmpty()][DirectoryInfo]$destFolder
 )
 if (-not $sourceZip.Exists) {
 throw "Zip $($sourceZip.Name) does not exist"
 }
 [ZipArchive]$archive = [ZipFile]::Open($sourceZip, "Read")
 [DeployFile[]]$files = $archive.Entries | where-object {$_.Length -gt 0} `
| %{[ArchiveFile]::new($_)}
 if ($files.Length -eq 0) {
 Write-Information "No files to copy"
 }
 $hasWritten = $false
 foreach ($file in $files) {
 [FileInfo]$destFile = "$destFolder$($file.GetName())"
 $copied = $file.TryCopy($destFile)
 if ($copied) { $hasWritten = $true }
 }
 Write-Information "Done"
 if (-not $hasWritten) {
 Write-Information "...Nothing copied"
 }
}

Since the incoming object is of type Fileinfo we can find out if the file exists with this statement: if (-not $sourceZip.Exists) . If the sourcezip exists then we progress on through our function. Else we throw an exception.

Since we’ve imported the dot net classes for filecompression we now have an available type we can cast our $archive variable to [ZipArchive]. Since ZipArchive requires a stream we can open the zip file with the ZipFile class and stream it to the ZipArchive object.

Now that we have the entire contents for the archive in a variable $archive we can use apply our class to the variable.  Below is what the value of my $archive looks like.

[DBG]: PS ps:\>> $archive

Entries Mode
------- ----
{Code/, Code/Lib/, Code/Lib/ICSharpCode.SharpZipLib.dll, Code/Mindscape.Samples.Powershell.ZipProvider.csproj...} Read

[DBG]: PS ps:\>> $archive.entries.count
11

The next line in the code is where we’ll start using the Class we’ve defined in our script.

[DeployFile[]]$files = $archive.Entries `
| where-object {$_.Length -gt 0} | %{[ArchiveFile]::new($_)}

Since we are creating a new object of type [deployFile[]] Powershell will see this and instantiate a new object from our Class.  In the example above we are taking each archive entry and creating a new [ArchiveFile]. If we follow the code through this loop we’ll find the first data element that’s length is greater than 0 will be defined as a [Archivefile].

class ArchiveFile : DeployFile {
 hidden [ZipArchiveEntry]$entry

 ArchiveFile([ZipArchiveEntry]$entry) {
 $this.entry = $entry
 }

 [DateTime] GetModifiedDate() {
 return $this.entry.LastWriteTime.UtcDateTime
 }

 [void] Copy([FileInfo]$file) {
 [ZipFileExtensions]::ExtractToFile($this.entry, $file.FullName, $true)
 }

 [string] GetName() {
 return "\$($this.entry.FullName)"
 }
}

As you can see from the declaration for this class [ArchiveFile] inherits the [DeployFile] class. PowerShell will hit the constructor that matches what was passed to the class.   We passed a [ZipArchiveEntry]

Since this is now defined a new object it inherits all the methods that are declared in the class for this object type.  This object type has The following methods defined:

GetModifiedDate, Copy, GetName

It then inherits from the [DeployFile] from this inheritance it gets the following methods:

ShouldCopy, Copy, TryCopy, ToString

ArchiveFile([ZipArchiveEntry]$entry) {
 $this.entry = $entry
 }

If we continue to loop through each item in our intial $archive variable we’ll notice that we end up with a new Variable of type [DeployFile]. This $files variable is now of that type if we pipe the variable to get member we’ll see that we have a class name of [ArchiveFile]. if we look at the members of the $files of the array we’ll see the [Archivefile] class and the methods that were inherited from the other class [DeployFiles].

DBG]: PS ps:\>> $files[0] | gm

 TypeName: ArchiveFile

Name MemberType Definition 
---- ---------- ---------- 
Copy Method void Copy(System.IO.FileInfo file) 
Equals Method bool Equals(System.Object obj) 
GetHashCode Method int GetHashCode() 
GetModifiedDate Method datetime GetModifiedDate() 
GetName Method string GetName() 
GetType Method type GetType() 
ShouldCopy Method bool ShouldCopy(System.IO.FileInfo file)
ToString Method string ToString() 
TryCopy Method bool TryCopy(System.IO.FileInfo file)

Now that we have our class we can move onto deploying these files to the intended target. Which is what this next line of code does.

 foreach ($file in $files) {
 [FileInfo]$destFile = "$destFolder$($file.GetName())"
 $copied = $file.TryCopy($destFile)
 if ($copied) { $hasWritten = $true }
 }

The Foreach loops through each file and gets the destingation location plus the name of the file by calling the classes method getname().

 [DBG]: PS ps:\>> $file
\Code/Lib/ICSharpCode.SharpZipLib.dll

[DBG]: PS ps:\>> $file.getname()
\Code/Lib/ICSharpCode.SharpZipLib.dll

Now that we have a [Fileinfo] object we can now call the TryCopy method on our $file.  TryCopy Expects a type of [Fileinfo]

$copied = $file.TryCopy($destFile)

Which takes us to our class for file into it’s method TryCopy

  [bool] TryCopy([FileInfo]$file) {
 if ($this.ShouldCopy($file)) {
 [DeployFile]::CreateFolderIfNeeded($file)
 Write-Verbose "Copying to $($file.Name)"
 $this.Copy($file)
 return $true
 }

The first this is we are going to test to see if we should copy this file with the should Copy method on the same object ($this).

  [bool] ShouldCopy([FileInfo]$file) {
 if (-not $file.Exists) {
 return $true
 }

 if ($this.GetModifiedDate() -gt $file.LastWriteTimeUtc) {
 return $true
 }

 return $false
 }

This function will check to see if the file doesn’t exist with -not $file.exists. Then it checks to see what the modified date is.  if the Modified date is greater than the files last writetime in UTC. Then we are going to return true.  Which means that this file is newer and should be copied. Hence the function name should copy.  If both those tests fail then we’ll return false because the file exists and its timestamp is less than the lastwritetimeutc.

Now we return back to the TryCopy. Provided the return results of the try copy is true we’ll next check to see if we need to create a directory through a call to the class [DeployFile]::CreateFolderifNeeded([fileinfo]). This function is part of the deployfile class and will create a folder if it isn’t present for the file in question.

Now that the folder is created.  We can now call the copy function from the $file object.

This will copy the file to the destination filename based on the $file object.

Note:

I haven’t been able to get this script to run on it’s own without writing a wrapper script to then call this one.  I’ve posted an article about this on Powershell.org.

https://powershell.org/forums/topic/system-io-compression-in-powershell-class/

Here is what I have in my wrapper Script:

#requires -version 5.0
using namespace System.IO
using namespace System.IO.Compression
param(
 [Parameter(Mandatory=$true)][string]$sourceZip, 
 [Parameter(Mandatory=$true)][string]$destPath
)
add-type -assemblyname 'System.IO.Compression'
add-type -assemblyname 'System.IO.Compression.FileSystem'
& .\copy-code.ps1 -sourceZip $sourceZip -destpath $destpath

 

 

I hope this helps someone.

Until then keep scripting

Thom

Full copy of the script is in this Gist:


#requires -version 5.0
using namespace System.IO
using namespace System.IO.Compression
param(
[Parameter(Mandatory=$true)][string]$sourceZip,
[Parameter(Mandatory=$true)][string]$destPath
)
add-type assemblyname 'System.IO.Compression'
add-type assemblyname 'System.IO.Compression.FileSystem'
#region Functions
function Deploy-Files {
param(
[ValidateNotNullOrEmpty()][FileInfo]$sourceZip,
[ValidateNotNullOrEmpty()][DirectoryInfo]$destFolder
)
if (-not $sourceZip.Exists) {
throw "Zip $($sourceZip.Name) does not exist"
}
[ZipArchive]$archive = [ZipFile]::Open($sourceZip, "Read")
[DeployFile[]]$files = $archive.Entries | where-object {$_.Length -gt 0} | %{[ArchiveFile]::new($_)}
if ($files.Length -eq 0) {
Write-Information "No files to copy"
}
$hasWritten = $false
foreach ($file in $files) {
[FileInfo]$destFile = "$destFolder$($file.GetName())"
$copied = $file.TryCopy($destFile)
if ($copied) { $hasWritten = $true }
}
Write-Information "Done"
if (-not $hasWritten) {
Write-Information "…Nothing copied"
}
}
class DeployFile {
static [void] CreateFolderIfNeeded([FileInfo]$file) {
if (-not $file.Directory.Exists) {
Write-Verbose "Creating folder $($file.Directory.FullName)"
$file.Directory.Create()
}
}
[string] GetName() { throw "Name must be specified" }
[bool] ShouldCopy([FileInfo]$file) {
if (-not $file.Exists) {
return $true
}
if ($this.GetModifiedDate() -gt $file.LastWriteTimeUtc) {
return $true
}
return $false
}
[void] Copy([FileInfo]$file) {
throw "copy method not overridden"
}
[bool] TryCopy([FileInfo]$file) {
if ($this.ShouldCopy($file)) {
[DeployFile]::CreateFolderIfNeeded($file)
Write-Verbose "Copying to $($file.Name)"
$this.Copy($file)
return $true
}
return $false
}
[string] ToString() {
return $this.GetName()
}
}
class ArchiveFile : DeployFile {
hidden [ZipArchiveEntry]$entry
ArchiveFile([ZipArchiveEntry]$entry) {
$this.entry = $entry
}
[DateTime] GetModifiedDate() {
return $this.entry.LastWriteTime.UtcDateTime
}
[void] Copy([FileInfo]$file) {
[ZipFileExtensions]::ExtractToFile($this.entry, $file.FullName, $true)
}
[string] GetName() {
return "\$($this.entry.FullName)"
}
}
# For physical file support if wanted in the future
class PhysicalFile : DeployFile {
hidden [FileInfo]$entry
hidden [DirectoryInfo]$root
PhysicalFile([FileInfo]$entry, [DirectoryInfo]$rootFolder) {
$this.entry = $entry
$this.root = $rootFolder
}
[DateTime] GetModifiedDate() {
return $this.entry.LastWriteTimeUtc
}
[void] Copy([FileInfo]$file) {
$this.entry.CopyTo($file, $true)
}
[string] GetName() {
return $this.entry.FullName.Substring($this.root.FullName.Length)
}
}
#endregion Functions
$dotnetversion = [Environment]::Version 
if(!($dotnetversion.Major -ge 4 -and $dotnetversion.Build -ge 30319)) {            
  write-error "Version of DotNet must be greater than or equal to $($dotnetversion.Major).30319"    
exit(1
}
Deploy-files sourceZip $sourcezip destFolder $destPath

view raw

Copy-code.ps1

hosted with ❤ by GitHub

Updating Azure Alert Email

We have a number of Email’s setup for alerting that need to be changed. Rather than go to each alert and update their properties I chose to update each available alert in my subscriptions using PowerShell.  This post is about how I did that.

I will assume for the purposes of this post that you already are aware of the means to connect to Azure. If you aren’t familiar with that process see the article posted here.

The first thing I needed to figure out is how do I get my already configured alerts.  I chose to use the Cmdlet Get-AzureRmResource.  I then took the results of my query to find all the alerts in the current subscription context:

$alerts = get-AzureRmResource `
 | Where-Object{$_.resourcetype -like '*alert*'}

Now that I have all my resources that look like an alert I can now iterate through each and find the properties of each alert Get-AzureRmAlertRule:

foreach($alert in $alerts)
get-azureRmalertRule -Resourcegroup&nbsp;`
$alert.ResourceGroupName -Name $alert.Name
}
Properties : Microsoft.Azure.Management.Insights.Models.Rule
Tags : {[$type,
 Microsoft.WindowsAzure.Management.Common.Storage.CasePreservedDictionary,
 Microsoft.WindowsAzure.Management.Common.Storage], [hidden-link:/subscripti
 ons/xxx/resourceGroups/AzureTesting/provid
 ers/Microsoft.Web/serverfarms/EasyAuth, Resource]}
Id : /subscriptions/xxxx/resourceGroups/AzureTes
 ting/providers/microsoft.insights/alertrules/LongHttpQueue
Location : East US
Name : LongHttpQueue 

After some testing of this particular function I discovered that the extra switch of -DetailedOutput provided the detail I was looking for.

foreach($alert in $alerts)
get-azureRmalertRule -Resourcegroup&nbsp;`
$alert.ResourceGroupName -Name $alert.Name
}
Properties :
 Name: : LongHttpQueue EasyAuth
 Condition :
 DataSource :
 MetricName : HttpQueueLength
 ResourceId : /subscriptions/xxxxxxxx-xxxxxx-xxxxx-xxxxx-xxxxxxxxxx/re
 sourceGroups/AzureTesting/providers/Microsoft.Web/serverfarms/EasyAuth
 Operator : GreaterThan
 Threshold : 100
 Aggregation operator: Total
 Window size : 00:05:00
 Description : The HTTP queue for the instances of EasyAuth has a
 large number of pending requests.
 Status : Disabled
 Actions :
 SendToServiceOwners : True
 E-mails : 

Tags :
 $type :
 Microsoft.WindowsAzure.Management.Common.Storage.CasePreservedDictionary,
 Microsoft.WindowsAzure.Management.Common.Storage
 hidden-link:/subscriptions/xxxxxxxx-xxxxxx-xxxxx-xxxxx-xxxxxxxxxx/resourceGro
 ups/AzureTesting/providers/Microsoft.Web/serverfarms/EasyAuth:
 Resource
Id : /subscriptions/xxxxxxxx-xxxxxx-xxxxx-xxxxx-xxxxxxxxxx/resourceGroups/AzureTes
 ting/providers/microsoft.insights/alertrules/LongHttpQueue EasyAuth
Location : East US
Name : LongHttpQueue EasyAuth

Now I need to find out what the Email property was for this object I retrieved from the Get-AzureRmAlertRule.   If I inspect the object a little closer I find that there is a  sub Object called properties and then under that object I find another object that my Emails are associated to.   What I discovered through trial and error was that the Actions property was an array of settings.  The first item if set is the customEmails and whether or not an email should be sent upon alert activation (shown below).

PS PS:\azure> $t = get-azureRmalertRule -Resourcegroup `
'Azure Testing' -Name 'LongHttpQueue EasyAuth'
PS PS:\azure> $t.properties.Actions[0]

CustomEmails SendToServiceOwners
------------ -------------------
{} True

So this means if there are no emails set then the Array Count is Zero.  The other item that happens to be in the Action Object is whether or not a WebHook is set or not.  This can be seen by looking at the serviceuri in the actions object as shown below:

PS PS:\azure> $t =(get-azurermalertrule -name 'CPUHigh Dev' `
 -resourcegroup Dev -DetailedOutput)

PS PS:\azure> $t.properties.Actions | fl

Properties : {[$type, Microsoft.WindowsAzure.Management.Common.Storage.CasePreservedDict
 ionary`1[[System.String, mscorlib]],
 Microsoft.WindowsAzure.Management.Common.Storage]}
ServiceUri : https://s1events.azure-automation.net/webhooks?token=xxxx

CustomEmails : {email@email.com, email2@email.com}
SendToServiceOwners : True

On to how to change the email.  According to the blog article from Microsoft, you can only delete or add alert rules. I found this to be partially true.  In that if I already have an alert I can update it by just calling Add-AzurermMetricAlertRule.

Now to add email Items to the Add-AzurermMetricAlertRule you can do it two different ways:

The first way is use the Cmdlet Microsoft provides which creates an object of the precise thing you want and in the format the Add-AzurermMetricAlertRule expects:

$email = 'youremail@youremailServer.com'
$newEmailObj = new-azurermAlertRuleEmail -CustomEmails $email
add-azurermmetricalertrule -name $Name `
 -Location $Location -ResourceGroup $ResourceGroupName `
-operator ($alert.operator) -Threshold ($alert.threshold)`
 -TargetResourceId $alert.DataSource.ResourceUri`
 -MetricName $alert.DataSource.MetricName`
 -WindowSize $alert.WindowsSize`
 -TimeAggregationOperator $alert.TimeAggregation`
 -Description $targetResourceId.properties.Description`
 -Actions $newEmailObj

Or the other way you can do it is when you have the return result of alert already in an object you can use the .Add of the object to add an email to it.

$email = 'youremail@youremailServer.com'
$targetResourceId = (get-azurermalertrule -ResourceGroup `
$ResourceGroupName -Name $Name -DetailedOutput)
$actions = $targetResourceId.properties.Actions
 if($actions.count -eq 0)
 {
 $targetresourceId.properties.actions.add((`
new-azurermAlertRuleEmail -CustomEmails $email ))
 $targetresourceid.properties.actions`
[($targetresourceid.properties.actions.count -1)].SendToServiceOwners = $true
 $addedEmail = $true
 }
 else
 {
 $emailActions = $targetResourceId.properties.Actions.Count -1
 $emails = $actions[$emailActions].customemails
 if($emails -notcontains $email)
 {
 $targetResourceId.properties.actions[$emailActions].customemails.add($email)
 $addedEmail = $true
 }
 }

I chose to use the .add method as I’m doing this over and over again and it was to my advantage to use that method. Only when I have a case of there not being an alert ($actions.count -eq 0) do I use the New-AzureRmAlertRuleEmail.

I assume if there isn’t at least one item in $actions then it’s safe to add the email.

$emailActions = $targetResourceId.properties.Actions.Count -1
 $emails = $actions[$emailActions].customemails

I use $addedEmail to tell my function whether or not I need to add the email. This is because the the function will run these steps in a ForEach loop.

Now that I have a means to get the alert email and update it doing the converse is a matter of  changing the .Add method to a .Remove method and Bingo I have a add and a delete.  To see the entire script in action see this Gist. PS. I’m still working on the help. Will update the GIST as it is updated:


function Update-PAzureAlertEmail
{
<#
.SYNOPSIS
This function either adds or removes an email from Alerts in azure
.DESCRIPTION
This function will remove the enclosed email from all the alerts in an azure subscription.
If ADD is specified it will add the email to the all the alerts in an azure subscription.
When this function is called it will return an object of the items that were added or removed.
.EXAMPLE
Update-AzureAlertEmail -Subscription 'your subscription name' -email 'youremail@youremail.com'
This will go to every alert in the subscription passed and update with the youremail@youremail.com address.
default behaviour for -actions is to perform an Add.
.EXAMPLE
Update-AzureAlertEmail -email 'youremail@youremail.com'
This will go to every alert in all your subscriptions and add the email address that was specified.
.EXAMPLE
Update-AzureAlertEmail -email 'youremail@youremail.com' -action remove
This will go to every alert in all your subscriptions and remove the email address that was specified.
.EXAMPLE
Update-AzureAlertEmail -Subscription 'your subscription name' -email 'youremail@youremail.com' -action remove
This will go to every alert in the subscription passed and remove with the youremail@youremail.com address.
#>
[CmdletBinding()]
param
(
[Parameter(Position=0)]
[Object]
$Subscriptions = (Get-AzureRmSubscription),
[Parameter(Mandatory=$true)][string]$email,
[ValidateSet('Add', 'Remove')]
[string]$action = 'Add'
)
Begin
{
if((($Subscriptions | Get-Member).TypeName | Select-Object Unique) -eq 'Microsoft.Azure.Commands.Profile.Models.PSAzureSubscription')
{
$subs = $Subscriptions
}
else
{
$subs = get-AzureRmSubscription subscriptionname $subscriptions
}
$Updated = $null
$AlertsUpdated = @()
}
Process
{
ForEach ($SubName in $Subs)
{
$null = Set-AzureRmContext SubscriptionName ($SubName.SubscriptionName)
$alerts = get-AzureRmResource | Where-Object{$_.resourcetype -like '*alert*'}
#azure web tests https://azure.microsoft.com/en-us/blog/creating-a-web-test-alert-programmatically-with-application-insights/
foreach($alert in $alerts)
{
Write-Progress Activity Updating Status ('{0} ->' -f $action) PercentComplete ($alerts.IndexOf($alert)) CurrentOperation $alert.name
if($action -eq 'Add')
{
$Updated = Add-PAzureEmailAlertRule Subscriptionname $subname.subscriptionName ResourceGroupName $alert.ResourceGroupName location $alert.Location name $alert.Name email $email
$AlertsUpdated += $Updated
}
elseif($action -eq 'Remove')
{
$Updated = Remove-PAzureEmailAlertRule Subscriptionname $subname.subscriptionName ResourceGroupName $alert.ResourceGroupName location $alert.Location name $alert.Name email $email
$AlertsUpdated += $Updated
}
}
$AlertsUpdated
}
}
}
function Get-PAzureAlertEmail
{
<#
.SYNOPSIS
this function will get all alerts for the subscription passed.
.DESCRIPTION
This function will retreive all the alerts for the subscription passed. It will return an object with the Name of the alert
a property named emails that will contain the alerts for that email.
Resourcetype, ResourceGroupName, ResourceID, SubscriptionName, SubscriptionID and the location of the alert.
.EXAMPLE
get-pazurealertemail -Subscriptions 'azure testing'
This will get all the alerts for the azure testing subscription.
.EXAMPLE
get-pazurealertemail -Subscriptions 'azure testing' -email 'youremail@youremail.com' -find
This will search through all alerts to find an alert that has the email specified in it.
Each alert that is found it'll return them when the function completes.
#>
[CmdletBinding()]
param
(
[Parameter(Position=0)]
[Object]
$Subscriptions = (Get-AzureRmSubscription),
[string]$email,
[switch]$find
)
Begin
{
if((($Subscriptions | Get-Member).TypeName | Select-Object Unique) -eq 'Microsoft.Azure.Commands.Profile.Models.PSAzureSubscription')
{
$subs = $Subscriptions
}
else
{
$subs = get-AzureRmSubscription subscriptionname $subscriptions
}
}
Process
{
ForEach ($SubName in $Subs)
{
Set-AzureRmContext SubscriptionName ($SubName.SubscriptionName) | out-null
$alerts = get-AzureRmResource | Where-Object{$_.resourcetype -like '*alert*'}
#azure web tests https://azure.microsoft.com/en-us/blog/creating-a-web-test-alert-programmatically-with-application-insights/
foreach($alert in $alerts)
{
if($find -and $email -ne $null -or $email -ne '')
{$action = ('Searching for {0}' -f $email)}
else{$action = 'Gathering all alerts'}
Write-Progress Activity Searching Status ('{0} ->' -f $action) PercentComplete ($alerts.IndexOf($alert)) CurrentOperation $alert.name
#$targetResourceId = ((Get-AzureRmAlertRule -Name $alert[0].name -ResourceGroup $alert[0].ResourceGroupName).tags[1].key -replace 'hidden-link:','')
#https://docs.microsoft.com/en-us/azure/monitoring-and-diagnostics/insights-alerts-powershell#comments-container
$targetResourceId = get-azurermalertrule ResourceGroup $alert.ResourceGroupName Name $alert.Name DetailedOutput
$alertSetup = ($targetResourceId).properties.condition
$actions = $targetResourceId.properties.Actions
if($actions -eq 0)
{ $emails = ''}
else
{
$emailActions = $targetResourceId.properties.Actions.Count -1
$emails = $actions[$emailActions].customemails
}
if($find -and $email -ne $null -or $email -ne '')
{
if($emails -contains $email)
{
[PSCustomObject]@{
'Name' = $alert.Name
'Emails'= $emails
'ResourceType' = $alert.ResourceType
'ResourceGroupName' =$alert.ResourceGroupName
'ResourceId' = $alert.ResourceId
'SubscriptionName' = $subname.SubscriptionName
'SubscriptionId' = $Subname.SubscriptionId
'Location' = $alert.Location
} #PSCustomObject
}
}
else
{
[PSCustomObject]@{
'Name' = $alert.Name
'Emails'= $emails
'ResourceType' = $alert.ResourceType
'ResourceGroupName' =$alert.ResourceGroupName
'ResourceId' = $alert.ResourceId
'SubscriptionName' = $subname.SubscriptionName
'SubscriptionId' = $Subname.SubscriptionId
'Location' = $alert.Location
}
}
}
}
}
}
function Add-PAzureEmailAlertRule
{
<#
.SYNOPSIS
Adds an email for an alert in Azure.
.DESCRIPTION
This script will an an email to an existing alert. It will not add email addresses to Web Test type alerts or Smart Detection type alerts. It only operates on standard metric alerts.
.EXAMPLE
Add-PAzureAlertRule -subscriptionName 'your subscription' -resourcegroup 'resource group' -location 'US West' -name 'Name of the alert' -email 'emailToadd@youemailserver.com'
.EXAMPLE
Add-PAzureAlertRule -subscriptionName 'your subscription' -resourcegroup 'resource group' -location 'US West' -name 'Name of the alert' -email 'emailToadd@youemailserver.com'
#>
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
HelpMessage = 'Subscription Name that the alert is a part of')]
[ValidateNotNullOrEmpty()]
[string]$Subscriptionname,
[Parameter(Mandatory = $true,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
HelpMessage = 'Resource group name that the alert belongs to')]
[ValidateNotNullOrEmpty()]
[string]$ResourceGroupName,
[Parameter(Mandatory = $true,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
HelpMessage = 'Location in Azure where the alert is used.')]
[ValidateNotNullOrEmpty()]
[string]$location,
[Parameter(Mandatory = $true,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
HelpMessage = 'Actual name of the alert')]
[ValidateNotNullOrEmpty()]
[Alias('alertname')]
[string]$name,
[Parameter(Mandatory = $true,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
HelpMessage = 'Email Address that will be added to the alert')]
[ValidateNotNullOrEmpty()]
[string]$email
)
Begin
{
if((get-azurermcontext).Subscription.SubscriptionName -ne $Subscriptionname)
{
$null = set-azurermcontext Subscriptionname $Subscriptionname
}
}
Process
{
#$targetResourceId = ((Get-AzureRmAlertRule -Name $alert[0].name -ResourceGroup $alert[0].ResourceGroupName).tags[1].key -replace 'hidden-link:','')
#https://docs.microsoft.com/en-us/azure/monitoring-and-diagnostics/insights-alerts-powershell#comments-container
write-verbose Message "checking to see if an email is set. If not we'll set it and update the send to true"
$addedEmail = $false
$targetResourceId = (get-azurermalertrule ResourceGroup $ResourceGroupName Name $Name DetailedOutput)
#$alertSetup = ($targetResourceId).properties.condition
$actions = $targetResourceId.properties.Actions
$alertSetup = ($targetResourceId).properties.condition
if($actions.count -eq 0)
{
$targetresourceId.properties.actions.add((new-azurermAlertRuleEmail CustomEmails $email ))
$targetresourceid.properties.actions[($targetresourceid.properties.actions.count -1)].SendToServiceOwners = $true
$addedEmail = $true
}
else
{
$emailActions = $targetResourceId.properties.Actions.Count -1
$emails = $actions[$emailActions].customemails
if($emails -notcontains $email)
{
$targetResourceId.properties.actions[$emailActions].customemails.add($email)
$addedEmail = $true
}
}
#$sendtoServiceOwners = $actions[1].sendtoserviceowners
#$targetResourceId.properties.actions[$emailActions].CustomEmails
if($addedEmail)
{
if($targetResourceId.properties.Description -eq $null -or $targetResourceId.properties.Description -eq '')
{
$description = 'Added by Automation'
}
else
{
$description = $targetResourceId.properties.Description
}
Write-verbose Message ('updating {0} in ResourceGroup {1}' -f ($Name), ($ResourceGroupName))
write-verbose Message ('{0}' -f $Location)
if($alertSetup.threshold -ne $null -and $alertsetup.TimeAggregation -ne $null) #if operator is not present we assume it is a web test.
{
$found = Test-PAzureAlertSource Subscriptionname $subscriptionName ResourceGroupName $ResourceGroupName location $location name $name
#try{
# write-verbose "don't set the alert if we can't find the target"
# $null = get-azurermresource -resourceid $alertSetup.DataSource.ResourceUri
# $found = $true
# }
#catch{ $found = $false}
if($found)
{
add-azurermmetricalertrule name $Name Location $Location ResourceGroup $ResourceGroupName operator $alertSetup.operator Threshold ($alertSetup.threshold) TargetResourceId $alertSetup.DataSource.ResourceUri MetricName $alertSetup.DataSource.MetricName WindowSize $alertSetup.WindowsSize TimeAggregationOperator $alertsetup.TimeAggregation Description $description Actions $targetResourceId.properties.Actions #-Debug
}
else
{
$addedEmail = $false
write-warning Message ('cannot find ResourceID: {0}' -f $alertSetup.DataSource.ResourceUri)
}
}
elseif($alertsetup.TimeAggregation -eq $null)
{
write-verbose Message ('Time Aggregation is null cannot add email to {0}' -f $name)
$addedemail = $false
}
else
{
write-verbose Message ('An alert email cannnot be setup for {0} as it is a web test' -f $name)
#Add-AzureRmWebtestAlertRule -Name "webtestRule" -Location "East US" -ResourceGroup "Default-Web-EastUS" -WindowSize 00:05:00 -MetricName "metric" -TargetResourceUri ":/subscriptions/b67f7fec-69fc-4974-9099-a26bd6ffeda3/resourcegroups/Default-Web-WestUS/providers/microsoft.insights/webtests/leowebtestr1-webtestr1" -Description "Nice Webtest rule" -Failed 3
#add-AzureRmWebtestAlertRule -name $Name -Location $Location -ResourceGroup $ResourceGroupName -WindowSize $alertSetup.WindowSize -metricName $alertsetup.datasource.metricname -TargetResourceUri $alertSetup.DataSource.ResourceUri -Description $description -failed $alertSetup.FailedLocationCount #-Debug
$addedEmail = $false
}
}
if($addedEmail -eq $true)
{$targetResourceId}
else
{$targetresourceid =$null}
$targetresourceid
}
}
function Remove-PAzureEmailAlertRule
{
<#
.SYNOPSIS
Removes an email for an alert in Azure.
.DESCRIPTION
This script will remove an email from an existing alert. It will not remove email addresses from Web Test type alerts or Smart Detection type alerts. It only operates on standard metric alerts.
.EXAMPLE
Remove-PAzureAlertRule -subscriptionName 'your subscription' -resourcegroup 'resource group' -location 'US West' -name 'Name of the alert' -email 'emailToadd@youemailserver.com'
.EXAMPLE
Remove-PAzureAlertRule -subscriptionName 'your subscription' -resourcegroup 'resource group' -location 'US West' -name 'Name of the alert' -email 'emailToadd@youemailserver.com'
#>
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
HelpMessage = 'Subscription Name that the alert is a part of')]
[ValidateNotNullOrEmpty()]
[string]$Subscriptionname,
[Parameter(Mandatory = $true,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
HelpMessage = 'Resource group name that the alert belongs to')]
[ValidateNotNullOrEmpty()]
[string]$ResourceGroupName,
[Parameter(Mandatory = $true,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
HelpMessage = 'Location in Azure where the alert is used.')]
[ValidateNotNullOrEmpty()]
[string]$location,
[Parameter(Mandatory = $true,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
HelpMessage = 'Actual name of the alert')]
[ValidateNotNullOrEmpty()]
[Alias('alertname')]
[string]$name,
[Parameter(Mandatory = $true,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
HelpMessage = 'Email Address that will be removed from the alert')]
[ValidateNotNullOrEmpty()]
[string]$email
)
Begin
{
if((get-azurermcontext).Subscription.SubscriptionName -ne $Subscriptionname)
{
$null = set-azurermcontext Subscriptionname $Subscriptionname
}
}
Process
{
#$targetResourceId = ((Get-AzureRmAlertRule -Name $alert[0].name -ResourceGroup $alert[0].ResourceGroupName).tags[1].key -replace 'hidden-link:','')
#https://docs.microsoft.com/en-us/azure/monitoring-and-diagnostics/insights-alerts-powershell#comments-container
write-verbose Message "checking to see if an email is set. If not we'll set it and update the send to true"
$removedEmail = $false
$targetResourceId = (get-azurermalertrule ResourceGroup $ResourceGroupName Name $Name DetailedOutput)
#$alertSetup = ($targetResourceId).properties.condition
$actions = $targetResourceId.properties.Actions
$alertSetup = ($targetResourceId).properties.condition
$emailActions = $targetResourceId.properties.Actions.Count -1
$emails = $actions[$emailActions].customemails
if($emails -contains $email)
{
$targetResourceId.properties.actions[$emailActions].customemails.Remove($email)
$removedEmail = $true
}
#$sendtoServiceOwners = $actions[1].sendtoserviceowners
#$targetResourceId.properties.actions[$emailActions].CustomEmails
if($removedEmail)
{
if($targetResourceId.properties.Description -eq $null -or $targetResourceId.properties.Description -eq '')
{
$description = 'Added by Automation'
}
else
{
$description = $targetResourceId.properties.Description
}
Write-verbose Message ('updating {0} in ResourceGroup {1}' -f ($Name), ($ResourceGroupName))
write-verbose Message ('{0}' -f $Location)
if($alertSetup.threshold -ne $null -and $alertsetup.TimeAggregation -ne $null)
{
$found = Test-PAzureAlertSource Subscriptionname $subscriptionName ResourceGroupName $ResourceGroupName location $location name $name
#try{
# write-verbose "don't set the alert if we can't find the target"
# $null = get-azurermresource -resourceid $alertSetup.DataSource.ResourceUri
# $found = $true
# }
#catch{ $found = $false}
if($found)
{
add-azurermmetricalertrule name $Name Location $Location ResourceGroup $ResourceGroupName operator $alertSetup.operator Threshold ($alertSetup.threshold) TargetResourceId $alertSetup.DataSource.ResourceUri MetricName $alertSetup.DataSource.MetricName WindowSize $alertSetup.WindowsSize TimeAggregationOperator $alertsetup.TimeAggregation Description $description Actions $targetResourceId.properties.Actions #-Debug
$removedEmail = $true
}
else
{
$removedEmail = $false
write-warning Message ('cannot find ResourceID: {0}' -f $alertSetup.DataSource.ResourceUri)
}
}
elseif($alertsetup.TimeAggregation -eq $null)
{
write-verbose Message ('Time Aggregation is null cannot remove email from {0}' -f $name)
$removedEmail = $false
}
else
{
write-verbose Message ('An alert email cannnot be setup for {0} as it is a web test' -f $name)
#Add-AzureRmWebtestAlertRule -Name "webtestRule" -Location "East US" -ResourceGroup "Default-Web-EastUS" -WindowSize 00:05:00 -MetricName "metric" -TargetResourceUri ":/subscriptions/b67f7fec-69fc-4974-9099-a26bd6ffeda3/resourcegroups/Default-Web-WestUS/providers/microsoft.insights/webtests/leowebtestr1-webtestr1" -Description "Nice Webtest rule" -Failed 3
#add-AzureRmWebtestAlertRule -name $Name -Location $Location -ResourceGroup $ResourceGroupName -WindowSize $alertSetup.WindowSize -metricName $alertsetup.datasource.metricname -TargetResourceUri $alertSetup.DataSource.ResourceUri -Description $description -failed $alertSetup.FailedLocationCount #-Debug
$removedEmail = $false
}
}
if($removedEmail -eq $true)
{$targetResourceId}
else
{$targetresourceid = $null}
$targetresourceid
}
}
function Test-PAzureAlertSource
{
<#
.SYNOPSIS
Tests to see if the alert source is valid for an alert in Azure.
.DESCRIPTION
This function will return a $true if the alert source is valid and found in azure. If if it is found that the alert source is invalid then it'll will return a false.
.EXAMPLE
Test-PAzureAlertSource -subscriptionName 'your subscription' -resourcegroup 'resource group' -location 'USWest' -name 'Name of the alert'
.EXAMPLE
Test-PAzureAlertSource -subscriptionName 'your subscription' -resourcegroup 'resource group' -location 'USWest' -name 'Name of the alert'
#>
param
(
[Parameter(Mandatory = $true,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
HelpMessage = 'Subscription Name that the alert is a part of')]
[ValidateNotNullOrEmpty()]
[string]$Subscriptionname,
[Parameter(Mandatory = $true,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
HelpMessage = 'Resource group name that the alert belongs to')]
[ValidateNotNullOrEmpty()]
[string]$ResourceGroupName,
[Parameter(Mandatory = $true,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
HelpMessage = 'Location in Azure where the alert is used.')]
[ValidateNotNullOrEmpty()]
[string]$location,
[Parameter(Mandatory = $true,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
HelpMessage = 'Actual name of the alert')]
[ValidateNotNullOrEmpty()]
[Alias('alertname')]
[string]$name
)
Begin
{
if((get-azurermcontext).Subscription.SubscriptionName -ne $Subscriptionname)
{
$null = set-azurermcontext Subscriptionname $Subscriptionname
}
}
Process
{
$targetResourceId = (get-azurermalertrule ResourceGroup $ResourceGroupName Name $Name DetailedOutput)
#$alertSetup = ($targetResourceId).properties.condition
$valid = $false
try{
write-verbose Message "don't set the alert if we can't find the target"
$null = get-azurermresource resourceid $targetResourceId.properties.condition.datasource.resourceuri
$found = $true
}
catch{ $found = $false}
$found
}
}
function Remove-PAzureOrphanedAlerts
{
<#
.SYNOPSIS
Removes orphaned alerts from a subscription
.DESCRIPTION
This function will find all alerts in an azure subscription and remove them if the corresponding Asset that is being monitored no longer exists.
.EXAMPLE
Remove-PAzureOrphanedAlerts -Subscriptions 'Azure Testing'
Will return an object of all alerts that were removed.
.EXAMPLE
Remove-PAzureOrphanedAlerts
Will return an object of all alerts that have been removed for all subscriptions that the current user/entity can reach.
#>
[CmdletBinding(SupportsShouldProcess=$True, ConfirmImpact = 'Medium')]
param
(
[Parameter(Position=0)]
[Object]
$Subscriptions = (Get-AzureRmSubscription)
)
Begin
{
if((($Subscriptions | Get-Member).TypeName | Select-Object Unique) -eq 'Microsoft.Azure.Commands.Profile.Models.PSAzureSubscription')
{
$subs = $Subscriptions
}
else
{
$subs = get-AzureRmSubscription subscriptionname $subscriptions
}
$Updated = $null
$AlertsUpdated = @()
if ($ConfirmPreference -eq 'Low') {
<#
User:
* selected -Confirm
* has $Global:ConfirmPreference set to Low.
#>
$YesToAll = $false
}
else {
# No -Confirm, so we won't prompt the user…
$YesToAll = $true
}
# NoToAll is always $false – we want to give it a try…
$NoToAll = $false
}
Process
{
$action = 'Remove'
ForEach ($SubName in $Subs)
{
$null = Set-AzureRmContext SubscriptionName ($SubName.SubscriptionName)
$alerts = get-AzureRmResource | Where-Object{$_.resourcetype -like '*alert*'}
#azure web tests https://azure.microsoft.com/en-us/blog/creating-a-web-test-alert-programmatically-with-application-insights/
foreach($alert in $alerts)
{
$status = $null
Write-Progress Activity Updating Status ('from Subscription {1} {0} ->' -f $action,$subName.SubscriptionName) PercentComplete ($alerts.IndexOf($alert)) CurrentOperation $alert.name
if(!(Test-PAzureAlertSource Subscriptionname $SubName.SubscriptionName ResourceGroupName $alert.ResourceGroupName location $alert.location name $alert.name))
{
write-verbose "removing $($alert.name) from $($SubName.SubscriptionName) in Resource group: $($alert.ResourceGroupName) "
$targetResourceId = get-azurermalertrule ResourceGroup $alert.ResourceGroupName Name $alert.Name DetailedOutput
if($PSCmdlet.ShouldProcess("Removed the Alert – '$($alert.Name)'",
"Remove the alert '$($alert.Name)'?",
"Removing Alert" ))
{
if($Force -Or $PSCmdlet.ShouldContinue("Are you REALLY sure you want to remove '$($alert.Name)'?", "Removing '$($alert.Name)'", [ref]$YesToAll, [ref]$NoToAll))
{$status = Remove-AzureRmAlertRule ResourceGroup $alert.ResourceGroupName name $alert.name }
}
}
if($status -ne $null)
{$AlertsUpdated += $targetResourceId}
}
}
$AlertsUpdated
}
}
function Get-PAzureOrphanedAlerts
{
<#
.SYNOPSIS
Gets orphaned alerts from a subscription
.DESCRIPTION
This function will find all alerts in an azure subscription and return those orphaned alert names in an object.
.EXAMPLE
Get-PAzureOrphanedAlerts -Subscriptions 'Azure Testing'
Will return an object of all alerts that are orphaned.
.EXAMPLE
Remove-PAzureOrphanedAlerts
Will return an object of all alerts that are orphaned for each subscription that the user can get to.
#>
[CmdletBinding()]
param
(
[Parameter(Position=0)]
[Object]
$Subscriptions = (Get-AzureRmSubscription)
)
Begin
{
if((($Subscriptions | Get-Member).TypeName | Select-Object Unique) -eq 'Microsoft.Azure.Commands.Profile.Models.PSAzureSubscription')
{
$subs = $Subscriptions
}
else
{
$subs = get-AzureRmSubscription subscriptionname $subscriptions
}
$Updated = $null
$AlertsUpdated = @()
}
Process
{
ForEach ($SubName in $Subs)
{
$action = 'Get'
$null = Set-AzureRmContext SubscriptionName ($SubName.SubscriptionName)
$alerts = get-AzureRmResource | Where-Object{$_.resourcetype -like '*alert*'}
#azure web tests https://azure.microsoft.com/en-us/blog/creating-a-web-test-alert-programmatically-with-application-insights/
foreach($alert in $alerts)
{
$status = $null
Write-Progress Activity Getting Status ('from Subscription {1} {0} ->' -f $action,$subName.SubscriptionName) PercentComplete ($alerts.IndexOf($alert)) CurrentOperation $alert.name
if(!(Test-PAzureAlertSource Subscriptionname $SubName.SubscriptionName ResourceGroupName $alert.ResourceGroupName location $alert.location name $alert.name))
{
write-verbose "removing $($alert.name) from $($SubName.SubscriptionName) in Resource group: $($alert.ResourceGroupName) "
$targetResourceId = get-azurermalertrule ResourceGroup $alert.ResourceGroupName Name $alert.Name DetailedOutput
$status = $true
}
if($status -ne $null)
{$AlertsUpdated += $targetResourceId}
}
}
$AlertsUpdated
}
}
Function Format-OrphanedAlerts
{
<#
.SYNOPSIS
Takes an Orphand Alert Object and creates a flattened data set.
.DESCRIPTION
if Get-PAzureOrphanedAlerts is piped to this function it will return an object that is flattened. This object contains the following information:
Name of the alert
SourceResourceID = This is the thing that is being watched by the alert. Which is no longer in Azure (orphaned).
ResourceId = This is the resourceId that is associated with the alert.
emails = These are the email addresses that are associated with the Alert if present
Name : PulteWebSqlMasterLogIoPercent
SourceResourceId : /subscriptions/4a41ecf5-7683-49a5-8f1a-53ce28b8152a/resourceGroups/PulteWebProd/providers/Microsoft.Sql/servers/pulteweb/databases/PulteWebMaster
ResourceId : /subscriptions/4a41ecf5-7683-49a5-8f1a-53ce28b8152a/resourceGroups/PulteWebProd/providers/microsoft.insights/alertrules/PulteWebSqlMasterLogIoPercent
emails : youremailaddress@yourcompany.com
.EXAMPLE
Get-PAzureOrphanedAlerts -Subscriptions 'Azure Testing'
Will return an object of all alerts that are orphaned.
.EXAMPLE
Remove-PAzureOrphanedAlerts
Will return an object of all alerts that are orphaned for each subscription that the user can get to.
#>
param(
[object]$OrphanedAlerts
)
$formattedAlerts= $OrphanedAlerts | select @{n='Name';e={$_.properties.name}}, @{n='SourceResourceId';e={$_.properties.Condition.datasource.resourceuri}}, @{n = 'ResourceId';e={$_.id}}, @{n='emails';e={$_.properties.actions.customemails -join ','}}
$formattedAlerts
}
#select name,@{n="Emails";e={($_.emails) -join ','}},subscriptionName

I hope this helps someone out.

Until then keep scripting

thom

Deploying CRM with TFS 2015 tasks and PowerShell

Recently I was asked to put together some automation that would Deploy’s a  CRM solution with Solution Packager.  This Blog post is about how I did that.

I started with the documentation on Solution Packager.   What I found was that I could write a simple script that takes the source location for the Solution and packages it into a zip.  Here is what that run line looks like:

.\SolutionPackager.exe /action:Pack  `
/folder:'C:\CRM\CRM Solutions\default' /zipfile:c:\temp\myzip2.zip  `
/packagetype:unmanaged

So now to put a try catch around it and some other House keeping.  Since I’m going to call this from TFS I need to make certain that I have a way to allow for -debug and the other standard switches with a Powershell script so I’ll include [CmdletBinding()]. The full script is below:

[CmdletBinding()]
param
(
 [String]
 [Parameter(Mandatory)]
 $SourceFolder,
 [String]
 $zipfile = 'Crm'
)
$ErrorActionPreference = 'Stop'
$sourcefolder = (Get-Item $SourceFolder).FullName
"ZipfFileName: $sourcefolder\$zipfile.zip"
 Try
 {
 & .\SolutionPackager.exe /action:Pack /folder:$SourceFolder `
"/zipfile:$sourcefolder\$zipfile.zip" /packagetype:unmanaged 
 }
 catch
 { 
 Write-Error $_.Exception.Message
 exit 1
 }
 Get-ChildItem -Path "$sourcefolder\$zipfile.zip" -Verbose

Now that I have the Solution zipped up from what the developer checks into source I need a means to deploy it.   The Powershell Developer in me wanted to write a script of my own, I found that someone had already written this capability and all I needed to do was to add it to TFS.    Here is what I found:

A Developer by the name of Chaminda Chandrasekara created a Plugin to TFS (task) that does a solution import and activation of workflows.

Now with that in mind I added Chaminda’s code to my release process in TFS and then added the script that I created to my build process in TFS for the full solution.   I did need to create a task for my script shown above.  This was done by following the steps found here.

2016-11-03-09_28_25-microsoft-team-foundation-server

My build process steps consist of two steps.

Step 1 is to create the package. 2016-11-03-09_31_18-microsoft-team-foundation-server

Step 2 is to copy the artifacts to a Staging directory

2016-11-03-09_29_54-microsoft-team-foundation-server

Now onto the release process which also consists of two steps.

Step 1 is to do the solution import:

2016-11-03-09_41_05-release

In this setup I specify the name of the Zip file from the earlier build.  I have TFS variables that are defined in my Release steps identified by:

$(CRMUrl),$(CRMOrg), $(CRMSolution) etc.

To see how these are implemented this site has a good write-up.

Step 2 the last step is to publish the workflow:

2016-11-03-09_45_42-crmservices-release-visual-studio-team-services

That does it except for all the rest of the setup work you must do to allow it to push through all your environments.

I hope this helps someone

 

Until then keep scripting

thom

Quick hit – Server and Site Ips

Recently I needed to quickly get server IPS and Website addresses.  So I put together a couple line script to do this.  This post is about how that works:

Since I need to get these remotely I’m going to use a PowerShell Session . So I’ll first create my variable to hold the servers i need:

$servers = 'server1','server2','server3','server4'

Now that i have a variable with servers in it I can send this to new-pssesion and use the variable I setup above.

$session = new-pssession -computername $servers

My $sesion variable contains a session to each server that I want to Invoke-Command on.  So now I need to come up with a way to get the IPS for every site I have setup in IIS and all the ips for the server I’m calling. If I do this locally I can use this script

import-module webadministration ;(Get-Website) | Select-Object name`
 ,@{Name='bindings' expression= `
{($_.bindings.collection.bindingInformation -replace ':\d+','').trim(':') }}}

I’m taking the values from Get-website  and selecting the name bindings and alson the bindings.collection binding information and removing the Port from the binding number. Return results look like this:

name : MySite
bindings : 10.10.10.39

Now all I need is to find out how to get my ip address. I can do this by using the command Get-Netipaddress if I enclose the function I can get at the property:

PS PS:\iis> (get-netipaddress).IPAddress
::1
10.10.10.1
127.0.0.1

Now to put it together in a single script:

 $servers = 'server1','server2','server3','server4'
 $sess = new-pssession -ComputerName $servers -Credential $admCredentials
 invoke-command -Session $sess {import-module webadministration ;(Get-Website) | Select-Object name ,@{Name='bindings' 
 expression={($_.bindings.collection.bindingInformation -replace ':\d+','').trim(':') }}}
 invoke-command -session $sess { $env:computername;(get-netipaddress).IPAddress}

name : website1
bindings : 10.10.10.48
PSComputerName : server1
RunspaceId : 91f9523f-58df-49d5-a0b1-064101822aae

name : websiteapi
bindings : 10.10.10.49
PSComputerName : server1
RunspaceId : 91f9523f-58df-49d5-a0b1-064101822aae

name : website1
bindings : 10.10.10.50
PSComputerName : server2
RunspaceId : 3cd44533-d827-4a60-bdcc-6c91e77d96b9

name : websiteapi
bindings : 10.10.10.51
PSComputerName : server2
RunspaceId : a90c36ca-630e-42a8-abcd-069e8cec5360

server1
server2
::1
::1
::1
10.10.10.48
10.10.10.49
10.10.10.40
10.10.10.51

I hope this helps someone out. .

 

 

Until then keep Scripting

 

thom