Autotask.psm1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
<#
    .COPYRIGHT
    Copyright (c) ECIT Solutions AS. All rights reserved. Licensed under the MIT license.
    See https://github.com/ecitsolutions/Autotask/blob/master/LICENSE.md for license information.
#>


[CmdletBinding(
    PositionalBinding = $false
)]
Param(
    [Parameter(
        Position = 0
    )]
    [pscustomobject]
    $Credential,
    
    [Parameter(
        Position = 1
    )]
    [string]
    $ApiTrackingIdentifier, 

    [Parameter(
        Position = 2,
        ValueFromRemainingArguments = $true
    )]
    [string[]]
    $entityName
)

Write-Debug ('{0}: Start of module import' -F $MyInvocation.MyCommand.Name)

# Explicit loading of namespace
#$namespace = 'Autotask'
#. ([scriptblock]::Create("using namespace $namespace"))

# Special consideration for -Verbose, as there is no $PSCmdLet context to check if Import-Module was called using -Verbose
# and $VerbosePreference is not inherited from Import-Module for some reason.

# Remove comments
$parentCommand = ($MyInvocation.Line -split '#')[0]

# Store Previous preference
$oldVerbosePreference = $VerbosePreference
if ($parentCommand -like '*-Verbose*') {
    Write-Debug ('{0}: Verbose preference detected. Verbose messages ON.' -F $MyInvocation.MyCommand.Name)
    $VerbosePreference = 'Continue'
}
$oldDebugPreference = $DebugPreference
if ($parentCommand -like '*-Debug*') {
    Write-Debug ('{0}: Debug preference detected. Debug messages ON.' -F $MyInvocation.MyCommand.Name)
    $DebugPreference = 'Continue'
}

# Read our own manifest to access configuration data
$manifestFileName = $MyInvocation.MyCommand.Name -replace 'pdm1$', 'psd1'
$manifestDirectory = Split-Path $MyInvocation.MyCommand.Path -Parent

Write-Debug ('{0}: Loading Manifest file {1} from {2}' -F $MyInvocation.MyCommand.Name, $manifestFileName, $manifestDirectory)

Import-LocalizedData -BindingVariable My -FileName $manifestFileName -BaseDirectory $manifestDirectory

# Add module path to manifest variable
$My['ModuleBase'] = $manifestDirectory

# Get all function files as file objects
# Private functions can only be called internally in other functions in the module

$privateFunction = @( Get-ChildItem -Path $PSScriptRoot\Private\*.ps1 -ErrorAction SilentlyContinue ) 
Write-Debug ('{0}: Found {1} script files in {2}\Private' -F $MyInvocation.MyCommand.Name, $privateFunction.Count, $PSScriptRoot)

# Public functions will be exported with Prefix prepended to the Noun of the function name

$publicFunction = @( Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -ErrorAction SilentlyContinue ) 
Write-Debug ('{0}: Found {1} script files in {2}\Public' -F $MyInvocation.MyCommand.Name, $publicFunction.Count, $PSScriptRoot)

# Static functions will be exported with Prefix prepended to the Noun of the function name

$staticFunction = @( Get-ChildItem -Path $PSScriptRoot\Static\*.ps1 -ErrorAction SilentlyContinue ) 
Write-Debug ('{0}: Found {1} script files in {2}\Static' -F $MyInvocation.MyCommand.Name, $staticFunction.Count, $PSScriptRoot)

# Static functions will be exported with Prefix prepended to the Noun of the function name

$dynamicFunction = @( Get-ChildItem -Path $PSScriptRoot\Dynamic\*.ps1 -ErrorAction SilentlyContinue ) 
Write-Debug ('{0}: Found {1} script files in {2}\Dynamic' -F $MyInvocation.MyCommand.Name, $dynamicFunction.Count, $PSScriptRoot)


Write-Verbose ('{0}: Importing {1} Private and {2} Public functions.' -F $MyInvocation.MyCommand.Name, $privateFunction.Count, $publicFunction.Count)

# Loop through all supporting script files and source them
foreach ($import in @($privateFunction + $publicFunction)) {
    Write-Debug ('{0}: Importing {1}' -F $MyInvocation.MyCommand.Name, $import)
    try {
        . $import.fullname
    }
    catch {
        throw "Could not import function $($import.fullname): $_"
    }
}

# If they tried to pass any variables
if ($Credential) {
    Write-Verbose ('{0}: Parameters detected. Connecting to Autotask API' -F $MyInvocation.MyCommand.Name)

    # Notify Get-AtwsFieldInfo that we are currently loading the module
    $Script:LoadingModule = $true

    Try { 
                if ($Credential -is [pscredential]) {
            ## Legacy
            # The user passed credentials directly
            $Parameters = @{
                Credential               = $Credential
                SecureTrackingIdentifier = ConvertTo-SecureString $ApiTrackingIdentifier -AsPlainText -Force
                DebugPref                = $DebugPreference
                VerbosePref              = $VerbosePreference
            }
            $Configuration = New-AtwsModuleConfiguration @Parameters
        }
        elseif (Test-AtwsModuleConfiguration -Configuration $Credential) {
            ## First parameter was a valid configuration object
            $Configuration = $Credential

            # Switch to configured debug and verbose preferences
            $VerbosePreference = $Configuration.VerbosePref
            $DebugPreference = $Configuration.DebugPref
        }
        else {
            throw (New-Object System.Management.Automation.ParameterBindingException)
        }

        ## Connect to the API
        # or die trying
        . Connect-AtwsWebServices -Configuration $Configuration -Erroraction Stop
    }
    catch {
        $message = "{0}`n`nStacktrace:`n{1}" -f $_, $_.ScriptStackTrace
        throw (New-Object System.Configuration.Provider.ProviderException $message)
    
        return
    }
    
    # From now on we should have module variable atws available
    if ($Script:Atws.Configuration.UseDiskCache) {
        
        $dynamicCache = $Script:Atws.DynamicCache

        # Locate and load the connection specific script files
        if (Test-Path $dynamicCache\*atws*.ps1) {
            # We have this many dynamic functions distributed with the module
            $FunctionCount = $dynamicFunction.Count
            
            # We have this many dynamic functions in the disc cache
            $dynamicFunction = @( Get-ChildItem -Path $dynamicCache\*atws*.ps1 -ErrorAction SilentlyContinue )
            
            Write-Debug ('{0}: Personal disk cache: Found {1} script files in {2}' -F $MyInvocation.MyCommand.Name, $dynamicFunction.Count, $dynamicCache)
            
            # This is the version string that should be inside every valid function
            $Versionstring = "#Version {0}" -F $My.ModuleVersion
            
            # This is the number of dynamic functions that have the correct version tag
            $ScriptVersion = Select-String -Pattern $Versionstring -Path $dynamicFunction.FullName -ErrorAction SilentlyContinue
            
            # All function files MUST have the correct version and be of the correct version, or they will
            # recreated just to be safe. Allow more cached functions than distributed with module - this happens
            # when the API version is updated before the module can be updated.
            if ($ScriptVersion.Count -lt $FunctionCount -or $Script:Atws.Configuration.RefreshCache) {
                if (-not($Script:Atws.Configuration.RefreshCache)) { 
                    Write-Warning ('{0}: Personal disk cache: Wrong number of script files or scripts are not the right version in {1}, refreshing all entities.' -F $MyInvocation.MyCommand.Name, $dynamicCache)
  
                    # Clear out old cache, it will be recreated
                    $null = Remove-Item -Path $dynamicFunction.fullname -Force -ErrorAction SilentlyContinue
                }
                # Refresh ALL dynamic entities.
                $entityName = '*' 
            }

            $OldFunctions = @(Get-ChildItem -Path $dynamicCache\*.ps1 -Exclude *Atws* -ErrorAction SilentlyContinue)
            if ($OldFunctions.Count -gt 0) {

                Write-Warning ('{0}: Personal disk cache: Found {1} old script files in {2}. Deleting.' -F $MyInvocation.MyCommand.Name, $OldFunctions.Count, $dynamicCache)
        
                $null = Remove-Item -Path $OldFunctions.fullname -Force -ErrorAction SilentlyContinue
            }
        }
        else {

            Write-Warning ('{0}: Personal disk cache {1} does not exist. Forcing load of all dynamic entities.' -F $MyInvocation.MyCommand.Name, $dynamicCache)
    
            # No personal dynamic cache. Refresh ALL dynamic entities.
            $entityName = '*'
        }

        # Refresh any entities the caller has ordered
        # We only consider entities that are dynamic
        $Entities = Get-AtwsFieldInfo -Dynamic
        $entitiesToProcess = @()

        Write-Debug ('{0}: {1} dynamic entities are eligible for refresh.' -F $MyInvocation.MyCommand.Name, $dynamicCache)

        foreach ($string in $entityName) {
            Write-Debug ('{0}: Selecting entities that match pattern "{1}"' -F $MyInvocation.MyCommand.Name, $string)
      
            $entitiesToProcess += $Entities.GetEnumerator().Where( { $_.Key -like $string })
        }
        # Prepare index for progressbar
        $index = 0
        $progressParameters = @{
            Activity = 'Updating diskcache for requested entities.'
            Id       = 10
        }

        # Make sure we only check each possible entity once
        $entitiesToProcess = $entitiesToProcess | Sort-Object -Property Name -Unique

        Write-Debug ('{0}: {1} entities have been selected for refresh' -F $MyInvocation.MyCommand.Name, $entitiesToProcess.Count)
  

        foreach ($entityToProcess in $entitiesToProcess) {
            $index++
            $percentComplete = $index / $entitiesToProcess.Count * 100

            # Add parameters for @splatting
            $progressParameters['PercentComplete'] = $percentComplete
            $progressParameters['Status'] = 'Entity {0}/{1} ({2:n0}%)' -F $index, $entitiesToProcess.Count, $percentComplete
            $progressParameters['CurrentOperation'] = 'Getting fieldinfo for {0}' -F $entityToProcess.Name

            Write-AtwsProgress @progressParameters

            $null = Get-AtwsFieldInfo -Entity $entityToProcess.Key -UpdateCache
        }
        if ($progressParameters['CurrentOperation']) { 
            Write-AtwsProgress @progressParameters -Completed
        }
        
        if ($entitiesToProcess.Count -gt 0) { 
            Write-Debug ('{0}: Calling Import-AtwsCmdLet with {1} entities to process' -F $MyInvocation.MyCommand.Name, $entitiesToProcess.Count)
  
            # Recreate functions that have been updated
            Import-AtwsCmdLet -Entities $entitiesToProcess

            # Re-read Dynamic functions
            $dynamicFunction = @( Get-ChildItem -Path $dynamicCache\*atws*.ps1 -ErrorAction SilentlyContinue ) 

            Write-Debug ('{0}: Personal disk cache: Found {1} script files in {2}' -F $MyInvocation.MyCommand.Name, $dynamicFunction.Count, $dynamicCache)
        }
    }

    Write-Verbose ('{0}: Importing {1} Static and {2} Dynamic functions.' -F $MyInvocation.MyCommand.Name, $staticFunction.Count, $dynamicFunction.Count)
  
    # Loop through all script files and source them
    foreach ($import in @($staticFunction + $dynamicFunction)) {
        Write-Debug ('{0}: Importing {1}' -F $MyInvocation.MyCommand.Name, $import)

        try {
            . $import.fullname
        }
        catch {
            throw "Could not import function $($import.fullname): $_"
        }
    }
  
    # Explicitly export public functions
    Write-Verbose ('{0}: Exporting {1} Public functions.' -F $MyInvocation.MyCommand.Name, $publicFunction.Count) 
    Export-ModuleMember -Function $publicFunction.Basename

    # Explicitly export static functions
    Write-Verbose ('{0}: Exporting {1} Static functions.' -F $MyInvocation.MyCommand.Name, $staticFunction.Count)
    Export-ModuleMember -Function $staticFunction.Basename

    # Explicitly export dynamic functions
    Write-Verbose ('{0}: Exporting {1} Dynamic functions.' -F $MyInvocation.MyCommand.Name, $dynamicFunction.Count)
    Export-ModuleMember -Function $dynamicFunction.Basename
}
else {
    Write-Verbose 'No Credentials were passed with -ArgumentList. Loading module without any connection to Autotask Web Services. Use Connect-AtwsWebAPI to connect.'
    Export-ModuleMember -Function 'Connect-AtwsWebAPI'
}


# Backwards compatibility since we are now trying to use consistent naming
Set-Alias -Scope Global -Name 'Connect-AutotaskWebAPI' -Value 'Connect-AtwsWebAPI'

# Done loading
$Script:LoadingModule = $false

# Restore Previous preference
if ($oldVerbosePreference -ne $VerbosePreference) {
    Write-Debug ('{0}: Restoring old Verbose preference' -F $MyInvocation.MyCommand.Name)
    $VerbosePreference = $oldVerbosePreference
}
if ($oldDebugPreference -ne $DebugPreference) {
    Write-Debug ('{0}: Restoring old Debug preference' -F $MyInvocation.MyCommand.Name)
    $DebugPreference = $oldDebugPreference
}