WebAdministration Not Loaded Correctly on Remote

on Friday, November 14, 2014

When making remote calls that use the WebAdministration module you can sometimes get this error, inconsistently:

ERROR: Get-WebSite : Could not load file or assembly ‘Microsoft.IIS.PowerShell.Framework' or one of its dependencies. The system cannot find the file specified.

It’s a really tricky error because its inconsistent. But, there is a workaround that will prevent the error from giving you too much trouble. From the community that has done troubleshooting on this, the problem seems to occur on the first call that uses the WebAdministration module. If you can wrap that call in a try/catch, then subsequent calls will work correctly.

$scriptBlock = {
    Import-Module WebAdministration

    try {
        $sites = Get-WebSite
    } catch {
        # http://help.octopusdeploy.com/discussions/problems/5172-error-using-get-website-in-predeploy-because-of-filenotfoundexception
        $sites = Get-WebSite
    }
}

Invoke-Command -ScriptBlock $scriptBlock -ComputerName Remote01

PowerShell AppPool Assignment Problems

on Friday, November 7, 2014

The WebAdministration module has a Function called IIS:. It essentially acts like a drive letter or an uri protocol. Its really convenient and makes accessing appPool, site information, and ssl bindings easy.

I recently noticed two problems with assigning values through the IIS: protocol or the objects which is works with:

StartMode Can’t Be Set Directly

For some reason, using Set-ItemProperty to set the startMode value directly throws an error. But, if you retrieve the appPool into a variable and set the value using an = operator, everything works fine.

# https://connect.microsoft.com/PowerShell/feedbackdetail/view/1023778/webadministration-apppool-startmode-cant-be-set-directly
ipmo webadministration

New-WebAppPool "delete.me"

Set-ItemProperty IIS:\AppPools\delete.me startMode "AlwaysRunning" # throws an error

$a = Get-Item IIS:\AppPools\delete.me
$a.startMode = "AlwaysRunning"
Set-Item IIS:\AppPools\delete.me $a # works

Here is the error that gets thrown:

Set-ItemProperty : AlwaysRunning is not a valid value for Int32.
At C:\Issue-PowershellThrowsErrorOnAppPoolStartMode.ps1:6 char:1
+ Set-ItemProperty IIS:\AppPools\delete.me startMode "AlwaysRunning" # throws an e ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Set-ItemProperty], Exception
    + FullyQualifiedErrorId : System.Exception,Microsoft.PowerShell.Commands.SetItemPropertyCommand

 

CPU’s resetLimit Can’t Directly Use New-TimeSpan’s Result

I think the example can show the problem better than I can describe it:

# https://connect.microsoft.com/PowerShell/feedbackdetail/view/1023785/webadministration-apppools-cpu-limit-interval-resetlimit-cant-be-set-directly
ipmo webadministration

New-WebAppPool "delete.me"

$a = Get-ItemProperty IIS:\AppPools\delete.me cpu
$a.resetInterval = New-TimeSpan -Minutes 4 # this will throw an error
Set-ItemProperty IIS:\AppPools\delete.me cpu $a

$a = Get-ItemProperty IIS:\AppPools\delete.me cpu
$k = New-TimeSpan -Minutes 4 # this will work
$a.resetInterval = $k
Set-ItemProperty IIS:\AppPools\delete.me cpu $a

Here is the error that gets thrown:

Set-ItemProperty : Specified cast is not valid.
At C:\Issue-PowershellThrowsErrorOnCpuLimitReset.ps1:8 char:1
+ Set-ItemProperty IIS:\AppPools\delete.me cpu $a
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Set-ItemProperty], InvalidCastException
    + FullyQualifiedErrorId : System.InvalidCastException,Microsoft.PowerShell.Commands.SetItemPropertyCommand

The links on each section correspond with bug reports for the issues, so hopefully they will get looked into.

PowerShell Wrapper for Http Namespaces

on Friday, October 31, 2014

When hosting HTTP WCF services as a self-hosted Windows Services the server needs to have the HTTP Namespace reserved. The reservation allows for the domain account which runs the service to setup a listener on a particular port, for a particular address.

There are some tools already available which can help in this process:

  • HTTP Namespace Manager – A nice GUI interface, which is easy to understand and setup. It also works on Server Core Servers.
  • httpcfg – Windows Server 2003
  • netsh – Windows Server 2008+

But, there are no PowerShell wrappers for these commands. So, here’s a wrapper that provides:

  • Add-HttpNamespace
  • Get-HttpNamespace
  • Get-HttpNamespaces
  • Test-HttpNamespaceExists

There’s no remove because I haven’t needed it yet. A namespace is usually associated with a particular port, and I haven’t been involved in a situation where a port needed to be reused.

<#
.SYNOPSIS
    Parses the output from netsh to turn them in PSObjects.
#>
Function Get-HttpNamespaces {
[CmdletBinding()]
[OutputType([PSObject[]])]
Param()

    # the $propsReady variable causes alot of errors to occur, but the results are accurate.
    # so this helps hide the errors
    $originalErrorAction = $ErrorActionPreference
    $ErrorActionPreference = 'SilentlyContinue'

    try {

        # pull the data from netsh
        $urlaclOutput = . netsh http show urlacl

        # parse the data into PSObjects
        $httpNamespaces = New-Object System.Collections.Generic.List[PSObject]
        $props = @{}
        $userProps = @{}
        $userRdy = $false
        for($i = 0; $i -lt $urlaclOutput.Count; $i++) {
            $line = $urlaclOutput[$i].Trim()

            $split = $line.Split(":", [StringSplitOptions]::RemoveEmptyEntries)

            $first = ""
            if($split.Count -gt 0) { $first = $split[0] }
        
            # line parsing
            switch($first.Trim()) {
                "Reserved URL" {
                    $props.ReservedUrl = $line.Substring(25).Trim()
                    $users = New-Object System.Collections.Generic.List[PSObject]
                }
                "User" {
                    if($userRdy) {
                        $user = New-Object PSObject -Property $userProps
                        $users.Add($user)

                        $userProps = @{}
                        $userRdy = $false
                    }

                    $userProps.User = $split[1].Trim()
                }
                "Listen" { $userProps.Listen = $split[1].Trim() }
                "Delegate" {
                    $userProps.Delegate = $split[1].Trim()
                    $userRdy = $true
                }
                "SDDL" {
                    $userProps.SDDL = $line.Substring(5).Trim()
                    $userRdy = $true
                }
                "" {
                    if($userRdy) {
                        # user
                        $user = New-Object PSObject -Property $userProps
                        $users.Add($user)

                        $userProps = @{}

                        # url
                        $props.Users = $users.ToArray()

                        $cnObj = New-Object PSObject -Property $props
                        $httpNamespaces.Add($cnObj)

                        $props = @{}

                        # reset flag
                        $userRdy = $false
                    }
                }
            }
        }
    } finally {
        $ErrorActionPreference = $originalErrorAction # revert the error action
    }

    return $httpNamespaces.ToArray()
}


<#
.SYNOPSIS
    Retrieves the namespace information for a given namespace. It will also search for namespaces
    which match but the host names have been replaced with + or * symbols.
#>
Function Get-HttpNamespace {
[CmdletBinding()]
[OutputType([PSObject])]
Param (
    [Parameter(Mandatory = $true)]
    [string] $HttpNamespace
)

    $httpNamespaces = Get-HttpNamespaces

    # get * and + versions of the url ready
    $starNamespace = $HttpNamespace
    $plusNamespace = $HttpNamespace
    $namespaceRegex = [regex] "http.*://(.*):.*/.*"
    if($HttpNamespace -match $namespaceRegex) {
        $hostname = $Matches[1]
        $starNamespace = $HttpNamespace.Replace($hostname, "*")
        $plusNamespace = $HttpNamespace.Replace($hostname, "+")
    }

    # sometimes the http namespaces get /'s added to the end
    $namespace = $httpNamespaces |? {
                            $_.ReservedUrl -eq $HttpNamespace `
                    -or     $_.ReservedUrl -eq ($HttpNamespace + '/') `
                    -or     $_.ReservedUrl -eq $starNamespace `
                    -or     $_.ReservedUrl -eq ($starNamespace + '/') `
                    -or     $_.ReservedUrl -eq $plusNamespace `
                    -or     $_.ReservedUrl -eq ($plusNamespace + '/')
                }

    return $namespace
}



<#
.SYNOPSIS
    Checks if a namespace already exists. It will also search if the namespace has had its host name
    replaced with + or * symbols.
#>
Function Test-HttpNamespaceExists {
[CmdletBinding()]
[OutputType([bool])]
Param (
    [Parameter(Mandatory = $true)]
    [string] $HttpNamespace
)

    $namespace = Get-HttpNamespace $HttpNamespace

    return $namespace -ne $null
}



<#
.SYNOPSIS
    Adds a new Http Namespace. This will automatically swap out the host name for a + symbol. The
    + symbol allows the Http Namespace to bind on all NIC addresses.
#>
Function Add-HttpNamespace {
[CmdletBinding()]
[OutputType([PSObject])]
Param (
    [Parameter(Mandatory = $true)]
    [string] $HttpNamespace,
    [Parameter(Mandatory = $true)]
    [string] $DomainAccount
)

    $create = $true
    if(Test-HttpNamespaceExists $HttpNamespace) {
        # it already exists, so maybe not create it
        $create = $false

        $namespace = Get-HttpNamespace $HttpNamespace
        # but, if the given DomainAccount doesn't exist then create it
        $user = $namespace.users |? { $_.user -eq $DomainAccount }
        if($user) {
            Write-Warning "NET $env:COMPUTERNAME - Http Namespace '$HttpNamespace' already contains a rule for '$DomainAccount'. Skipping creation."
            return
        } else {
            $create = $true
        }
    }

    if($create) {
        # the standard pattern to use is http://+:port/servicename.
        #   eg. http://contoso01:15110/EmployeeService would become http://+:15110/EmployeeService
        $plusNamespace = $HttpNamespace
        $namespaceRegex = [regex] "http.*://(.*):.*/.*"
        if($HttpNamespace -match $namespaceRegex) {
            $hostname = $Matches[1]
            $plusNamespace = $HttpNamespace.Replace($hostname, "+")
        } else {
            throw "NET $env:COMPUTERNAME - Http Namespace '$HttpNamespace' could not be parsed into plus format before being added. Plus format " + `
                "looks like http://+:port/servicename. For example, http://contoso01:15110/EmployeeService would be formatted into " + `
                "http://+:15110/EmployeeService."
        }

        # ensure the full domain account name is used
        $fullDomainAccount = Get-FullDomainAccount $DomainAccount

        # create the permission
        Write-Warning "NET $env:COMPUTERNAME - Adding Http Namespace '$Httpnamespace' for account '$fullDomainAccount'"
        $results = . netsh http add urlacl url=$plusNamespace user=$fullDomainAccount listen=yes delegate=yes
        Write-Host "NET $env:COMPUTERNAME - Added Http Namespace '$Httpnamespace' for account '$fullDomainAccount'"
    }

    $namespace = Get-HttpNamespace $HttpNamespace
    return $namespace
}

Quick Redis with PowerShell

on Friday, October 24, 2014

ASP.NET has some great documentation on How To Setup a SignalR Backplane using Redis, but it uses a linux based server as the host. The open source port of Redis maintained by MSOpenStack creates an incredibly easy to install Redis server for a Windows Server environment (using Chocolatey ... KickStarter). This is a quick PowerShell script to install redis as a Windows Service, add the firewall rule, and start the service.

# PreRequisites
#    The assumption is that the server can use chocolatey to install

# use chocolatey to install precompiled redis application
#    this will install under the binaries under $env:ChocolateyInstall\lib\redis-X.X.X\
cinst redis-64

# install redis as a service
#    redis will make the service run under Network Service credentials and setup all appropriate permissions on disk
redis-server --service-install

# open firewall ports
. netsh advfirewall firewall add rule name=SignalR-Redis dir=in protocol=tcp action=allow localport=6379 profile=DOMAIN

# start the service
redis-server --service-start

Note: When installing the service there is an error message "# SetNamedSecurityInfo Error 5". But, it doesn't seem to affect anything; everything seems to run without a problem.

PowerShell VirtualDirectory Wrappers

on Friday, October 17, 2014

The WebAdministration Module ships with a couple functions for working with virtual directories:

But, there are some glitches with them.

A way to get around these problems is to write a wrapper class around the functions. Making the PhysicalPath property on New-WebVirtualDirectory is pretty easy to do, but the other one …

To prevent the confirmation prompt from popping up, the wrapper function can create an empty temporary directory, repoint the virtual directory to it, remove the virtual directory, and then remove the temporary directory.

This code below also wrapped the Get-WebVirtualDirectory because I wanted an API that took a Url as input and figure out how to use it.

Here’s a full list of the wrappers and helper functions:

<#
.SYNOPSIS
 Takes a url and breaks it into these parts: Ssl, SiteName, AppName, AppNames.
 
.DESCRIPTION 
 Takes a url and breaks it into these parts for a [PSObject]:

 Ssl   - true/false - does the url request ssl
 SiteName - string - the dns host name
 AppName  - string - the AppNames as a single string. It starts with '/'.
 AppNames - Array<string> - each folder name within the local path

.PARAMETER Url
 The url to convert into it's UriPaths

.EXAMPLE
 $uriPaths = ConvertTo-WebUriPaths "https://www.contoso.com/services"
#>
Function ConvertTo-WebUriPaths {
Param (
 [Parameter(Mandatory = $true)]
 [string] $Url
)

 $paths = @{
  Ssl = $null;
  SiteName = "";
  AppNames = @();
  AppName = "";
 };

 $uri = New-Object System.Uri $Url;

 $paths = New-PsType "UriPaths"

 Add-PsTypeField $paths "Ssl" ($uri.Scheme -eq "https")
 Add-PsTypeField $paths "SiteName" $uri.Host
 Add-PsTypeField $paths "AppNames" $uri.LocalPath.Split("/", [StringSplitOptions]::RemoveEmptyEntries)
 Add-PsTypeField $paths "AppName" $uri.LocalPath

 # remove trailing slash, if exists
 if($paths.AppName) {
  $appNameLen = $paths.AppName.Length;
  if($paths.AppName[$appNameLen - 1] -eq "/") {
   $paths.AppName = $paths.AppName.Substring(0, $appNameLen - 1);
  }
 }

 return $paths;
}



<#
.SYNOPSIS
 Takes a UriPaths object (from ConvertTo-UriPaths) and turns it into
 and IIS:\Sites\XXXX string value.

.PARAMETER UriPaths
 The UriPaths object from ConvertTo-WebUriPaths

.EXAMPLE
    $uriPaths = ConvertTo-WebUriPaths "https://www.contoso.com/services"
 $iisPath = ConvertTo-WebIISPath $uriPaths

.LINK
 ConvertTo-WebUriPaths
#>
Function ConvertTo-WebIISPath {
Param (
 [Parameter(Mandatory = $true)]
 [PSObject] $UriPaths
)

 $iisPath = "IIS:\Sites\" + $UriPaths.SiteName;
 $UriPaths.AppNames |% { $iisPath += "\" + $_ }; # alternateively, AppName could also be used

 return $iisPath;
}



<#
.SYNOPSIS
    Using the given url to search the current server for the longest parent website/webapp path that matches the url.
    It will only return the parent website/app information. if the root website is given, then $null will be returned.
    The webapp's information from the IIS:\Sites protocol is returned.

.PARAMETER Url
    The url to search on
    
.EXAMPLE
    $webApp = Get-WebParentAppByUrl "http://www.contoso.com/services" 
#>
Function Get-WebParentAppByUrl {
Param (
    [Parameter(Mandatory = $true)]
    [string] $Url
)

    $uriPaths = ConvertTo-WebUriPaths -Url $Url

    $currentPath = "IIS:\Sites\{0}" -f $uriPaths.SiteName
    if((Test-Path $currentPath) -eq $false) { return $null }
    if($uriPaths.AppName -eq "" -or $uriPaths.AppName -eq "/") { return $null}

    $webApp = Get-Item $currentPath
    if($uriPaths.AppNames -is [Array]) {
        for($i = 0; $i -lt $uriPaths.AppNames.Count - 1; $i++) {
            $currentPath += "\{0}" -f $uriPaths.AppNames[$i]
            if(Test-Path $currentPath) { $webApp = Get-Item $currentPath }
        }
    }

    return $webApp
}



<#
.SYNOPSIS
 Get virtual directory information from a site/app. If the given Url is a site/app, this will
    search for all virtual directories at the same level as the given the path. If not a site/app,
    this will search for a virtual directory under the parent site/app.

.PARAMETER Url
 The url to search at.

.EXAMPLE
 $vdirs = Get-WebVirtualDirectoryWrapper -Url "http://www.contoso.com"
#>
Function Get-WebVirtualDirectoryWrapper {
[OutputType([Microsoft.IIs.PowerShell.Framework.ConfigurationElement])]
Param (
    [Parameter(Mandatory = $true)]
    [string] $Url
)

    $uriPaths = ConvertTo-WebUriPaths -Url $Url

    # check if the url is a site/app
    $iisPath = ConvertTo-WebIISPath -UriPaths $uriPaths

    if(-not (Test-Path $iisPath)) {
        Write-Warning "IIS $env:COMPUTERNAME - No path could be found for '$Url'. No virtual directories could be looked up."
        return $null
    }

    $node = Get-Item $iisPath
    if(@("Application", "Site") -contains $node.ElementTagName) {
        # search for virtual directories below this level

        $vdirs = Get-WebVirtualDirectory -Site $uriPaths.SiteName -Application $uriPaths.AppName

    } else {
        # search the parent app for the given virtual directory name

        $parentApp = Get-WebParentAppByUrl $Url
        $vdir = $uriPaths.AppName.Substring($parentApp.path.Length)
        $appPath = $parentApp.path
        if(-not $appPath) { $appPath = "/" }

        $vdirs = Get-WebVirtualDirectory -Site $uripaths.SiteName -Application $appPath -Name $vdir
    }
        
    return $vdirs
}


<#
.SYNOPSIS
 Set a virtual directory for a site/app. This will set the physical path for the given Url.

.PARAMETER Url
 The url to turn into a virtual directory.

.PARAMETER PhysicalPath
    The physical path on the server to attach to the virtual path.

.PARAMETER Force
    Overwrites the current physical path if already set.

.EXAMPLE
    # Create a new virtual directory

 $vdir = New-WebVirtualDirectoryWrapper `
                    -Url "http://admissions.{env}.sa.ucsb.edu" `
                    -PhysicalPath "D:\AllContent\Data\admissions.{env}.sa.ucsb.edu"
                    -ServerName "SA89"  
#>
Function New-WebVirtualDirectoryWrapper {
[OutputType([Microsoft.IIs.PowerShell.Framework.ConfigurationElement])]
Param (
    [Parameter(Mandatory = $true)]
    [string] $Url,
    [Parameter(Mandatory = $true)]
    [string] $PhysicalPath,
    [switch] $Force
)

    $uriPaths = ConvertTo-WebUriPaths -Url $Url

    # parse the name of the virtual directory from the given url
    if($uriPaths.AppName -eq "" -or $uriPaths.AppName -eq "/") {
        throw "IIS $env:COMPUTERNAME - No virtual path could be found in url '$Url'. A subpath needs to be defined within the url."
    }

    $parentApp = Get-WebParentAppByUrl -Url $Url

    if($parentApp -eq $null) {
        throw "IIS $env:COMPUTERNAME - No parent application could be found for url '$Url'. No virtual directory could be added."
    }

    $appPath = $parentApp.path
    if(-not $appPath) { $appPath = "/" }

    $vdirPath = $uriPaths.AppName.Substring($appPath.Length)

    # if the vdirPath is multiple levels deep, check that the root path exists
    if($vdirPath.Split("/", [StringSplitOptions]::RemoveEmptyEntries).Count -gt 1) {
        $i = $vdirPath.LastIndexOf("/")
        $rootSubLevel = $vdirPath.Substring(0,$i).Replace("/","\")
        $iisPath = "IIS:\Sites\{0}\{1}" -f $uriPaths.SiteName, $rootSubLevel
        if((Test-Path $iisPath) -eq $false) {
            throw "IIS $env:COMPUTERNAME - Part of the sub path for '$Url' could not be found. Please ensure the full base path exists in IIS."
        }
    }

    Write-Warning "IIS $env:COMPUTERNAME - Creating a virtual directory for $Url to $PhysicalPath."
    if($Force) {
        if($appPath -eq "/") { # it adds an extra / if you set the applicationName to '/'
            $vdir = New-WebVirtualDirectory -Site $uriPaths.SiteName -Name $vdirPath -PhysicalPath $PhysicalPath -Force
        } else {
            $vdir = New-WebVirtualDirectory -Site $uriPaths.SiteName -Application $appPath -Name $vdirPath -PhysicalPath $PhysicalPath -Force
        }
    } else {
        if($appPath -eq "/") { # it adds an extra / if you set the applicationName to '/'
            $vdir = New-WebVirtualDirectory -Site $uriPaths.SiteName -Name $vdirPath -PhysicalPath $PhysicalPath
        } else {
            $vdir = New-WebVirtualDirectory -Site $uriPaths.SiteName -Application $appPath -Name $vdirPath -PhysicalPath $PhysicalPath
        }
    }
    Write-Host "IIS $env:COMPUTERNAME - Created a virtual directory for $Url to $PhysicalPath."

    return $vdir
}


<#
.SYNOPSIS
 Removes a virtual directory from a site/app. It will only remove the virtual directory if the Url
    given matches up with a virtual directory.

.PARAMETER Url
 The url to search at.

.EXAMPLE
    Remove-WebVirtualDirectoryWrapper -Url "http://www.contoso.com/services"
#>
Function Remove-WebVirtualDirectoryWrapper {
Param (
    [Parameter(Mandatory = $true)]
    [string] $Url
)

    $uriPaths = ConvertTo-WebUriPaths -Url $Url

    # parse the name of the virtual directory from the given url
    if($uriPaths.AppName -eq "" -or $uriPaths.AppName -eq "/") {
        throw "IIS $env:COMPUTERNAME - No virtual path could be found in url '$Url'. A subpath needs to be defined within the url."
    }

    $parentApp = Get-WebParentAppByUrl -Url $Url

    if($parentApp -eq $null) {
        throw "IIS $env:COMPUTERNAME - No parent application could be found for url '$Url'. No virtual directory could be added."
    }

    # ensure the path is a virtual directory
    $iisPath = ConvertTo-WebIISPath -UriPaths $uriPaths
    if(-not (Test-Path $iisPath)) {
        throw "IIS $env:COMPUTERNAME - No path for $Url could be found in IIS."
    }

    $node = Get-Item $iisPath
    if($node.ElementTagName -ne "VirtualDirectory") {
        switch($node.GetType().FullName) {
            "System.IO.FileInfo" { $type = "File" }
            "System.IO.DirectoryInfo" { $type = "Directory" }
            "Microsoft.IIs.PowerShell.Framework.ConfigurationElement" { $type = $node.ElementTagName }
        }
        throw "IIS $env:COMPUTERNAME - The url '$Url' doesn't match with a Virtual Directory. It is a $type."
    }

    $vdirPath = $uriPaths.AppName.Substring($parentApp.path.Length)

    # check if the virtual path has files or folders beneath it. An error will occur if there are.
    $iisVPath = ConvertTo-WebIISPath -UriPaths $uriPaths

    $childItems = Get-ChildItem $iisVPath
    if($childItems) {
            Write-Warning ("IIS $env:COMPUTERNAME - The virtual path at '$Url' has items beneth it. Due to a bug in " + `
            " WebAdministration\Remove-WebVirtualDirectory this would force a windows pop-up dialog to get approval." + `
            " To get around this, a temporary folder will be created and the current virtual directory will" + `
            " be repointed to the new (empty) location before removal. After removal of the virtual directory" + `
            " the temporary folder will also be removed. The domain account this process runs under will need" + `
            " permissions to the temporary folder location to create and remove it.")


        $guid = [Guid]::NewGuid()
        $PhysicalPath = (Get-WebVirtualDirectoryWrapper -Url $Url).PhysicalPath
        $tempPath = Join-Path $PhysicalPath $guid

        Write-Warning "IIS $env:COMPUTERNAME - Creating temp directory '$tempDir' in order to remove a virtual directory."
        $tempDir = New-Item $tempPath -ItemType Directory
        Write-Host "IIS $env:COMPUTERNAME - Created temp directory '$tempDir' in order to remove a virtual directory."
        $void = New-WebVirtualDirectoryWrapper -Url $Url -PhysicalPath $tempPath -Force
    }

    $appPath = $parentApp.path
    if(-not $appPath) { $appPath = "/" }

    Write-Warning "IIS $env:COMPUTERNAME - Removing a virtual directory '$vdirPath' for '$Url'."
    Remove-WebVirtualDirectory -Site $uriPaths.SiteName -Application $appPath -Name $vdirPath
    Write-Host "IIS $env:COMPUTERNAME - Removed a virtual directory '$vdirPath' for '$Url'."

    if($tempDir) {
        Write-Warning "IIS $env:COMPUTERNAME - Removing temp directory '$tempDir' in order to remove a virtual directory."
        $void = Remove-Item $tempDir
        Write-Host "IIS $env:COMPUTERNAME - Removed temp directory '$tempDir' in order to remove a virtual directory."
    }
}

Customized Internal NuGet Gallery

on Friday, August 15, 2014

NuGet’s great and there are plenty of resources to help get your team setup with private feeds (MyGet, Inedo's ProGet, JFrog's Artifactory, Sonatype's Nexus), but sometimes there are needs to host your own feed internally.

It’s not too hard to do, but there are a few hoops that you need to jump through in order to get it all setup:

  1. NuGet already provides a great guide for downloading the Gallery code and getting it running on your local machine.
  2. They also have a guide for altering the Gallery code (LocalGuide) to prepare it to run on a local IIS instance.
  3. But, there are a few details that you might want to change to customize the Gallery for your organization/needs:
    1. At the end of the LocalGuide it mentions “you can register a user and make it an Admin by adding a record to the UserRoles table”. Here’s the script:
      select * from [dbo].[Users] -- find your id
      insert into [dbo].[Roles] (name) values ('Admins')
      insert into [dbo].[UserRoles] (UserKey, RoleKey) values (<your id>, 1)
    2. Remove Alert.md – This feeds the yellow bar that appears at the top of the screen and states “This is a development environment. No data will be preserved.”
      1. It’s under FrontEnd/NuGetGallery/App_Data/Files/Content/Alert.md
      2. I think it’s a good idea to remember that file. It’s a really nice implementation to be able to set an alert without disrupting the service.
    3. Update Web.config – These will kinda be obvious
      1. Gallery.Environment should be empty
      2. Gallery.SiteRoot
      3. Gallery.SmtpUri
      4. Gallery.Brand
      5. Gallery.GalleryOwner
      6. Remove <rewrite> rules (from LocalGuide)
    4. Update the Title Icon/Name – This is defined by CSS
      1. FrontEnd/NuGetGallery/Content – Both Layout.css and Site.css (it’s just a good idea to keep them insync)
      2. If you have the time to make a new image, that would be best.
      3. If you don’t have time, then
        1. comment out
          1. background
          2. text-indent
        2. add
          1. font-weight: bold
          2. color: white
          3. font-size: 1.2 em
          4. text-decoration: none
        3. The Web.Config setting of Gallery.Brand text will be displayed
    5. Add Gallery URL
      1. FrontEnd/NuGetGallery/Views/Pages/Home.cshtml
      2. Add some text before @ViewBag.Content like: Visual Studio URL: http://nuget.xyz.com/api/v2/
    6. Have Lucene Search Index update on each package upload
      1. FrontEnd/NuGetGallery/Controllers/ApiController – PublishPacakge function – By default the line IndexingService.UpdatePackage(package) is supposed to update the search index. But, sometimes it doesn’t.
      2. Replace that line with: IndexingService.UpdateIndex(forceRefresh: true)

I’m sure the first thing you’ll want to do once you have the website up and running is play around with some test packages. Here is a script to help cleanup the database once you’re done testing. (Also, delete any .nupkg files under <website>/App_Data/Files/packages/)

declare @trunc bit = 0
if(@trunc = 1) begin
 truncate table [dbo].[GallerySettings]
 truncate table [dbo].[PackageAuthors]
 truncate table [dbo].[PackageDependencies]
 truncate table [dbo].[PackageRegistrationOwners]
 delete from [dbo].[PackageStatistics] where [key] = 1
 delete from dbo.Packages where [key] = 1
 delete from [dbo].[PackageRegistrations] where [key] = 2
 /*delete from [dbo].[UserRoles] where [Userkey] = 1
 delete from [dbo].[Users] where [key] = 1*/
end

/****** Script for SelectTopNRows command from SSMS  ******/
select * from [dbo].[GallerySettings]
select * from [dbo].[PackageAuthors]
select * from [dbo].[PackageDependencies]
select * from [dbo].[PackageRegistrationOwners]
select * from [dbo].[PackageRegistrations]
select * from dbo.Packages
select * from [dbo].[PackageStatistics]
/*select * from [dbo].[Roles]
select * from [dbo].[UserRoles]
select * from [dbo].[Users]*/

Remote profile.ps1

on Friday, August 8, 2014

There have been a lot of articles on how profile.ps1 is used.

They seem to be incorrect; or at least the system has changed under their feet. You can check out how much your system conforms to the documentation standard by creating 6 (six!) different profile.ps1 files. Each one, with a statement of “Write-Host ‘ran xyz profile.ps1’”.

The fun part is that none of them will run when connecting from a remote a session. To do that you Have To Use a Session Profile. Which is kind of weird. But, it kinda fits in with the whole DSC thing. You configure a server once; when its created, and you never touch it again.

I’m not sure I agree with that approach.


Creative Commons License
This site uses Alex Gorbatchev's SyntaxHighlighter, and hosted by herdingcode.com's Jon Galloway.