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.

Running Local On Remote

on Friday, August 1, 2014

A lot of PowerShell functions/Cmdlets are written in a way that they can only be run on a localhost. But, sometimes you need to run them remotely.

PSSession will let you run a command on a remote host (One Hop). If you need to connect to more hosts than that, you’ll to need setup CredSSP in your environment.

One Hop Scripts

This function is a template for running a local command on a remote host:

Function Verb-Noun {
[CmdletBinding()]
[OutputType(If you can set this, that's awesome)]
Param (
    [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "A")]
    [PSObject] $A
    [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "A")]
    [string] $AZ,
    [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "B")]
    [string] $B,
    [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
    [string] $ServerName = $env:COMPUTERNAME,
    [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
    [System.Management.Automation.Runspaces.PSSession] $Session = $null
)

    $scriptBlock = {
        Import-Module WebAdministration
        Import-Module ABC

        if($args) {
   Merge-AllParams -Arguments $args[0];
  }

        ... code goes here ...

        return $XYZ
    }

    # handle calling with sessions
    $sessInfo = Test-CreateNewSession -Session $Session -ServerName $ServerName
    $Session = $sessInfo.Session

    try {
     if($session -and -not (Test-IsLocalSession $session)) {
            # copy all variables to pass across with Invoke-Command
         $allParams = Get-AllParams -Command $MyInvocation.MyCommand -Local (Get-Variable -Scope Local)

      # if a session is avaliable, run it in the session; unless its the local sysem
      $XYZ = Invoke-Command -Session $Session -ArgumentList $allParams -ScriptBlock $scriptblock;
     } else {
      # if it's a local session or if no session is avaliable, then run the script block inline
      $XYZ = (. $scriptblock);
     }
    } finally {
        if($sessInfo.CreatedSession) { Remove-PSSession $Session }
    }

    return $XYZ
}

The function relies on Get-AllParams, Merge-AllParams, and Test-CreateNewSession.

<#
.SYNOPSIS
 Will retrieve all arguments passed into a function. This can help ease passing those values
 to an Invoke-Command cmdlet.

.EXAMPLE
 Get-AllParams -Command $MyInvocation.MyCommand -Locals (Get-Variable -Scope Local);
#>
Function Get-AllParams {
[CmdletBinding()]
Param(
 [Parameter(Mandatory = $true)]
 [System.Management.Automation.FunctionInfo]$Command,
 [Parameter(Mandatory = $true)]
 [Array]$Locals
)

 $allParams = @{};
 $Command.Parameters.Keys| foreach {
   $i = $_;
   $allParams[$i] = ($Locals |? { $_.Name -eq $i; }).Value;
  }
 return $allParams;
}

<#
.SYNOPSIS
 Will load all parameters passed in into the Script scope. This can be used in conjuction with
 Get-AllParams to pass variables into an Invoke-Command block.

.EXAMPLE
 Merge-AllParams -Arguments $args[0];
#>
Function Merge-AllParams {
[CmdletBinding()]
Param (
 [Hashtable]$Arguments
)

 $Arguments.GetEnumerator() |% { Set-Variable -Name $_.key -Value  $_.value -Scope Global; }
}

<#
.SYNOPSIS
    Sets up a Session object if needed. It also returns a flag if a session object was created.

.DESCRIPTION
    Sets up a Session object if needed. It also returns a flag if a session object was created.

    When functions sometimes need to run remotely (through a Session) or sometime locally, the
    code can be written to use a script block and logic can be added to call the code with a Session.
    The logic can become redundant when determing if and how to call the Session. This helper
    function helps with the process.

.PARAMETER Session
    The current Session variable passed into the calling function

.PARAMETER ServerName
    The current ServerName variable available in the calling function

.EXAMPLE
    $sessInfo = Test-CreateNewSession -Session $Session -ServerName $ServerName
    $Session = $sessInfo.Session

    try {
        ... determine if the session needs to be called or a local execution should be used ...
    } finally {
        if($sessInfo.CreatedSession) { Remove-PSSession $sessInfo.Session }
    }
#>
Function Test-CreateNewSession {
[CmdletBinding()]
Param (
    [System.Management.Automation.Runspaces.PSSession] $Session = $null,   
    [string] $ServerName = ""
)

    $createdSession = $false
    if($Session -eq $null -and $ServerName -ne "") {
        if(-not (Test-IsLocalComputerName $ServerName)) {
            $Session = New-PSSession $ServerName
            $createdSession = $true
        }
    }

    $sessInfo = New-PsType "CoreUcsb.PSSessionCreate" @{
                    Session = $Session
                    CreatedSession = $createdSession
                }

    return $sessInfo
}


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