From a2339bc179e69f70f40034bf14b3cf7c181f3c55 Mon Sep 17 00:00:00 2001 From: Grant Curell Date: Tue, 16 Feb 2021 16:24:07 -0500 Subject: [PATCH] #138 - script for getting SAE cases - Completed python and powershell versions of a script to retrieve SAE cases - Updated API docs - Fixed a minor bug in CSV output for Set-PowerState.ps1 - Updated library code for outputting to CSV - Completes #138 Signed-off-by: Grant Curell --- Core/PowerShell/Get-SupportassistCases.ps1 | 306 +++++++++++++++++++++ Core/PowerShell/Set-PowerState.ps1 | 2 +- Core/Python/get_supportassist_cases.py | 216 +++++++++++++++ Core/Python/new_template.py | 2 +- docs/API.md | 34 ++- docs/categories.yml | 3 + docs/powershell_library_code.md | 20 +- 7 files changed, 578 insertions(+), 5 deletions(-) create mode 100644 Core/PowerShell/Get-SupportassistCases.ps1 create mode 100644 Core/Python/get_supportassist_cases.py diff --git a/Core/PowerShell/Get-SupportassistCases.ps1 b/Core/PowerShell/Get-SupportassistCases.ps1 new file mode 100644 index 0000000..9b0c55f --- /dev/null +++ b/Core/PowerShell/Get-SupportassistCases.ps1 @@ -0,0 +1,306 @@ +#Requires -Version 7 + +<# +_author_ = Grant Curell + +Copyright (c) 2021 Dell EMC Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +#> + +<# + + .SYNOPSIS + Retrieves the case data from the SupportAssist Enterprise (SAE) Plugin on OME + .DESCRIPTION + The -OutFile argument is optional. If specified the output will go to a CSV file. Otherwise it prints to screen. + + For authentication X-Auth is used over Basic Authentication + Note that the credentials entered are not stored to disk. + .PARAMETER IpAddress + IP Address of the OME Appliance + .PARAMETER Credentials + Credentials used to talk to the OME Appliance + .PARAMETER OutFile + Path to which you want to write the output. + + .EXAMPLE + .\Get-SupportassistCases.ps1' -credentials $creds -outfile test.csv -ipaddress 192.168.1.93 +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [System.Net.IPAddress] $IpAddress, + + [Parameter(Mandatory)] + [pscredential] $Credentials, + + [Parameter(Mandatory=$false)] + [string] $OutFile +) + +function Get-Data { + <# + .SYNOPSIS + Used to interact with API resources + + .DESCRIPTION + This function retrieves data from a specified URL. Get requests from OME return paginated data. The code below + handles pagination. This is the equivalent in the UI of a list of results that require you to go to different + pages to get a complete listing. + + .PARAMETER Url + The API url against which you would like to make a request + + .PARAMETER OdataFilter + An optional parameter for providing an odata filter to run against the API endpoint. + + .PARAMETER MaxPages + The maximum number of pages you would like to return + + .INPUTS + None. You cannot pipe objects to Get-Data. + + .OUTPUTS + dict. A dictionary containing the results of the API call or an empty dictionary in the case of a failure + + #> + + [CmdletBinding()] + param ( + + [Parameter(Mandatory)] + [string] + $Url, + + [Parameter(Mandatory = $false)] + [string] + $OdataFilter, + + [Parameter(Mandatory = $false)] + [int] + $MaxPages = $null + ) + + $Data = @() + $NextLinkUrl = $null + try { + + if ($PSBoundParameters.ContainsKey('OdataFilter')) { + $CountData = Invoke-RestMethod -Uri $Url"?`$filter=$($OdataFilter)" -Method Get -Credential $Credentials -SkipCertificateCheck + + if ($CountData.'@odata.count' -lt 1) { + Write-Error "No results were found for filter $($OdataFilter)." + return @{} + } + } + else { + $CountData = Invoke-RestMethod -Uri $Url -Method Get -Credential $Credentials -ContentType $Type ` + -SkipCertificateCheck + } + + if ($null -ne $CountData.'value') { + $Data += $CountData.'value' + } + else { + $Data += $CountData + } + + if ($CountData.'@odata.nextLink') { + $NextLinkUrl = "https://$($IpAddress)$($CountData.'@odata.nextLink')" + } + + $i = 1 + while ($NextLinkUrl) { + if ($MaxPages) { + if ($i -ge $MaxPages) { + break + } + $i = $i + 1 + } + + $NextLinkData = Invoke-RestMethod -Uri "$($NextLinkUrl)" -Method Get -Credential $Credentials ` + -ContentType $Type -SkipCertificateCheck + + if ($null -ne $NextLinkData.'value') { + $Data += $NextLinkData.'value' + } + else { + $Data += $NextLinkData + } + + if ($NextLinkData.'@odata.nextLink') { + $NextLinkUrl = "https://$($IpAddress)$($NextLinkData.'@odata.nextLink')" + } + else { + $NextLinkUrl = $null + } + } + + return $Data + + } + catch [System.Net.Http.HttpRequestException] { + Write-Error "There was a problem connecting to OME or the URL supplied is invalid. Did it become unavailable?" + return @{} + } +} + +function Read-Confirmation { + <# + .SYNOPSIS + Prompts a user with a yes or no question + + .DESCRIPTION + Prompts a user with a yes or no question. The question text should include something telling the user + to type y/Y/Yes/yes or N/n/No/no + + .PARAMETER QuestionText + The text which you want to display to the user + + .OUTPUTS + Returns true if the user enters yes and false if the user enters no + #> + [CmdletBinding()] + param ( + + [Parameter(Mandatory)] + [string] + $QuestionText + ) + do { + $Confirmation = (Read-Host $QuestionText).ToUpper() + } while ($Confirmation -ne 'YES' -and $Confirmation -ne 'Y' -and $Confirmation -ne 'N' -and $Confirmation -ne 'NO') + + if ($Confirmation -ne 'YES' -and $Confirmation -ne 'Y') { + return $false + } + + return $true +} + +function Confirm-IsValid { + <# + .SYNOPSIS + Tests whether a filepath is valid or not. + + .DESCRIPTION + Performs different tests depending on whether you are testing a file for the ability to read + (InputFilePath) or write (OutputFilePath) + + .PARAMETER OutputFilePath + The path to an output file you want to test + + .PARAMETER InputFilePath + The path to an input file you want to test + + .OUTPUTS + Returns true if the path is valid and false if it is not + #> + [CmdletBinding()] + param ( + + [Parameter(Mandatory = $false)] + [string] + $OutputFilePath, + + [Parameter(Mandatory = $false)] + [string] + $InputFilePath + ) + + if ($PSBoundParameters.ContainsKey('InputFilePath') -and $PSBoundParameters.ContainsKey('OutputFilePath')) { + Write-Error "You can only provide either an InputFilePath or an OutputFilePath." + Exit + } + + # Some of the tests are the same - we can use the same variable name + if ($PSBoundParameters.ContainsKey('InputFilePath')) { + $OutputFilePath = $InputFilePath + } + + if ($PSBoundParameters.ContainsKey('InputFilePath')) { + if (-not $(Test-Path -Path $InputFilePath -PathType Leaf)) { + Write-Error "The file $($InputFilePath) does not exist." + return $false + } + } + else { + if (Test-Path -Path $OutputFilePath -PathType Leaf) { + if (-not $(Read-Confirmation "$($OutputFilePath) already exists. Do you want to continue? (Y/N)")) { + return $false + } + } + } + + $ParentPath = $(Split-Path -Path $OutputFilePath -Parent) + if ($ParentPath -ne "") { + if (-not $(Test-Path -PathType Container $ParentPath)) { + Write-Error "The path '$($OutputFilePath)' does not appear to be valid." + return $false + } + } + + if (Test-Path $(Split-Path -Path $OutputFilePath -Leaf) -PathType Container) { + Write-Error "You must provide a filename as part of the path. It looks like you only provided a folder in $($OutputFilePath)!" + return $false + } + + return $true +} + +try { + + Write-Host "Sending the request to OME..." + $Cases = Get-Data "https://$($IpAddress)/api/SupportAssistService/Cases?sort=id" + + if($Cases.count -gt 0) { + if ($PSBoundParameters.ContainsKey('OutFile')) { + if (-not $(Confirm-IsValid -OutputFilePath $OutFile)) { + Exit + } + + $CasesDictionary = @() + foreach ($Case in $Cases) { + $CaseDict = @{ + "Id" = $Case.Id + "Name" = $Case.Name + "Title" = $Case.Title + "ServiceTag" = $Case.ServiceTag + "Status" = $Case.Status + "EventSource" = $Case.EventSource + "DummyCase" = $Case.DummyCase + "UpdatedDate" = $Case.UpdatedDate + "CreatedDate" = $Case.CreatedDate + } + $CasesDictionary += $CaseDict + } + + $Cases | Export-Csv -Path $OutFile -NoTypeInformation + $(Foreach($Case in $CasesDictionary){ + New-object psobject -Property $Case + }) | Export-Csv $OutFile + } + else { + $Cases + } + } + + Write-Host "Task completed successfully!" + +} +catch { + Write-Error "Exception occured at line $($_.InvocationInfo.ScriptLineNumber) - $($_.Exception.Message)" +} diff --git a/Core/PowerShell/Set-PowerState.ps1 b/Core/PowerShell/Set-PowerState.ps1 index 3266890..b03cde9 100644 --- a/Core/PowerShell/Set-PowerState.ps1 +++ b/Core/PowerShell/Set-PowerState.ps1 @@ -622,7 +622,7 @@ Try { $DevicePowerStates | Export-Csv -Path $CsvFile -NoTypeInformation $(Foreach($Device in $DevicePowerStates){ New-object psobject -Property $Device - }) | Export-Csv test.csv + }) | Export-Csv $CsvFile } else { diff --git a/Core/Python/get_supportassist_cases.py b/Core/Python/get_supportassist_cases.py new file mode 100644 index 0000000..e128941 --- /dev/null +++ b/Core/Python/get_supportassist_cases.py @@ -0,0 +1,216 @@ +# +# _author_ = Grant Curell +# +# Copyright (c) 2021 Dell EMC Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +""" +#### Synopsis +Retrieves the case data from the SupportAssist Enterprise (SAE) Plugin on OME + +#### Description +The --out-file argument is optional. If specified the output will go to a file. Otherwise it prints to screen. + +For authentication X-Auth is used over Basic Authentication +Note that the credentials entered are not stored to disk. + +#### Example + python get_supportassist_cases.py --ip --user --password --out-file +""" + +import argparse +import csv +import json +import sys +from argparse import RawTextHelpFormatter +from urllib.parse import urlparse +from getpass import getpass +from pprint import pprint + +try: + import urllib3 + import requests +except ModuleNotFoundError: + print("This program requires urllib3 and requests. To install them on most systems run `pip install requests" + "urllib3`") + sys.exit(0) + + +def authenticate(ome_ip_address: str, ome_username: str, ome_password: str) -> dict: + """ + Authenticates with OME and creates a session + + Args: + ome_ip_address: IP address of the OME server + + ome_username: Username for OME + ome_password: OME password + + Returns: A dictionary of HTTP headers + + Raises: + Exception: A generic exception in the event of a failure to connect + """ + + authenticated_headers = {'content-type': 'application/json'} + session_url = 'https://%s/api/SessionService/Sessions' % ome_ip_address + user_details = {'UserName': ome_username, + 'Password': ome_password, + 'SessionType': 'API'} + try: + session_info = requests.post(session_url, verify=False, + data=json.dumps(user_details), + headers=authenticated_headers) + except requests.exceptions.ConnectionError: + print("Failed to connect to OME. This typically indicates a network connectivity problem. Can you ping OME?") + sys.exit(0) + + if session_info.status_code == 201: + authenticated_headers['X-Auth-Token'] = session_info.headers['X-Auth-Token'] + return authenticated_headers + + print("There was a problem authenticating with OME. Are you sure you have the right username, password, " + "and IP?") + raise Exception("There was a problem authenticating with OME. Are you sure you have the right username, " + "password, and IP?") + + +def get_data(authenticated_headers: dict, url: str, odata_filter: str = None, max_pages: int = None) -> dict: + """ + This function retrieves data from a specified URL. Get requests from OME return paginated data. The code below + handles pagination. This is the equivalent in the UI of a list of results that require you to go to different + pages to get a complete listing. + + Args: + authenticated_headers: A dictionary of HTTP headers generated from an authenticated session with OME + url: The API url against which you would like to make a request + odata_filter: An optional parameter for providing an odata filter to run against the API endpoint. + max_pages: The maximum number of pages you would like to return + + Returns: Returns a dictionary of data received from OME + + """ + + next_link_url = None + + if odata_filter: + count_data = requests.get(url + '?$filter=' + odata_filter, headers=authenticated_headers, verify=False) + + if count_data.status_code == 400: + print("Received an error while retrieving data from %s:" % url + '?$filter=' + odata_filter) + pprint(count_data.json()['error']) + return {} + + count_data = count_data.json() + if count_data['@odata.count'] <= 0: + print("No results found!") + return {} + else: + count_data = requests.get(url, headers=authenticated_headers, verify=False).json() + + if 'value' in count_data: + data = count_data['value'] + else: + data = count_data + + if '@odata.nextLink' in count_data: + # Grab the base URI + next_link_url = '{uri.scheme}://{uri.netloc}'.format(uri=urlparse(url)) + count_data['@odata.nextLink'] + + i = 1 + while next_link_url is not None: + # Break if we have reached the maximum number of pages to be returned + if max_pages: + if i >= max_pages: + break + else: + i = i + 1 + response = requests.get(next_link_url, headers=authenticated_headers, verify=False) + next_link_url = None + if response.status_code == 200: + requested_data = response.json() + if requested_data['@odata.count'] <= 0: + print("No results found!") + return {} + + # The @odata.nextLink key is only present in data if there are additional pages. We check for it and if it + # is present we get a link to the page with the next set of results. + if '@odata.nextLink' in requested_data: + next_link_url = '{uri.scheme}://{uri.netloc}'.format(uri=urlparse(url)) + \ + requested_data['@odata.nextLink'] + + if 'value' in requested_data: + data += requested_data['value'] + else: + data += requested_data + else: + print("Unknown error occurred. Received HTTP response code: " + str(response.status_code) + + " with error: " + response.text) + raise Exception("Unknown error occurred. Received HTTP response code: " + str(response.status_code) + + " with error: " + response.text) + + return data + + +if __name__ == '__main__': + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=RawTextHelpFormatter) + parser.add_argument("--ip", "-i", required=True, help="OME Appliance IP") + parser.add_argument("--user", "-u", required=False, + help="Username for OME Appliance", default="admin") + parser.add_argument("--password", "-p", required=False, + help="Password for OME Appliance") + parser.add_argument("--out-file", "-f", required=False, + help="The name of a file to which you want to write your VLANs") + args = parser.parse_args() + + base_uri = 'https://%s/api/SupportAssistService/Cases?sort=id' % args.ip + + if not args.password: + args.password = getpass() + + try: + headers = authenticate(args.ip, args.user, args.password) + + if not headers: + sys.exit(0) + + print("Sending the request to OME...") + + cases = get_data(headers, base_uri) + + if cases: + if args.out_file: + # Use UTF 8 in case there are non-ASCII characters like 格蘭特 + print("Writing CSV to file...") + with open(args.out_file, 'w', encoding='utf-8', newline='') as csv_file: + csv_columns = ["Id", "Name", "Title", "ServiceTag", "Status", "EventSource", "ServiceContract", + "DummyCase", "UpdatedDate", "CreatedDate"] + writer = csv.DictWriter(csv_file, fieldnames=csv_columns, extrasaction='ignore') + writer.writeheader() + for case in cases: + writer.writerow(case) + else: + pprint(cases) + else: + print("There was a problem retrieving the SupportAssist data from OME! Exiting.") + sys.exit(0) + + print("Task completed successfully!") + + except Exception as error: + pprint(error) diff --git a/Core/Python/new_template.py b/Core/Python/new_template.py index f87288b..3dd6118 100644 --- a/Core/Python/new_template.py +++ b/Core/Python/new_template.py @@ -112,7 +112,7 @@ def post_data(url: str, authenticated_headers: dict, payload: dict, error_messag return {} -def import_template(ome_ip_address, authenticated_headers, template_name, filename): +def import_template(ome_ip_address, authenticated_headers, template_name, filename) -> bool: """ Imports a template from file diff --git a/docs/API.md b/docs/API.md index 81ddcb4..416896a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -80,6 +80,8 @@ You can find a current copy of the OME API documentation [here](https://dl.dell.
  • Get Report List
  • +
  • Get Supportassist Cases
  • +
  • Invoke Report Execution
  • @@ -631,7 +633,7 @@ Note that the credentials entered are not stored to disk. #### Example python new_template.py --ip 192.168.1.93 --password password --template-file gelante.xml - python new_template.py --ip 192.168.1.93 --password password --template-file gelante.xml --template-name 格蘭特第一實驗 + python new_template.py --ip 192.168.1.93 --password password --template-file gelante.xml --template-name 格蘭特是最好的 @@ -1269,6 +1271,36 @@ PS C:\>$cred = Get-Credential ``` +--- +### Get Supportassist Cases + +#### Available Scripts + +- [get_supportassist_cases.py](../Core/Python/get_supportassist_cases.py) + +- [Get-SupportassistCases.ps1](../Core/PowerShell/Get-SupportassistCases.ps1) + + +#### Synopsis +Retrieves the case data from the SupportAssist Enterprise (SAE) Plugin on OME + +#### Description +The --out-file argument is optional. If specified the output will go to a file. Otherwise it prints to screen. + +For authentication X-Auth is used over Basic Authentication +Note that the credentials entered are not stored to disk. + +#### Example + python get_supportassist_cases.py --ip --user --password --out-file + + +#### PowerShell Example +``` +PS C:\>.\Get-SupportassistCases.ps1' -credentials $creds -outfile test.csv -ipaddress 192.168.1.93 + +``` + + --- ### Invoke Report Execution diff --git a/docs/categories.yml b/docs/categories.yml index ccfcb64..2550a39 100644 --- a/docs/categories.yml +++ b/docs/categories.yml @@ -87,6 +87,9 @@ monitor: get_report_list: - get_report_list.py - Get-ReportList.ps1 + get_supportassist_cases: + - get_supportassist_cases.py + - Get-SupportassistCases.ps1 invoke_report_execution: - invoke_report_execution.py - Invoke-ReportExecution.ps1 diff --git a/docs/powershell_library_code.md b/docs/powershell_library_code.md index d5dac2d..d501273 100644 --- a/docs/powershell_library_code.md +++ b/docs/powershell_library_code.md @@ -397,12 +397,28 @@ else { ### Writing an Array of Hashtables to a CSV File +This is a bit strange in PowerShell. The main thing is that before passing code to the second part (the foreach loop actually doing the export) you have to remove the date from the Get-Data output and manually put it in a PS hashtable. + ```powershell +$DevicePowerStates = @() +foreach ($DeviceId in $Targets) { + $DeviceStatus = Get-Data "https://$($IpAddress)/api/DeviceService/Devices($DeviceId)" + $DevicePowerState = @{ + "OME ID" = $DeviceStatus.Id + Identifier = $DeviceStatus.Identifier + Model = $DeviceStatus.Model + "Device Name" = $DeviceStatus.DeviceName + "idrac IP" = $DeviceStatus.DeviceManagement[0]['NetworkAddress'] + "Power State" = $PowerStateMap[[int]$($DeviceStatus.PowerState)] + } + $DevicePowerStates += $DevicePowerState +} + $DevicePowerStates | Export-Csv -Path $CsvFile -NoTypeInformation # Using $( foreach ($x in $a) {} ) | Export-Csv -$(Foreach($Device in $DevicePowerStates){ +$(foreach($Device in $DevicePowerStates){ New-object psobject -Property $Device -}) | Export-Csv test.csv +}) | Export-Csv $CsvFile ``` See [this StackOverflow link](https://stackoverflow.com/questions/11173795/powershell-convert-array-of-hastables-into-csv)