Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

D42 13677 - Dell API v5 Support #27

Merged
merged 3 commits into from
Dec 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Files/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
5 changes: 3 additions & 2 deletions Files/warranty.cfg.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
261 changes: 151 additions & 110 deletions Files/warranty_dell.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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 = {}
Expand All @@ -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
Expand All @@ -77,114 +135,97 @@ 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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This "else" does not seem to be properly lined up with an "if".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a try-except-else clause. was confused about this too but it seems it is an alternative to finally that is run before all finally statements. Previous version was written like this

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 KeyError:
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 KeyError:
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()
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
* 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
3 changes: 2 additions & 1 deletion starter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down