Overview
The OTbase Python SDK consists of the following classes:
- OtDevice for manipulating individual device data
- OtDeviceSet for manipulating data from multiple devices
- OtNetworks for reading network data from OTbase Inventory
- OtNetwork for manipulating individual network data.
Most use cases for the OTbase Python SDK will deal with multiple OT devices, and hence will use the OtDeviceSet class. For the few use cases where individual OT devices need to be manipulated, the OtDevice class can be used.
The network related classes come handy when you are dealing with a large number of networks and want to change metadata.
Understanding OT device sets
The OtDeviceSet class represents OT devices as sets. A set contains asset data of one or more devices. This data is loaded from a Portable Inventory Data file or via the REST API in OTbase Inventory.
A device set can be manipulated using various methods. As an example, you can add or remove tags. You can inspect and overwrite attributes of the devices in the set, such as the description, the device name, or device criticality.
Device sets can be partitioned using the select method, which returns a subset of a set where devices share certain characteristics that are specified in the method's arguments.
Instantiating an OtDeviceSet object
After importing the ot_base module, you can instantiate an object of class OtDeviceSet. Example:
from ot_base import OtDeviceSet
otdevices = OtDeviceSet()
Initializing an OtDeviceSet object
OtDeviceSet objects can be initialized by loading a Portable Inventory Data file, by pulling asset data from OTbase Inventory using the REST API, by loading data from an Excel spreadsheet, by passing a device list that was generated using the devices method, or by adding other OtDeviceSet objects using the "+" operator.
In order to initialize an object by loading a Portable Inventory Data file, the loadFile method is used:
otdevices.loadFile('OTbase Devices.json')
In order to initialize an object by pulling data online from OTbase Inventory, the loadREST method is used:
otdevices.loadREST('192.168.0.15', (userid, password))
In order to initialize an object by loading data from an Excel spreadsheet, the loadExcel method is used. When calling loadExcel, you must specify which column headers to include, and how these column headers map to supported field names. Example:
otdevices.loadExcel('myfile.xslx', {'ID': 'deviceId', 'description': 'description', 'vendor': 'hardware_vendor'})
Creating subsets
The OTbase Python SDK is mostly designed for bulk processing of asset data, hence it uses sets rather than looping through a device list. This implies that you must make sure that your set only contains devices for which a certain operation, such as changing asset attributes, is intended. If the constructor method left you with more devices in your set than you need, the way to proceed is to select a subset from the original set using the select method. select allows you to pull those assets that match certain criteria, such as location, hardware category, or operating system.
With select, you specify one or more asset characteristic that you want to use as a filter criterion. The result set will only contain devices that match those characteristics. You can also specify if multiple characteristics shall be combined using logical AND, or logical OR. Finally, you can decide if you want the matching to be precise (e.g. including case sensitivity) or loose.
In the following example we select devices from our original set where the hardware vendor is Rockwell and the device type is PLC:
rockwell_plcs = otdevices.select({'hardware_vendor': 'Rockwell', 'hardware_type': 'PLC'})
If we don't add match modifiers, matching will be loose, and multiple arguments will be combined with logical AND. If we modify our select statement to use logical OR:
rockwell_plcs = otdevices.select({'hardware_vendor': 'Rockwell', 'hardware_type': 'PLC'}, op = 'OR')
the result set will contain any device manufactured by Rockwell, and also PLCs from manufacturers other than Rockwell (assuming that some of those are in our initial set).
Finally, if we do an exact match:
rockwell_plcs = otdevices.select({'hardware_vendor': 'Rockwell', 'hardware_type': 'PLC'}, match = 'EXACT')
our result set might not contain much, because many Rockwell devices use the vendor name "Rockwell Automation/Allen-Bradley", which will not return a result with this select.
Set arithmetic
You can do set arithmetics on device sets. So for example, if your initial set contains PLCs and RTUs, the following statements will remove the PLCs from the initial set:
plcs = otdevices.select({'hardware_type': 'PLC'})
otdevices -= plcs
A set will never contain duplicate entries for a device. Even if you add two sets that contain identical devices, the result set will contain no duplicate entries. Example:
plcs += plcs
The result set will contain an identical number of devices as the initial set.
Set arithmetic is particularly useful when you want to arrive at a subset that does NOT show a certain characteristic. For example, if we want to isolate Windows machines in a set that don't have security patch KB4499180 installed, we can first build a subset of devices where this patch is present, and then substract it from our original set, resulting in the desired subset:
kb4499180_installed = all_windows_pcs.select({'software_patch_name': 'KB4499180'}, match = 'EXACT')
kb4499180_not_installed = all_windows_pcs - kb4499180_installed
Specification of new builds: Cloning device sets
The OTbase Python SDK helps you in the process of specifying configurations for new machines, Distributed Control Systems, etc. You can clone existing devices and device sets in order to arrive at a high fidelity specification for a new build with ease.
The clone method will clone an existing device set and assign new device IDs in the process. It will also clear asset data reflecting the original system, such as IP addresses, serial numbers, and so forth. Last but not least, it will set the device lifecycle of all devices to 'Planned'.
A special feature of the clone method is that you can specify a multiplication factor. Imagine, for example, that you want to specify a new build with 30 HMI stations. An existing HMI station, let's imagine using the device ID 'xyz.Desktop44', shall act as the functional model for all stations. Here is how this can be accomplished:
hmi_model = set1.select({'deviceId': 'xyz.Desktop44'}, mode = 'EXACT')
my_new_system += hmi_model.clone(prefix = 'newSysName', mul = 30)
Note: Besides single devices, you can also clone subsystems. The clone()
method does not care how many devices are in your source set, and what device type they are. So just as an example, you can clone a whole manufacturing line with hundreds of devices.
Getting content of your device sets
After selecting, adding, substracting and cloning, you will want to check if the content of your result set is what you intended, and maybe process the data. There are several ways to do this:
- len(otdevices) will return the number of devices in the set
- get() returns specific, singular asset characteristics as a dictionary, set, or list
- devices() returns full asset data as a list
- show() displays asset data as a table.
get() is called with the name of the attribute that you are interested in -- for example, device ID, operating system name, device type, criticality. By default it returns a dictionary with attribute values and their frequencies, as in the following example:
If you primarily want to check that set content is what you expect, the show method is recommended. show() displays asset data in a table format, and you can specify the fields that shall be included in the output. (Note that you don't have to specify columns; show will simply use a good default subset of columns that will give you an idea of a set's content.)
Modifying asset attributes
The set method can be used to modify asset attributes. It will set an atomic variable to the value that is passed as an argument. You can use set to set() one or multiple attributes per call. In the following example, we set all devices in the set to the stage 'Operational':
For manipulating tags, additional methods are provided because an asset can have multiple tags assigned. addTags() will add a new tag to the existing tags, and removeTags() will remove a specific tag from the tags for the devices in the set.
Let's assume that we want to tag all assets which are still running XP with the tags "Urgent OS update required" and "INSECURE". This could be achieved with the following statements, where the last get() is only performed to verify that our new tags have been set:
Writing data to OTbase Inventory
Note: Modifications that you execute on asset data are local to your device set. If you want to write your modified data back to OTbase Asset Center, the following methods can be used:
- create a Portable Inventory Data file with toFile(), and then import this file manually in OTbase Inventory
- using an online connection to OTbase Inventory and the REST API with toAssetCenter.
There are pros and cons for both methods. Saving the result set to a file gives you another opportunity to check all your modifications before importing into OTbase Inventory, using a JSON-capable editor. In the following example, we save our device set for which we have added tags to file:
When loaded into an editor, we see that our tags have been attached:
After importing the JSON file into OTbase Inventory, we can also confirm success by checking the changes as documented in the device log and going to the device profiles of the imported devices.
Dealing with single OT devices: The OtDevice class
Whenever you need to manipulate or analyze the data of a particular OT device, the OtDevice class comes in handy. It relieves you from the need to process raw Portable Inventory Data.
Instantiating an OtDevice object
After importing the ot_base module, you can instantiate an object of class OtDevice. Example:
from ot_base import OtDevice
device = OtDevice()
Another way to create an OtDevice object is by breaking an OtDeviceSet object apart into a list of individual OtDevice objects. Think of it similar to un-grouping a complex graphics object in a Microsoft Office product, or in a graphics editor.
Example:
from ot_base import OtDevice, OtDeviceSet
set1 = OtDeviceSet()
set1.loadREST('192.168.5.20', auth=(user,password), otsystem='Ovation')
deviceList = set1.devices() # Result type is list of OtDevice objects
for d in deviceList:
print(d.Id)
Initializing OtDevice objects
There are two ways how to initialize an OtDevice object:
- Loading a device's asset information from OTbase Inventory via the REST API
- Creating device objects by breaking up an OtDeviceSet set, as explained above.
Note that you cannot directly initialize an OtDevice object from a Portable Inventory Data file.
In order to initialize an object by pulling data online from OTbase Inventory, the loadREST method is used:
device.loadREST(myDeviceId, '192.168.0.15', (userid, password))
Inspecting device properties
Most device characteristics can be inspected by querying various properties
# Device ID
print(device.Id)
# Device name
print(device.name)
# Device description
print(device.description)
# Device tags
print(device.tags)
# List of IP addresses used by the device
print(device.ipList)
# List of networks associated with the device
print(device.networks)
# Operating system of the device
print(device.os)
# Serial number of the device
print(device.serialNumber)
# Location of the device
print(device.location)
# Hardware vendor
print(device.vendor)
# Hardware type
print(device.hwType)
# Hardware model
print(device.hwModel)
Note that besides general hardware and software properties, you can also query vulnerability and patch data for the device:
print(device.nVulns) # number of known vulnerabilities
print(device.nPatches) # number of security patches for the device
Modifying device properties
Device properties that aren't automatically obtained by OTbase Discovery can also be changed. So for example, you can change the description and criticality assessment for a device, but not it's operating system version or known vulnerabilities. In most cases, changing a property is done by setting the property to a new value. Example:
device.description = 'my new description'
device.criticality = 'SAFETY:2'
Using modified device data
Ultimately you will want to write your modified device data back to OTbase Asset Center, or to a Portable Inventory Data file for later processing. This can be done in different ways:
- directly writing device data to OTbase Inventory using the REST API
- adding device data to a data set of class OtDeviceSet that is then written to OTbase Inventory or to a Portable Inventory Data file.
In order to directly write device data to OTbase Inventory, the toAssetCenter() method can be used.
Example:
device.toAssetCenter('192.168.0.15', (userid, password))
Note that toAssetCenter() can check if the device already exists in OTbase Inventory or not. This is useful when specifying new devices during the course of planning a plant extension, where you want to make sure that any new device IDs you have assigned are not used already. In this situation, the argument overwrite = False can be used:
device.toAssetCenter('192.168.0.15', (userid, password), overwrite = False)
In order to add a device to a device set, you can use set arithmetic:
set1 += device
Finally, you can bind a list of devices into a new device set by passing the list as an argument to the constructor of class OtDeviceSet:
deviceList = [device1, device2, device3]
myNewSet = OtDeviceSet(deviceList)
This set can then be written to OTbase Inventory in one batch, rather than using individual calls.
Dealing with networks
For manipulating network data, the logic usually follows a code structure like this:
- read all network data from OTbase Inventory using an OtNetworks object
- process network data in a loop using OtNetwork objects for easy access of data items
- write selected network data back to OTbase Inventory using the toAssetCenter() method of OtNetwork.
The network data loaded from OTbase Inventory by an OtNetworks object can be accessed in the PID variable. PID is a list that contains data for individual networks. List elements can be used to initialize an OtNetwork object that allows for easy extraction and manipulation of the data.
Sample:
networks = OtNetworks()
networks.loadREST(ac_url, (userid, password), prot='https')
for nn in networks.PID: # PID = Portable Inventory Data representation of network data
n = OtNetwork(nn) # initialize the new network object with the PID data
# do some data modifications here
n.toAssetCenter(ac_url, (userid,password), prot = 'https') # write back to OTbase Inventory
If you already know the network ID for a network that you want to manipulate, you don't have to load all networks but can call the loadREST() method of OtNetwork.
Comments
0 comments
Please sign in to leave a comment.