From ed6468a6267bcbca94f13e040756f19bad3a4a82 Mon Sep 17 00:00:00 2001 From: Omar Sanchez Date: Thu, 10 Oct 2019 13:36:25 -0400 Subject: [PATCH 1/3] added support for Dell API version 5 --- Files/shared.py | 6 +- Files/warranty.cfg.example | 5 +- Files/warranty_dell.py | 267 ++++++++++++++++++++++--------------- starter.py | 3 +- 4 files changed, 166 insertions(+), 115 deletions(-) diff --git a/Files/shared.py b/Files/shared.py index 78ca133..5bb397f 100644 --- a/Files/shared.py +++ b/Files/shared.py @@ -81,10 +81,12 @@ def __get_discover_cfg(self): def __get_dell_cfg(self): # Dell --------------------------------------------- dell_url = self.cc.get('dell', 'url') - dell_api_key = self.cc.get('dell', 'api_key') + dell_client_id = self.cc.get('dell', 'client_id') + dell_client_secret = self.cc.get('dell', 'client_secret') return { 'url': dell_url, - 'api_key': dell_api_key + 'client_id': dell_client_id, + 'client_secret': dell_client_secret } def __get_hp_cfg(self): diff --git a/Files/warranty.cfg.example b/Files/warranty.cfg.example index 031851f..b4b7f10 100644 --- a/Files/warranty.cfg.example +++ b/Files/warranty.cfg.example @@ -15,8 +15,9 @@ forcedupdate = False [dell] # set api_key as provided by Dell -api_key = -url = https://sandbox.api.dell.com/support/assetinfo/v4/getassetwarranty +client_id = +client_secret = +url = https://apigtwb2c.us.dell.com/PROD/sbil/eapi/v5/asset-entitlements [hp] # set api_key as provided by HP diff --git a/Files/warranty_dell.py b/Files/warranty_dell.py index 826296d..edcb01e 100644 --- a/Files/warranty_dell.py +++ b/Files/warranty_dell.py @@ -1,7 +1,9 @@ +import json import sys import time import random import requests +from datetime import datetime, timedelta from shared import DEBUG, RETRY, ORDER_NO_TYPE, left from warranty_abstract import WarrantyBase @@ -16,7 +18,8 @@ class Dell(WarrantyBase, object): def __init__(self, params): super(Dell, self).__init__() self.url = params['url'] - self.api_key = params['api_key'] + self.client_id = params['client_id'] + self.client_secret = params['client_secret'] self.debug = DEBUG self.retry = RETRY self.order_no = ORDER_NO_TYPE @@ -26,6 +29,42 @@ def __init__(self, params): if self.order_no == 'common': self.common = self.generate_random_order_no() + # OAuth 2.0 + self.expires_at = None + self.access_token = None + + # OAth 2.0 + def get_access_token(self, client_id, client_secret): + access_token_request_url = "https://apigtwb2c.us.dell.com/auth/oauth/v2/token" + + timeout = 10 + + payload = { + 'client_id': client_id, + 'client_secret': client_secret, + 'grant_type': 'client_credentials' + } + try: + resp = requests.post(access_token_request_url, data=payload, timeout=timeout) + + msg = 'Status code: %s' % str(resp.status_code) + + if str(resp.status_code) == '400' or str(resp.status_code) == '401' or str(resp.status_code) == '404': + print 'HTTP error. Message was: %s' % msg + elif str(resp.status_code) == '500': + print 'HTTP error. Message was: %s' % msg + print 'token access services may be down, try again later...' + print resp.text + else: + # assign access token and expiration to instance variables + result = resp.json() + self.access_token = "Bearer " + str(result['access_token']) + self.expires_at = datetime.utcnow() + timedelta(seconds=int(result['expires_in'])) + if self.debug > 1: + print "Request Token Acquired" + except requests.RequestException as e: + self.error_msg(e) + def run_warranty_check(self, inline_serials, retry=True): global full_serials full_serials = {} @@ -51,10 +90,29 @@ def run_warranty_check(self, inline_serials, retry=True): inline_serials.append(d42_serial) inline_serials = ','.join(inline_serials) - payload = {'id': inline_serials, 'apikey': self.api_key, 'accept': 'Application/json'} + if self.expires_at is None or self.expires_at is not None and self.expires_at <= datetime.utcnow(): + if self.debug > 1: + print 'attempting to acquire access_token' + + self.get_access_token(self.client_id, self.client_secret) + + if self.access_token is None: + if self.debug > 1: + print 'unable to acquire access_token' + return None + + payload = { + 'servicetags': inline_serials, + 'Method': 'GET', + } + + headers = { + 'Accept': 'Application/json', + 'Authorization': self.access_token + } try: - resp = requests.get(self.url, params=payload, verify=True, timeout=timeout) + resp = requests.get(self.url, params=payload, headers=headers, verify=True, timeout=timeout) msg = 'Status code: %s' % str(resp.status_code) if str(resp.status_code) == '401' or str(resp.status_code) == '404': print '\t[!] HTTP error. Message was: %s' % msg @@ -68,6 +126,8 @@ def run_warranty_check(self, inline_serials, retry=True): return None else: result = resp.json() + print json.dumps(result) + print return result except requests.RequestException as e: self.error_msg(e) @@ -77,114 +137,101 @@ def process_result(self, result, purchases): global full_serials data = {} - if 'AssetWarrantyResponse' in result: - for item in result['AssetWarrantyResponse']: - try: - warranties = item['AssetEntitlementData'] - asset = item['AssetHeaderData'] - product = item['ProductHeaderData'] - except IndexError: - if self.debug: - try: - msg = str(result['InvalidFormatAssets']['BadAssets']) - if msg: - print '\t\t[-] Error: Bad asset: %s' % msg - except Exception as e: - print e + for item in result: + try: + warranties = item['entitlements'] + except IndexError: + if self.debug: + try: + msg = str(result['InvalidFormatAssets']['BadAssets']) + if msg: + print '\t\t[-] Error: Bad asset: %s' % msg + except Exception as e: + print e + else: + # saw this order number code, did not see this in the response with test devices, + # but leaving it here in case there is a product that does return this information + # if self.order_no == 'vendor': + # order_no = item['orderNumber'] + if self.order_no == 'common': + order_no = self.common else: - if self.order_no == 'vendor': - order_no = asset['OrderNumber'] - elif self.order_no == 'common': - order_no = self.common - else: - order_no = self.generate_random_order_no() - - serial = asset['ServiceTag'] - customernumber = asset['CustomerNumber'] - country = asset['CountryLookupCode'] - - ''' - For future implementation of registering the purchase date as a lifecycle event - Add a lifecycle event for the system - data.update({'date':ship_date}) - data.update({'type':'Purchased'}) - data.update({'serial_no':serial}) - d42.upload_lifecycle(data) - data.clear() - ''' + order_no = self.generate_random_order_no() - # We need check per warranty service item - for sub_item in warranties: + serial = item['serviceTag'] + + # We need check per warranty service item + for sub_item in warranties: + data.clear() + ship_date = item['shipDate'].split('T')[0] + try: + product_id = item['ProductId'] + except: + product_id = 'notspecified' + + data.update({'order_no': order_no}) + if ship_date: + data.update({'po_date': ship_date}) + data.update({'completed': 'yes'}) + + data.update({'vendor': 'Dell Inc.'}) + data.update({'line_device_serial_nos': full_serials[serial]}) + data.update({'line_type': 'contract'}) + data.update({'line_item_type': 'device'}) + data.update({'line_completed': 'yes'}) + + line_contract_id = sub_item['itemNumber'] + data.update({'line_notes': line_contract_id}) + data.update({'line_contract_id': line_contract_id}) + + # Using notes as well as the Device42 API doesn't give back the line_contract_id, + # so notes is now used for identification + # Mention this to device42 + + service_level_group = sub_item['serviceLevelGroup'] + if service_level_group == -1 or service_level_group == 5 or service_level_group == 8 or service_level_group == 99999: + contract_type = 'Warranty' + elif service_level_group == 8 and 'compellent' in product_id: + contract_type = 'Service' + elif service_level_group == 11 and 'compellent' in product_id: + contract_type = 'Warranty' + else: + contract_type = 'Service' + data.update({'line_contract_type': contract_type}) + if contract_type == 'Service': + # Skipping the services, only want the warranties + continue + + try: + # There's a max 64 character limit on the line service type field in Device42 (version 13.1.0) + service_level_description = left(sub_item['serviceLevelDescription'], 64) + data.update({'line_service_type': service_level_description}) + except: + pass + + start_date = sub_item['startDate'].split('T')[0] + end_date = sub_item['endDate'].split('T')[0] + + data.update({'line_start_date': start_date}) + data.update({'line_end_date': end_date}) + + # update or duplicate? Compare warranty dates by serial, contract_id, start date and end date + hasher = serial + line_contract_id + start_date + end_date + try: + d_purchase_id, d_order_no, d_line_no, d_contractid, d_start, d_end, forcedupdate = purchases[hasher] + + if forcedupdate: + data['purchase_id'] = d_purchase_id + data.pop('order_no') + raise KeyError + + # check for duplicate state + if d_contractid == line_contract_id and d_start == start_date and d_end == end_date: + print '\t[!] Duplicate found. Purchase ' \ + 'for SKU "%s" and "%s" with end date "%s" ' \ + 'order_id: %s and line_no: %s' % (serial, line_contract_id, end_date, d_purchase_id, d_line_no) + + except KeyError: + self.d42_rest.upload_data(data) data.clear() - ship_date = asset['ShipDate'].split('T')[0] - try: - product_id = product['ProductId'] - except: - product_id = 'notspecified' - - data.update({'order_no': order_no}) - if ship_date: - data.update({'po_date': ship_date}) - data.update({'completed': 'yes'}) - - data.update({'vendor': 'Dell Inc.'}) - data.update({'line_device_serial_nos': full_serials[serial]}) - data.update({'line_type': 'contract'}) - data.update({'line_item_type': 'device'}) - data.update({'line_completed': 'yes'}) - - line_contract_id = sub_item['ItemNumber'] - data.update({'line_notes': line_contract_id}) - data.update({'line_contract_id': line_contract_id}) - - # Using notes as well as the Device42 API doesn't give back the line_contract_id, - # so notes is now used for identification - # Mention this to device42 - - service_level_group = sub_item['ServiceLevelGroup'] - if service_level_group == -1 or service_level_group == 5 or service_level_group == 8 or service_level_group == 99999: - contract_type = 'Warranty' - elif service_level_group == 8 and 'compellent' in product_id: - contract_type = 'Service' - elif service_level_group == 11 and 'compellent' in product_id: - contract_type = 'Warranty' - else: - contract_type = 'Service' - data.update({'line_contract_type': contract_type}) - if contract_type == 'Service': - # Skipping the services, only want the warranties - continue - - try: - # There's a max 64 character limit on the line service type field in Device42 (version 13.1.0) - service_level_description = left(sub_item['ServiceLevelDescription'], 64) - data.update({'line_service_type': service_level_description}) - except: - pass - - start_date = sub_item['StartDate'].split('T')[0] - end_date = sub_item['EndDate'].split('T')[0] - - data.update({'line_start_date': start_date}) - data.update({'line_end_date': end_date}) - - # update or duplicate? Compare warranty dates by serial, contract_id, start date and end date - hasher = serial + line_contract_id + start_date + end_date - try: - d_purchase_id, d_order_no, d_line_no, d_contractid, d_start, d_end, forcedupdate = purchases[hasher] - - if forcedupdate: - data['purchase_id'] = d_purchase_id - data.pop('order_no') - raise KeyError - - # check for duplicate state - if d_contractid == line_contract_id and d_start == start_date and d_end == end_date: - print '\t[!] Duplicate found. Purchase ' \ - 'for SKU "%s" and "%s" with end date "%s" ' \ - 'order_id: %s and line_no: %s' % (serial, line_contract_id, end_date, d_purchase_id, d_line_no) - - except KeyError: - self.d42_rest.upload_data(data) - data.clear() diff --git a/starter.py b/starter.py index d8450dd..23ac23c 100644 --- a/starter.py +++ b/starter.py @@ -32,7 +32,8 @@ def get_vendor_api(name): if vendor == 'dell': dell_params = { 'url': current_cfg['url'], - 'api_key': current_cfg['api_key'], + 'client_id': current_cfg['client_id'], + 'client_secret': current_cfg['client_secret'], 'd42_rest': d42_rest } api = Dell(dell_params) From f9cd82578d5974ec40a0880c4385ac4723a2cc97 Mon Sep 17 00:00:00 2001 From: Omar Sanchez Date: Thu, 10 Oct 2019 13:39:27 -0400 Subject: [PATCH 2/3] updated readme for version 5 of dell API, added an updates section since it may not be clear what is being done on the script --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index eeede5c..93ca1a6 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This script checks warranty status for Dell, HP, IBM, Lenovo and Meraki manufact In order for this script to check warranty status of the device, the device must have hardware model and serial number entered in Device42. Dell Warranty Status API key must be acquired as well. - Device42 Hardware model must have "Dell", "Hewlett Packard", "IBM", "LENOVO" or "Meraki" in it's manufacturer data. - Device42 Serial number must be set to "Dell", "Hewlett Packard", "IBM", "LENOVO" or "Meraki" device serial number. -- Dell's API key can be obtained by filling the on-boarding form. New and existing API users will need to register an account with TechDirect. Please check: http://en.community.dell.com/dell-groups/supportapisgroup/ +- Dell's client id and client secret can be obtained by filling the on-boarding form. New and existing API users will need to register an account with TechDirect. Please check: http://en.community.dell.com/dell-groups/supportapisgroup/ - HP's API key can be obtained by filling the on-boarding form. Please, follow the instructions from here: https://developers.hp.com/css-enroll - Merakis API key can be obtained by going to the organization > settings page on the Meraki dashboard. Ensure that the enable access to API checkbox is selected then go to your profile to generate the API key. Please check https://developer.cisco.com/meraki/api/#/rest/getting-started/what-can-the-api-be-used-for ## Plans @@ -42,4 +42,7 @@ In order for this script to check warranty status of the device, the device must ## Compatibility * requests module required * Script runs on Linux and Windows -* Python 2.7 \ No newline at end of file +* Python 2.7 + +## Updates +10/10/19 - Updated Dell warranty sync to use version 5 of their API (OAuth2.0), Version 4 EOL is scheduled for 12/15/19, Please update before this date \ No newline at end of file From 815c40e817a709046a45c60ca795811238e04393 Mon Sep 17 00:00:00 2001 From: Omar Sanchez Date: Thu, 10 Oct 2019 14:20:44 -0400 Subject: [PATCH 3/3] better defined exceptions, removed unneded pring statement, explained try-except-else statement on github --- Files/warranty_dell.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Files/warranty_dell.py b/Files/warranty_dell.py index edcb01e..d6d268a 100644 --- a/Files/warranty_dell.py +++ b/Files/warranty_dell.py @@ -126,8 +126,6 @@ def run_warranty_check(self, inline_serials, retry=True): return None else: result = resp.json() - print json.dumps(result) - print return result except requests.RequestException as e: self.error_msg(e) @@ -150,10 +148,6 @@ def process_result(self, result, purchases): print e else: - # saw this order number code, did not see this in the response with test devices, - # but leaving it here in case there is a product that does return this information - # if self.order_no == 'vendor': - # order_no = item['orderNumber'] if self.order_no == 'common': order_no = self.common else: @@ -167,7 +161,7 @@ def process_result(self, result, purchases): ship_date = item['shipDate'].split('T')[0] try: product_id = item['ProductId'] - except: + except KeyError: product_id = 'notspecified' data.update({'order_no': order_no}) @@ -207,7 +201,7 @@ def process_result(self, result, purchases): # There's a max 64 character limit on the line service type field in Device42 (version 13.1.0) service_level_description = left(sub_item['serviceLevelDescription'], 64) data.update({'line_service_type': service_level_description}) - except: + except KeyError: pass start_date = sub_item['startDate'].split('T')[0]