Function Help & Internal Repository

I have a small function that works as a script including the help I built into it.
Using help .\Script.ps1 works and the help shows just fine.

I wanted to make this a module and publish it to an internal repository. I renamed the .ps1 file to .psm1 and successfully published to our repository.

It installs using install-module just fine, but that is where my successes end… I can’t use the function or call up the help for it.

The module gets installed into a module directory in the path variable. The Get-Module -ListAvailable see’s it is there, I just can’t use it. Also Import-Module doesn’t seem to do anything, no errors are reported.

Here is the code, which works as a .ps1 file. Again, once published into our internal repository and installed locally I can’t seem to access it. I’m not sure what I’m doing wrong…

function Get-ADMissingServers {
    [CmdletBinding()]
    param (
        [Parameter(
            Position = 0,
            Mandatory = $false
        )]
        [int]
        $LastSeen = -180
    )
    <#
    .SYNOPSIS
        Retrieves Windows servers from Active Directory that have not been seen for a period of time.
    .DESCRIPTION
        Use this to locate servers in Active Directory that appear to be missing. Missing servers are determined using the last logon date stored in AD. A default date
        of 180 days is used in the abscense of a specified date.
    .NOTES
        This function is not supported in Linux and only retrieves Windows based servers from Active Directory.
        This function only uses an negative integer number to specify the "LastSeen" days; ie:"-25","-90","-365" etc.
    .EXAMPLE
        Get-ADMissingServers.ps1
        Retrieves all Windows servers that are enabled in Active Directory but have not been seen by AD since the default past date (-180 days from the current date.)
    .EXAMPLE
        Get-ADMissingServers.ps1 -LastSeen -365
        Retrieves all Windows servers that are enabled in Active Directory but have not been seen by AD in the last year.
    .EXAMPLE
        Get-ADMissingServers.ps1 -LastSeen -90 -Verbose
        Retrieves all Windows servers that are enabled in Active Directory but have not been seen by AD in the 90 days with verbose output.
    #>
    
    
 
    begin {
        
    }
    
    process {
        $ServerResults = @()
        $MissingServers = @()

        Write-Verbose "Retrieving all Servers from Active Directory."
        $Servers = Get-ADComputer -Filter 'OperatingSystem -Like "*Server*"' -Properties DNSHostName, Enabled, LastLogonTimeStamp, operatingsystem, DistinguishedName
        Write-Verbose "Processing each AD Server object."
        $Servers | ForEach-Object -Process {
            $ServerData = [PSCustomObject]@{
                DNSHostName     = 'UNKNOWN' 
                Enabled         = 'UNKNOWN' 
                LastLogonDate   = 'UNKNOWN'  
                OperatingSystem = 'UNKNOWN' 
                OU              = 'UNKNOWN'  
            }

            $ServerData.DNSHostName = $_.DNSHostName
            $ServerData.Enabled = $_.enabled
            $ServerData.LastLogonDate = $([datetime]::FromFileTime($_.LastLogonTimeStamp))
            $ServerData.OperatingSystem = $_.OperatingSystem
            $ServerData.OU = $_.DistinguishedName
            $ServerResults += $ServerData
            Write-Verbose "Processed $_.DNSHostName."
        }
        Write-Verbose "Comparing each AD Server object's last login time stamp with today $LastSeen."
        $ServerResults | ForEach-Object -Process {
            if ($_.LastLogonDate -le (Get-Date).AddDays($LastSeen)) {
                #Do something
                if ($_.OU.EndsWith("OU=Disabled_Servers,OU=ComputerAccounts,DC=canfor,DC=ca")) {
                    #do nothing
                    Write-Verbose "$_.DNSHostName does not fall within the criteria."
                }
                else {
                    $MissingServers += $_
                    Write-Verbose "$_.DNSHostName hasn't been seen since $_.LastLogonDate."
                }
            }
        }
        Write-Verbose "Outputing the final results."
        Return Write-Output $MissingServers
    }
    end {
        
    }
}

Try using import-module with -Verbose and -Force to see what exactly it’s doing.

How did you publish it? And how are you importing it? Historically, if I wanted to make a script a module, I simply rename to psm1 like you did, and then put it in a folder with the same name somewhere in the psmodulepath. I just tested this with your function, and it worked fine.

import-module admissingservers -verbose -force
VERBOSE: Loading module from path 'C:\Program Files\WindowsPowerShell\Modules\admissingservers\0.1.5\admissingservers.psd1'.
VERBOSE: Populating RepositorySourceLocation property for module admissingservers.

I published to my internal repository like this

Publish-Module -Name .\ADMissingServers\ -NuGetApiKey "RandomText" -Repository CfpRepository

Then I installed the module like this

Install-Module -name ADMissingServers -Repository CfpRepository -Scope AllUsers

Yeah it’s looking for a .psd1. I’m not sure how to publish with just a psm1. Have you tried

Publish-Module -Name .\ADMissingServers\ADMissingServers.psm1 -NuGetApiKey "RandomText" -Repository CfpRepository

See if you can publish while targeting the psm1 instead. Outside of that, I’d recommend just going ahead and creating a manifest.

New-ModuleManifest -Path .\ADMissingServers\ADMissingServers.psd1 -Author "Author Name" -CompanyName "Company Name" -Description "Description if you like" -Copyright "2024"

And then publish it again. That should do it.

Oh you may need to set the FunctionsToExport to * or explicitly list Get-ADMissingServers (if you go the manifest route)

It’s better to be explicit. One of the reasons is performance (though smaller modules it won’t matter), but the better one, IMO, is to get tab completion to work.


Once that gets annoying most people build it as part of a task in a CI/CD pipeline, but… that’s a whole other thing.

I tried

Publish-Module -Name .\ADMissingServers\ADMissingServers.psm1 -NuGetApiKey "RandomText" -Repository CfpRepository
uninstall-module admissingservers
install-module ADMissingServers -Repository CfpRepository
help get-admissingservers

That didn’t make any difference.
Just to be certain I made sure to uninstall-module the ADMissingServers module(s) from the system before installing it again. I also deleted any instance(s) of it from the repository.

I am already using a manifest. I used the New-ModuleManifest cmdlet to create it. It took a couple of tries to get the export cmd formatted correctly before the publish would work, but this works without errors.

# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = 'Get-ADMissingServers'

Alright, I copied your function to a file, made it a .psm1, made a new module manifest for it and made my “FunctionsToExport” line look like yours. I skipped publishing/installing it and just made a directory for it at "C:\Program Files\Windowspowershell\modules\ADMissingServers\1.0"
Then I imported it, and got the same results as you. I tried moving the comment based help before the param() block, still no luck. I tried altering the psd1 “FunctionsToExport” to explicitly be an array

# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = @('Get-ADMissingServers')  

And it stilll didn’t work. Running:

Get-Command -Module ADMissingServers  

Still returns nothing.

So the good news is, you’re not crazy. I’m going to keep tinkering.

IIRC Publishing a module requires a manifest. When you created your manifest (psd1) did you fill out the rootmodule in the file manually or via New-ModuleFaniest?

Sounds like the RootModule might be default (commented out). so I’d suggest uncommenting it and setting it equal to your PSM1.

#this is excerpt from the PSD1 file
# Script module or binary module file associated with this manifest.
RootModule = 'ADMissingServers.psm1'

That way when you import the module using the PSD1, it knows to dot source your ‘root’ psm1’ file which should load your function into memory. That, along with your FunctionsToexport, should export that function.

IMO when using a manifest file, FunctionsToExport should be used, not Export-ModuleMember. in a PSM1, if you don’t have Export-ModuleMember, and you load a module straight from PSM1, i think it just works.

i think @dotnVo wins. I noticed that RootModule wasn’t filled out in my new .psd1 I made but my other modules do have that. I added it (like he showed above) and when I imported it I was greeted with this verbose text:

VERBOSE: Loading module from path 'C:\Program Files\WindowsPowerShell\Modules\ADMissingServers\1.0\ADMissingServers.psd1'.
VERBOSE: Loading module from path 'C:\Program Files\WindowsPowerShell\Modules\ADMissingServers\1.0\ADMissingServers.psm1'.
VERBOSE: Exporting function 'Get-ADMissingServers'.
VERBOSE: Importing function 'Get-ADMissingServers'.  

Thank you everone. The missing RootModule directive was indeed the issue.