From 0882b6a6e6669cc29670ecf63dbdb5f42f71689e Mon Sep 17 00:00:00 2001 From: "Rodric GK. Vos" Date: Fri, 24 Aug 2018 17:45:53 +0200 Subject: [PATCH] AnsibleJob.ps1 - improved to coding guidelines. Added function Wait-AnsibleJob. Added function Get-AnsibleJobTemplateID. Added help for function Get-AnsibleJobTemplate. Get-AnsibleJobTemplate accepts a name or id, where name is now the default. Invoke-AnsibleJobTemplate accepts a name or id, where name is now the default. Bumped module version from 0.1 to 0.2. Added function Disable-CertificateVerification. Added function Join-AnsibleUrl. Added function Get-AnsibleResourceUrl, which caches the resource urls. Added help for Connect-AnsibleTower. The module now supports BasicAuth, specified via the switch BasicAuth in Connect-AnsibleTower. Property created in the class User is now of type DateTime?. Added property external_account to class User. Property unified_job_template in class Job is now of type int. Added property launch_type to class Job. Added property status to class Job. Added property failed to class Job. Added property elapsed to class Job. Added property job_explanation to class Job. Property job_template in class Job is now of type int. Added property result_stdout to class Job. --- AnsibleJob.ps1 | 159 ++++-- AnsibleJobTemplate.ps1 | 168 +++++-- AnsibleTower.psd1 | Bin 5742 -> 5742 bytes AnsibleTower.psm1 | 459 +++++++++++++----- .../AnsibleTower/AnsibleTower/DataTypes.cs | 48 +- .../AnsibleTower/bin/Debug/AnsibleTower.dll | Bin 23040 -> 22016 bytes 6 files changed, 611 insertions(+), 223 deletions(-) diff --git a/AnsibleJob.ps1 b/AnsibleJob.ps1 index 623b30a..c17d1e4 100644 --- a/AnsibleJob.ps1 +++ b/AnsibleJob.ps1 @@ -1,69 +1,156 @@ function Get-AnsibleJob { [CmdletBinding()] - Param ( + param( [Parameter(ValueFromPipelineByPropertyName=$true)] - [int]$id + [int]$ID ) - if ($id) - { - $Return = Invoke-GetAnsibleInternalJsonResult -ItemType "jobs" -Id $id - } - Else - { - $Return = Invoke-GetAnsibleInternalJsonResult -ItemType "jobs" + if ($ID) { + $result = Invoke-GetAnsibleInternalJsonResult -ItemType "jobs" -Id $ID; + } else { + $result = Invoke-GetAnsibleInternalJsonResult -ItemType "jobs"; } - - if (!($Return)) - { - #Nothing returned from the call - Return + if (!$result) { + # Nothing returned from the call. + return $null; } - $returnobj = @() - foreach ($jsonorg in $return) + $returnObjs = @(); + foreach ($jsonorg in $result) { - #Shift back to json and let newtonsoft parse it to a strongly named object instead - $jsonorgstring = $jsonorg | ConvertTo-Json - $org = $JsonParsers.ParseToJob($jsonorgstring) - $returnobj += $org; $org = $null + # Shift back to json and let newtonsoft parse it to a strongly named object instead. + $jsonorgstring = $jsonorg | ConvertTo-Json; + $org = $JsonParsers.ParseToJob($jsonorgstring); + $returnObjs += $org; + $org = $null; } - #return the things - $returnobj + + # Return the job(s). + $returnObjs; } function Invoke-AnsibleJob { [CmdletBinding()] - Param ( - [Parameter(ValueFromPipelineByPropertyName=$true,Mandatory=$true,ParameterSetName='ByObj')] + param( + [Parameter(ValueFromPipelineByPropertyName=$true,Mandatory=$true,Position=0,ParameterSetName='ByObj')] [AnsibleTower.JobTemplate]$JobTemplate, - [Parameter(ValueFromPipelineByPropertyName=$true,Mandatory=$true,ParameterSetName='ById')] - [int]$id + [Parameter(ValueFromPipelineByPropertyName=$true,Mandatory=$true,Position=0,ParameterSetName='ById')] + [int]$ID ) if ($JobTemplate) { $ThisJobTemplate = $JobTemplate - $id = $ThisJobTemplate.id + $ID = $ThisJobTemplate.id } Else { - $ThisJobTemplate = Get-AnsibleJobTemplate -id $id + $ThisJobTemplate = Get-AnsibleJobTemplate -id $ID; } - if (!$ThisJobTemplate) {Write-Error "No Job template with id $id"; return} + if (!$ThisJobTemplate) { + throw ("Job template with id [{0}] not found" -f $ID); + } - Write-Verbose "Submitting job from template $id" - $result = Invoke-PostAnsibleInternalJsonResult -ItemType "job_templates" -itemId $id -ItemSubItem "jobs" - $JobId = $result.id - Write-Verbose "Starting job with jobid $jobid" - $result = Invoke-PostAnsibleInternalJsonResult -ItemType "jobs" -itemId $JobId -ItemSubItem "start" - $job = get-ansibleJob -id $JobId - $job + Write-Verbose ("Creating job from job template [{0}]" -f $ID); + $result = Invoke-PostAnsibleInternalJsonResult -ItemType "job_templates" -itemId $id -ItemSubItem "jobs"; + $JobID = $result.id; + Write-Verbose ("Starting job with id [{0}]" -f $JobID); + $result = Invoke-PostAnsibleInternalJsonResult -ItemType "jobs" -itemId $JobId -ItemSubItem "start"; + Get-AnsibleJob -ID $JobId } +function Wait-AnsibleJob +{ + <# + .SYNOPSIS + Waits for an Ansible job to finish. + + .DESCRIPTION + Waits for an Ansible job to finish by monitoring the 'finished' property of the job. + Every Interval the job details are requested and while 'finished' is empty the job is considered to be still running. + When the job is finished, the function returns. The caller must analyse the job state and/or result. + Inspect the status, failed and result_output properties for more information on the job result. + + If the Timeout has expired an exception is thrown. + + .PARAMETER Job + The Job object as returned by Get-AnsibleJob or Invoke-AnsibleJobTemplate. + + .PARAMETER ID + The job ID. + + .PARAMETER Timeout + The timeout in seconds to wait for the job to finish. + + .PARAMETER Interval + The interval in seconds at which the job status is inspected. + + .EXAMPLE + $job = Invoke-AnsibleJobTemplate 'Demo Job Template' + Wait-AnsibleJob -ID $job.id + + Starts a new job for job template 'Demo Job Template' and then waits for the job to finish. Inspect the $job properties status, failed and result_stdout for more details. + + .EXAMPLE + $job = Invoke-AnsibleJobTemplate 'Demo Job Template' | Wait-AnsibleJob -Interval 1 + + Starts a new job for job template 'Demo Job Template' and then waits for the job to finish by polling every second. Inspect the $job properties status, failed and result_stdout for more details. + + .EXAMPLE + $job = Invoke-AnsibleJobTemplate 'Demo Job Template' | Wait-AnsibleJob -Interval 5 -Timeout 60 + + Starts a new job for job template 'Demo Job Template' and then waits for the job to finish by polling every 5 seconds. If the job did not finish after 60 seconds, an exception is thrown. + Inspect the $job properties status, failed and result_stdout for more details. + + .OUTPUTS + The job object. + #> + [CmdletBinding(DefaultParameterSetName='Job')] + param( + [Parameter(ValueFromPipelineByPropertyName=$true,Mandatory=$true,Position=0,ParameterSetName='Job')] + [AnsibleTower.Job]$Job, + + [Parameter(ValueFromPipelineByPropertyName=$true,Mandatory=$true,Position=0,ParameterSetName='ID')] + [int]$ID, + + [int]$Timeout = 3600, + [int]$Interval = 3 + ) + + if ($ID) { + $Job = Get-AnsibleJob -id $ID; + if (!$Job) { + throw ("Failed to get job with id [{0}]" -f $ID) + } + } + + Write-Verbose ("Waiting for job [{0}] to finish..." -f $Job.id); + $startDate = Get-Date; + $finished = $false; + while (!$finished) + { + if (![string]::IsNullOrEmpty($Job.finished)) { + Write-Verbose ("Job [{0}] finished." -f $Job.id); + $finished = $true; + } else { + $timeSpan = New-TimeSpan -Start $startDate -End (Get-Date); + Write-Verbose ("Waiting for job [{0}] to finish. Job status is [{1}]. Elapsed time is [{2}] seconds." -f $Job.id,$Job.status,[math]::Round($timeSpan.TotalSeconds)); + if ($timeSpan.TotalSeconds -ge $Timeout) { + throw ("Timeout waiting for job [{0}] to finish" -f $Job.id); + } + + Write-Verbose ("Sleeping [{0}] seconds..." -f $Interval); + sleep -Seconds $Interval + } + $Job = Get-AnsibleJob -id $Job.id; + } + + # Return the job object. + $Job +} diff --git a/AnsibleJobTemplate.ps1 b/AnsibleJobTemplate.ps1 index 86a2a93..081fcdf 100644 --- a/AnsibleJobTemplate.ps1 +++ b/AnsibleJobTemplate.ps1 @@ -1,75 +1,169 @@ -function Get-AnsibleJobTemplate +function Get-AnsibleJobTemplateID { <# - .SYNOPSIS + .SYNOPSIS + Gets the job template ID from a job template name. + + .EXAMPLE + Get-AnsibleJobTemplateID -Name 'Demo Job Template' - Gets one or multiple Job Templates + .EXAMPLE + 'Demo Job Template' | Get-AnsibleJobTemplateID + .OUTPUTS + The job ID. #> + [CmdletBinding()] + param( + [Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Position=0)] + [string]$Name + ) + + # We should be passing a search term here, prevents from passing all jobs via the REST call. + (Get-AnsibleJobTemplate | ? { $_.name -eq $Name }).id +} + +function Get-AnsibleJobTemplate +{ + <# + .SYNOPSIS + Gets one or all job templates. + + .EXAMPLE + Get-AnsibleJobTemplate + + Gets all job templates. + + .EXAMPLE + Get-AnsibleJobTemplate | where { $_.project -eq 4 } + + Gets all job templates that belong to project ID 4. + + .EXAMPLE + Get-AnsibleJobTemplate 'Demo Job Template' + + Gets details about job template named 'Demo Job Template'. + + .EXAMPLE + $jobTemplate = Get-AnsibleJobTemplate -ID 5 + + .OUPUTS + Strongly typed job template object(s). + #> + [CmdletBinding(DefaultParameterSetName='Name')] Param ( - [Parameter(ValueFromPipelineByPropertyName=$true)] - [int]$id + [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Position=0,ParameterSetName='Name')] + [string]$Name, + + [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Position=0,ParameterSetName='ID')] + [int]$ID ) - if ($id) - { - $Return = Invoke-GetAnsibleInternalJsonResult -ItemType "job_templates" -Id $id + if ($Name) { + $ID = Get-AnsibleJobTemplateID -Name $Name + if (!$ID) { + throw ("Failed to get the ID for job template named [{0}]" -f $Name) + } } - Else - { - $Return = Invoke-GetAnsibleInternalJsonResult -ItemType "job_templates" + + if ($ID) { + $return = Invoke-GetAnsibleInternalJsonResult -ItemType "job_templates" -Id $ID + } else { + $return = Invoke-GetAnsibleInternalJsonResult -ItemType "job_templates" } - - if (!($Return)) + if (!$return) { - #Nothing returned from the call - Return + # Nothing returned from the call + return } - $returnobj = @() + + $returnObjs = @() foreach ($jsonorg in $return) { - #Shift back to json and let newtonsoft parse it to a strongly named object instead + # Shift back to json and let newtonsoft parse it to a strongly named object instead $jsonorgstring = $jsonorg | ConvertTo-Json $org = $JsonParsers.ParseToJobTemplate($jsonorgstring) - $returnobj += $org; $org = $null + $returnObjs += $org; + $org = $null } #return the things - $returnobj + $returnObjs } function Invoke-AnsibleJobTemplate { <# - .SYNOPSIS + .SYNOPSIS + Runs an Ansible job template. + + .PARAMETER Name + Name of the Ansible job template. - Invokes an Ansible Job Template + .PARAMETER ID + ID of the Ansible job template. - .EXAMPLE + .PARAMETER Data + Any additional data to be supplied to Tower in order to run the job template. Most common is "extra_vars". + Supply a normal Powershell hash table. It will be converted to JSON. See the examples for more information. - Connect-AnsibleTower -Credential (get-credential) -TowerUrl "https://mytower" -DisableCertificateVerification - $jobtemplate = get-ansibleJobTemplate -id 5 - $jobtemplate | Invoke-AnsibleJobTemplate + .EXAMPLE + Invoke-AnsibleJobTemplate -Name 'Demo Job Template' + + Runs a job for job template named 'Demo Job Template'. + + .EXAMPLE + $job = Invoke-AnsibleJobTemplate -ID 5 + + Runs a job for job template with ID 5. + + .EXAMPLE + $jobTemplateData = @{ + "extra_vars" = @{ + 'var1' = 'value1'; + 'var2' = 'value2'; + }; + } + $job = Invoke-AnsibleJobTemplate -Name 'My Ansible Job Template' -Data $jobTemplateData + Launches job template named 'My Ansible Job Template' and passes extra variables for the job to run with. + + .OUTPUTS + Strongly typed job object. #> - [CmdletBinding()] + [CmdletBinding(DefaultParameterSetName='Name')] Param ( - [Parameter(ValueFromPipelineByPropertyName=$true,Mandatory=$true)] - [int]$id - ) + [Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Position=0,ParameterSetName='Name')] + [string]$Name, - $ThisJobTemplate = Get-AnsibleJobTemplate -id $id + [Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Position=0,ParameterSetName='ID')] + [int]$ID, - if (!$ThisJobTemplate) {Write-Error "No Job template with id $id"; return} - - $result = Invoke-PostAnsibleInternalJsonResult -ItemType "job_templates" -itemId $id -ItemSubItem "jobs" - $JobId = $result.id - $job = get-ansibleJob -id $JobId - $job -} + [Object]$Data + ) + if ($Name) { + $ID = Get-AnsibleJobTemplateID -Name $Name + if (!$ID) { + throw ("Failed to get the ID for job template named [{0}]" -f $Name) + } + } + $params = @{ + ItemType = 'job_templates'; + itemId = $ID; + ItemSubItem = 'launch'; + }; + if ($Data) { + $params.Add('InputObject', $Data); + } + $result = Invoke-PostAnsibleInternalJsonResult @params; + if (!$result -and !$result.id) { + throw ("Failed to start job for job template ID [{0}]" -f $ID); + } + Get-AnsibleJob -id $result.id +} diff --git a/AnsibleTower.psd1 b/AnsibleTower.psd1 index 7af3ea8233b59036a066f002dffc83d71117c593..0a6c41be9c769b24b16b1a56321036897aca563c 100644 GIT binary patch delta 14 VcmaE-^G;`j1{0&vW=$qn5dbQz1bF}e delta 14 VcmaE-^G;`j1{0& -#Compile the .net classes -$ClassPath = Join-Path $PSScriptRoot "AnsibleTowerClasses\AnsibleTower\AnsibleTower\DataTypes.cs" -#$ClassPath2 = Join-Path $PSScriptRoot "AnsibleTower (c# project)\AnsibleTower\AnsibleTower\JsonParsers.cs" -$Code = Get-Content -Path $ClassPath -Raw -#$Code2 = Get-Content -Path $ClassPath2 -Raw + # Danm you here-strings for messing up my indendation!! + Add-Type @" + using System.Net; + using System.Security.Cryptography.X509Certificates; + + public class NoSSLCheckPolicy : ICertificatePolicy { + public NoSSLCheckPolicy() {} + public bool CheckValidationResult( + ServicePoint sPoint, X509Certificate cert, + WebRequest wRequest, int certProb) { + return true; + } + } +"@ + [System.Net.ServicePointManager]::CertificatePolicy = new-object NoSSLCheckPolicy +} -add-type -TypeDefinition $Code -ReferencedAssemblies $NewtonSoftJsonPath +function Join-AnsibleUrl +{ + <# + .SYNOPSIS + Joins url parts together into a valid Tower url. -#Load the json parsers to have it handy whenever. -$JsonParsers = New-Object AnsibleTower.JsonFunctions + .PARAMETER Parts + Url parts that will be joined together. + .EXAMPLE + Join-AnsibleUrl 'https://tower.domain.com','api','v1','job_templates' -#Dot-source/Load the other powershell scripts -Get-ChildItem "*.ps1" -path $PSScriptRoot | where {$_.Name -notmatch "test"} | ForEach-Object { . $_.FullName } + .OUTPUTS + Combined url with a trailing slash. + #> + param( + [string[]]$Parts + ) + + return (($Parts | ? { $_ } | % { $_.trim('/').trim() } | ? { $_ } ) -join '/') + '/'; +} -Function Invoke-GetAnsibleInternalJsonResult +function Get-AnsibleResourceUrl { - Param ($AnsibleUrl=$AnsibleUrl, - [System.Management.Automation.PSCredential]$Credential=$AnsibleCredential, - $ItemType, - $Id, - $ItemSubItem) + <# + .SYNOPSIS + Gets the url part for a Tower API resource of function. + + .PARAMETER Resource + The resource name to get the API url for. + + .EXAMPLE + Get-AnsibleResourceUrl 'job_templates' + Returns: "/api/v1/job_templates/" + + .OUTPUTS + API url part for the specified resource, e.g. "/api/v1/job_templates/" + #> + param( + [Parameter(Mandatory=$true)] + [string]$Resource + ) - if ((!$AnsibleUrl) -or (!$Credential)) + $cachedUrl = $script:AnsibleResourceUrlCache[$Resource]; + if ($cachedUrl) { + return $cachedUrl; + } + + $args = @{ + Uri = $script:TowerApiUrl; + }; + if ($script:AnsibleUseBasicAuth) { - throw "You need to connect first, use Connect-AnsibleTower" + Write-Verbose "Get-AnsibleResourceUrl: Using Basic Authentication"; + $args.Add('Headers',$script:AnsibleBasicAuthHeaders); } - $Result = Invoke-RestMethod -Uri ($AnsibleUrl + "/api/v1/") -Credential $Credential - $ItemApiUrl = $result.$ItemType - if ($id) + else { - $ItemApiUrl += "$id/" + Write-Verbose "Get-AnsibleResourceUrl: Using detected Authentication"; + $args.Add('Credential',$script:AnsibleCredential); + } + $result = Invoke-RestMethod @args; + if (!$result) { + throw "Failed to access the Tower api list"; + } + if (!$result.$Resource) { + throw ("Failed to find the url for resource [{0}]" -f $Resource); } - if ($ItemSubItem) - { - $ItemApiUrl = $ItemApiUrl + "/$ItemSubItem/" + $script:AnsibleResourceUrlCache.Add($Resource,$result.$Resource); + + return $result.$Resource; +} + +function Invoke-GetAnsibleInternalJsonResult +{ + param( + [Parameter(Mandatory=$true)] + $ItemType, + + $Id, + $ItemSubItem + ) + + if (!$script:AnsibleUrl -and (!$script:AnsibleCredential -or !$script:AnsibleBasicAuthHeaders)) { + throw "You need to connect first, use Connect-AnsibleTower"; + } + + $ItemApiUrl = Get-AnsibleResourceUrl $ItemType + + if ($id) { + $ItemApiUrl = Join-AnsibleUrl $ItemApiUrl, $id } - $ItemApiUrl = $ItemApiUrl.Replace("//","/") + if ($ItemSubItem) { + $ItemApiUrl = Join-AnsibleUrl $ItemApiUrl, $ItemSubItem + } - $invokeresult = Invoke-RestMethod -Uri ($ansibleurl + $ItemApiUrl) -Credential $Credential + $params = @{ + 'Uri' = (Join-AnsibleUrl $script:AnsibleUrl,$ItemApiUrl); + 'ErrorAction' = 'Stop'; + } + if ($id -eq $null -and $ItemSubItem -eq $null) { + Write-Verbose "Appending ?page_size=1000 to url"; + $params.Uri += '?page_size=1000'; + } - if ($InvokeResult.id) + if ($script:AnsibleUseBasicAuth) { - return $InvokeResult + Write-Verbose "Invoke-GetAnsibleInternalJsonResult: Using Basic Authentication"; + $params.Add('Headers',$script:AnsibleBasicAuthHeaders); } - Elseif ($InvokeResult.results) + else { - return $InvokeResult.results + Write-Verbose "Invoke-GetAnsibleInternalJsonResult: Using detected Authentication"; + $params.Add('Credential',$script:AnsibleCredential); + } + Write-Verbose ("Invoke-GetAnsibleInternalJsonResult: Invoking url [{0}]" -f $params.Uri); + $invokeResult = Invoke-RestMethod @params; + if ($invokeResult.id) { + return $invokeResult + } + Elseif ($invokeResult.results) { + return $invokeResult.results } - } Function Invoke-PostAnsibleInternalJsonResult { - Param ( - $AnsibleUrl=$AnsibleUrl, - [System.Management.Automation.PSCredential]$Credential=$AnsibleCredential, + param( + [Parameter(Mandatory=$true)] $ItemType, + $itemId, $ItemSubItem, - $InputObject) + $InputObject + ) - if ((!$AnsibleUrl) -or (!$Credential)) - { - throw "You need to connect first, use Connect-AnsibleTower" + if (!$script:AnsibleUrl -and (!$script:AnsibleCredential -or !$script:AnsibleBasicAuthHeaders)) { + throw "You need to connect first, use Connect-AnsibleTower"; } - $Result = Invoke-RestMethod -Uri ($AnsibleUrl + "/api/v1/") -Credential $Credential - $ItemApiUrl = $result.$ItemType - - if ($itemId) - { - $ItemApiUrl = $ItemApiUrl + "$itemId" + + $ItemApiUrl = Get-AnsibleResourceUrl $ItemType + + if ($itemId) { + $ItemApiUrl = Join-AnsibleUrl $ItemApiUrl, $itemId } - if ($ItemSubItem) - { - $ItemApiUrl = $ItemApiUrl + "/$ItemSubItem/" + if ($ItemSubItem) { + $ItemApiUrl = Join-AnsibleUrl $ItemApiUrl, $ItemSubItem } - $Params = @{ - "uri"=($ansibleurl + $ItemApiUrl); - "Credential"=$Credential; - } - if ($InputObject) - { + $params = @{ + 'Uri' = Join-AnsibleUrl $script:AnsibleUrl, $ItemApiUrl; + 'Method' = 'Post'; + 'ContentType' = 'application/json'; + 'ErrorAction' = 'Stop'; + } + if ($InputObject) { $params.Add("Body",($InputObject | ConvertTo-Json -Depth 99)) } - Write-Debug "Credentials are: $($credential.UserName)" - Write-Debug "Invoking call with body: $($InputObject | ConvertTo-Json -Depth 99)" - $invokeresult += Invoke-RestMethod -Method Post -ContentType "application/json" @params - $invokeresult - + if ($script:AnsibleUseBasicAuth) + { + Write-Verbose "Invoke-GetAnsibleInternalJsonResult: Using Basic Authentication"; + $params.Add('Headers',$script:AnsibleBasicAuthHeaders); + } + else + { + Write-Verbose "Invoke-GetAnsibleInternalJsonResult: Using detected Authentication"; + $params.Add('Credential',$script:AnsibleCredential); + } + Write-Verbose ("Invoke-PostAnsibleInternalJsonResult: Invoking url [{0}]" -f $params.Uri); + return Invoke-RestMethod @params } Function Invoke-PutAnsibleInternalJsonResult { Param ( - $AnsibleUrl=$AnsibleUrl, - [System.Management.Automation.PSCredential]$Credential=$AnsibleCredential, - $ItemType,$InputObject + $ItemType, + $InputObject ) - if ((!$AnsibleUrl) -or (!$Credential)) + if (!$script:AnsibleUrl -and (!$script:AnsibleCredential -or !$script:AnsibleBasicAuthHeaders)) { + throw "You need to connect first, use Connect-AnsibleTower"; + } + $ItemApiUrl = Get-AnsibleResourceUrl $ItemType + + $id = $InputObject.id + + $ItemApiUrl = Join-AnsibleUrl $ItemApiUrl, $id + + $params = @{ + 'Uri' = Join-AnsibleUrl $script:AnsibleUrl, $ItemApiUrl; + 'Method' = 'Put'; + 'ContentType' = 'application/json'; + 'Body' = ($InputObject | ConvertTo-Json -Depth 99); + 'ErrorAction' = 'Stop'; + } + + if ($script:AnsibleUseBasicAuth) { - throw "You need to connect first, use Connect-AnsibleTower" + Write-Verbose "Invoke-GetAnsibleInternalJsonResult: Using Basic Authentication"; + $params.Add('Headers',$script:AnsibleBasicAuthHeaders); } - $Result = Invoke-RestMethod -Uri ($AnsibleUrl + "/api/v1/") -Credential $Credential - $ItemApiUrl = $result.$ItemType - if (!($id)) + else { - Write-Error "I couldnt find the id of that object" - return + Write-Verbose "Invoke-GetAnsibleInternalJsonResult: Using detected Authentication"; + $params.Add('Credential',$script:AnsibleCredential); } - $id = $InputObject.id + Write-Verbose ("Invoke-PutAnsibleInternalJsonResult: Invoking url [{0}]" -f $params.Uri); + return Invoke-RestMethod @params; +} - $ItemApiUrl += "$id/" +function Connect-AnsibleTower +{ + <# + .SYNOPSIS + Connects to the Tower API and returns the user details. + .PARAMETER Credential + Credential to authenticate with at the Tower API. - $invokeresult += Invoke-RestMethod -Uri ($ansibleurl + $ItemApiUrl) -Credential $Credential -Method Put -Body ($InputObject | ConvertTo-Json -Depth 99) -ContentType "application/json" - $invokeresult + .PARAMETER UserName + Username for connecting to AnsibleTower. -} + .PARAMETER Password + Password of type SecureString for the UserName. -Function Connect-AnsibleTower -{ - Param ( + .PARAMETER PlainPassword + Password in plain text for the UserName. + + .PARAMETER TowerUrl + Url of the Tower host, e.g. https://ansible.mydomain.local + + .PARAMETER DisableCertificateVerification + Disables Certificate verification. Use when Tower responds with 'troublesome' certificates, such as self-signed. + + .PARAMETER BasicAuth + Forces the AnsibleTower module to use Basic authentication when communicating with AnsibleTower. + + .EXAMPLE + Connect-AnsibleTower -Credential (Get-Credential) -TowerUrl 'https://ansible.domain.local' + + User is prompted for credentials and then connects to the Tower host at 'https://ansible.domain.local'. User details are displayed in the output. + + .EXAMPLE + $me = Connect-AnsibleTower -Credential $myCredential -TowerUrl 'https://ansible.domain.local' -DisableCertificateVerification + + Connects to the Tower host at 'https://ansible.domain.local' using the credential supplied in $myCredential. Any certificate errors are ignored. + User details beloning to the specified credential are in the $me variable. + + .EXAMPLE + $me = Connect-AnsibleTower -UserName srvcAsnible -Password $securePassword -BasicAuth -TowerUrl 'https://ansible.domain.local' + + Connects to the Tower host at 'https://ansible.domain.local' using the specified UserName and Password. The username and password are send with each request to force basic authentication. + #> + param ( + [Parameter(Mandatory=$true, ParameterSetName="Credential")] [System.Management.Automation.PSCredential]$Credential, + + [Parameter(Mandatory=$true, ParameterSetName="SecurePassword")] + [Parameter(ParameterSetName="PlainPassword")] + [string]$UserName, + [Parameter(Mandatory=$true, ParameterSetName="PlainPassword")] + [string]$PlainPassword, + [Parameter(Mandatory=$true, ParameterSetName="SecurePassword")] + [securestring]$Password, + + [Parameter(Mandatory=$true)] [string]$TowerUrl, + + [Switch]$BasicAuth, + [Switch]$DisableCertificateVerification ) if ($DisableCertificateVerification) { - #Danm you, here-strings for messing up my indendation!! - add-type @" - using System.Net; - using System.Security.Cryptography.X509Certificates; - - public class NoSSLCheckPolicy : ICertificatePolicy { - public NoSSLCheckPolicy() {} - public bool CheckValidationResult( - ServicePoint sPoint, X509Certificate cert, - WebRequest wRequest, int certProb) { - return true; - } - } -"@ - [System.Net.ServicePointManager]::CertificatePolicy = new-object NoSSLCheckPolicy + Disable-CertificateVerification; } - #Try and figure out what address we were given - - if ($TowerUrl -match "/api/") - { + if ($TowerUrl -match "/api") { throw "Specify the URL without the /api part" } - Else - { - $TestUrl = $TowerUrl + "/api/" - } - - #$towerurl = $TowerUrl.Replace("//","/") try { + [System.Uri]$uri = $TowerUrl; + if ($uri.Scheme -eq "https") + { + Write-Verbose "TowerURL scheme is https. Enforcing TLS 1.2"; + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; + } + Write-Verbose "Determining current Tower API version url..." + $TestUrl = Join-AnsibleUrl $TowerUrl, 'api' + Write-Verbose "TestUrl=$TestUrl" $result = Invoke-RestMethod -Uri $TestUrl -ErrorAction Stop + if (!$result.current_version) { + throw "Could not determine current version of Tower API"; + } + $TowerApiUrl = Join-AnsibleUrl $TowerUrl, $result.current_version } catch { - Throw "That didn't work at all" + throw ("Could not connect to Tower api url: " + $_.Exception.Message); } - - #Get the version - $TowerVersion = $result.current_version - $TowerApiUrl = $TowerUrl + $TowerVersion - #Try to log on - $MeUri = $TowerApiUrl + "me/" try { - $MeResult = Invoke-RestMethod -Uri $MeUri -Credential $Credential -ErrorAction Stop + switch($PsCmdlet.ParameterSetName) + { + "Credential" { + Write-Verbose "Extracting username and password from credential"; + $UserName = $Credential.UserName; + $PlainPassword = $Credential.GetNetworkCredential().Password; + } + "PlainPassword" { + Write-Verbose "Constructing credential object from UserName and PlainPassword"; + $Credential = New-Object System.Management.Automation.PSCredential ($UserName, (ConvertTo-SecureString $PlainPassword -AsPlainText -Force)); + } + "SecurePassword" { + Write-Verbose "Constructing credential object from UserName and SecurePassword"; + $Credential = New-Object System.Management.Automation.PSCredential ($UserName, $Password); + $PlainPassword = $Credential.GetNetworkCredential().Password; + } + } + + $params = @{ + Uri = Join-AnsibleUrl $TowerApiUrl, 'me'; + ErrorAction = 'Stop'; + }; + + if ($BasicAuth.IsPresent) + { + Write-Verbose "Constructing headers for Basic Authentication."; + $encodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes("$($UserName):$($PlainPassword)")); + $headers = @{ + Authorization = "Basic $encodedCreds" + } + $script:AnsibleUseBasicAuth = $true; + $params.Add('Headers', $headers); + } + else { + Write-Verbose "Using credential for authentication." + $params.Add('Credential', $Credential); + } + + Write-Verbose "Connecting to AnsibleTower, requesting the /me function." + $meResult = Invoke-RestMethod @params; + if (!$meResult -or !$meResult.results) { + throw "Could not authenticate to Tower"; + } + $me = $JsonParsers.ParseToUser((ConvertTo-Json ($meResult.results | select -First 1))); + Write-Verbose "Connection to AnsibleTower successful." } Catch { - throw "Could not authenticate" + throw "Could not authenticate: " + $_.Exception.Message; } - - #Code for error-handling goes here - #If we got this far, we could connect. Go ahead and get a session ticket - - #Set the global connection var - #$MDwebapiurl = $WebApiUrl - set-variable -Name AnsibleUrl -Value $TowerUrl -Scope 1 - - set-variable -Name AnsibleCredential -Value $Credential -Scope 1 - -} + # Connection and login success. + $script:AnsibleUrl = $TowerUrl; + $script:TowerApiUrl = $TowerApiUrl; + $script:AnsibleCredential = $Credential; + $script:AnsibleBasicAuthHeaders = $headers; + return $me; +} diff --git a/AnsibleTowerClasses/AnsibleTower/AnsibleTower/DataTypes.cs b/AnsibleTowerClasses/AnsibleTower/AnsibleTower/DataTypes.cs index ba91429..c56e5ff 100644 --- a/AnsibleTowerClasses/AnsibleTower/AnsibleTower/DataTypes.cs +++ b/AnsibleTowerClasses/AnsibleTower/AnsibleTower/DataTypes.cs @@ -1,10 +1,6 @@ -using System; +using Newtonsoft.Json; +using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Newtonsoft.Json; - namespace AnsibleTower { @@ -23,18 +19,19 @@ public class Organization public class User { - - public int id { get; set; } - public string type { get; set; } - public string url { get; set; } - public string created { get; set; } - public string username { get; set; } - public string first_name { get; set; } - public string last_name { get; set; } - public string email { get; set; } - public bool is_superuser { get; set; } - public string ldap_dn { get; set; } - + public int id { get; set; } + public string type { get; set; } + public string url { get; set; } + //public string created { get; set; } + public string username { get; set; } + public string first_name { get; set; } + public string last_name { get; set; } + public string email { get; set; } + public bool is_superuser { get; set; } + public bool is_system_auditor { get; set; } + public string ldap_dn { get; set; } + public string external_account { get; set; } + public DateTime? created { get; set; } } public class Project @@ -89,9 +86,16 @@ public class JobTemplate public class Job { public int id { get; set; } + public string type { get; set; } + public string url { get; set; } public string name { get; set; } public string description { get; set; } - //public object unified_job_template { get; set; } + public int unified_job_template { get; set; } + public string launch_type { get; set; } + public string status { get; set; } + public bool failed { get; set; } + public double elapsed { get; set; } + public string job_explanation { get; set; } public string job_type { get; set; } public object inventory { get; set; } public object project { get; set; } @@ -103,7 +107,8 @@ public class Job public int verbosity { get; set; } public string extra_vars { get; set; } public string job_tags { get; set; } - //public object job_template { get; set; } + public int job_template { get; set; } + public string result_stdout { get; set; } public DateTime? started { get; set; } public DateTime? finished { get; set; } } @@ -216,8 +221,5 @@ public AnsibleTower.Group ParseToGroup(string JsonString) AnsibleTower.Group ConvertedObject = JsonConvert.DeserializeObject(JsonString); return ConvertedObject; } - } - - } diff --git a/AnsibleTowerClasses/AnsibleTower/AnsibleTower/bin/Debug/AnsibleTower.dll b/AnsibleTowerClasses/AnsibleTower/AnsibleTower/bin/Debug/AnsibleTower.dll index 3fc439f1281f9efdaef82dfb73edc997d829cc40..a3d4367d5e7365da423d63e4390ca464a4e4341a 100644 GIT binary patch literal 22016 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 P^=sL?-*oC%jKKc`Q!4VW