= Python Library API

:!example-caption:

RacksDB provides a Python library API that can be used to fully integrate the
database with your tools and software stack. This document explains the overall
logic of this library with some examples.

== Module

RacksDB provides one main module `racksdb` with essentially one class `RacksDB`.
This should be all you need to import to interact with RacksDB.

[source,python]
----
>>> from racksdb import RacksDB
>>> RacksDB
<class 'racksdb.RacksDB'>
----

[#load]
== Load database

The `RacksDB` class has one static method `load()` to load the database:

[source,python]
----
>>> from racksdb import RacksDB
>>> db = RacksDB.load()
----

This method returns an instanciated `RacksDB` object:

[source,python]
----
>>> type(db)
<class 'racksdb.RacksDB'>
----

This object can then be used to interact with database content.

Without more arguments, the `load()` method loads the
xref:db:files.adoc[database], xref:db:schema.adoc[schema] and
xref:db:ext.adoc[extensions] at their defaults paths. Alternative paths can be
specified in arguments:

[source,python]
----
>>> from racksdb import RacksDB
>>> db = RacksDB.load(
...    schema='/usr/share/racksdb/schemas/racksdb.yml',
...    ext='/etc/racksdb/extensions.yml',
...    db='/var/lib/racksdb',
... )
----

All arguments are optional and can be specified individually. The method accepts
both strings and standards
https://docs.python.org/3/library/pathlib.html[Paths objects] as arguments.

The default paths are defined as class attributes:

[source,python]
----
>>> RacksDB.DEFAULT_DB
'/var/lib/racksdb'
>>> RacksDB.DEFAULT_EXT
'/etc/racksdb/extensions.yml'
>>> RacksDB.DEFAULT_SCHEMA
'/usr/share/racksdb/schemas/racksdb.yml'
----

Two exceptions can be raised by the `load()` method:

* `racksdb.generic.errors.DBSchemaError` exception in case of error with the
  schema.
* `racksdb.generic.errors.DBFormatError` exception when an error is encountered
  with the database content.

The exceptions are raised with a detailed description of the error.

[#model]
== Generic Object Model

The database is essentially composed of 3 types of Python objects, detailled in
the following subsections.

=== `DBObject`

RacksDB database structure is composed of
xref:db:structure.adoc#objects[many objects]. Each type of object is represented
with its own specific class, inherited from the abstract `DBObject` class.

.Example
====
The xref:db:structure.adoc#obj-types[Types object] is represented with the
`RacksDBTypes` class which inherits from the `DBObject` class:

[source,python]
----
>>> type(db.types)
<class 'racksdb.generic.db.RacksDBTypes'>
>>> from racksdb.generic.db import DBObject
>>> isinstance(db.types, DBObject)
True
----
====

Every xref:db:concepts.adoc#properties[*objects properties*] (including
xref:db:concepts.adoc#backref[*automatic back references*]) defined in
xref:db:structure.adoc#objects[database structure] can be directly accessed
through the corresponding attribute.

.Example
====
The `RacksDBTypes` object has attributes `nodes`, `network`, `storage`, `misc`
and `racks` corresponding to the properties of
xref:db:structure.adoc#obj-types[Types object]:

[source,python]
----
>>> for prop in ['nodes', 'network', 'storage', 'misc', 'racks']:
...   hasattr(db.types, prop)
...
True
True
True
True
True
----
====

The type of the attributes depends on the
xref:db:concepts.adoc#attributes[attributes] and the
xref:db:concepts.adoc#types[data type] of the corresponding property:

* For xref:db:concepts.adoc#sequence[*sequences*], the attribute is either a
  xref:#list[`DBList`] or a xref:#dict[`DBDict`] object, depending on the
  presence of a key property. Please refer to the following dedicated sections
  for more details.

* For xref:db:concepts.adoc#native[*native types*], the attribute has the
  corresponding Python native type (_ex:_ str or float).
+
--
.Examples
====
The type of `model` property of xref:db:structure.adoc#obj-nodetype[NodeType
object] is a string, this is converted to a native Python _str_:

[source,python]
----
>>> type(db.types.nodes.first().model)
<class 'str'>
----

The type of `height` property of xref:db:structure.adoc#obj-racktype[RackType
object] is an integer, this is converted to a native Python _int_:
[source,python]
----
>>> type(db.types.racks.first().height)
<class 'int'>
----
====
--

* For xref:db:concepts.adoc#reference[*references*] and
  xref:db:concepts.adoc#backref[*back references*], the attribute is a reference
  to the corresponding object in the database.
+
--
.Examples
====
The type of `type` property of xref:db:structure.adoc#obj-node[Node object] is a
reference to xref:db:structure.adoc#obj-nodetype[NodeType object], the attribute
is a reference the corresponding `RacksDBNodeType` object in database:

[source,python]
----
>>> type(db.infrastructures.first().layout.first().nodes.first().type)
<class 'racksdb.generic.db.RacksDBNodeType'>
----

There is a back reference named `datacenter` on
xref:db:structure.adoc#obj-datacenterroom[DatacenterRoom object], the attribute
is a reference to the parent `RacksDBDatacenter` object:

[source,python]
----
>>> db.datacenters.first().rooms.first().datacenter
<racksdb.generic.db.RacksDBDatacenter object at 0x7f0aaf13db10>
>>> db.datacenters.first() == db.datacenters.first().rooms.first().datacenter
True
----
====
--

* For xref:db:concepts.adoc#deftype[*defined types*], the attribute has the
 resulting type of the defined type.
+
--
.Examples
====
The `depth` property of
xref:db:structure.adoc#obj-datacenterroomdimensions[DatacenterRoomDimensions
object] is a xref:db:structure.adoc#deftype-dimension[`~dimension` defined type]
whose resulting type is an integer. The attribute is a Python native _int_:

[source,python]
----
>>> type(db.datacenters.first().rooms.first().dimensions.depth)
<class 'int'>
----

The `width` property of
xref:db:structure.adoc#obj-networkequipmenttype[NetworkEquipmentType
object] is a xref:db:structure.adoc#deftype-rack_width[`~rack_width` defined type]
whose resulting type is a float. The attribute is a Python native _float_:

[source,python]
----
>>> type(db.types.network.first().width)
<class 'float'>
----
====
--

[NOTE]
====
The `RacksDB` object, representing the root of the whole xref:#load[loaded
database] is also a `DBObject`. This can be verified with this:

[source,python]
----
>>> isinstance(db, DBObject)
True
----

For this reaison, it has the `types`, `datacenters` and `infrastructures`
attributes corresponding to the properties of the
xref:db:structure.adoc#obj-root[database structure root].
====

[#list]
=== `DBList`

The `DBList` class extends standard Python `list` type. It is generally used to
represent values of properties with
xref:db:concepts.adoc#sequence[sequence attribute] (except for objects with
xref:db:concepts.adoc#key[key property] represented by xref:#dict[`DBDict`]).

.Example
====
The list of xref:db:structure.adoc#obj-nodetypenetif[`NodeTypeNetif`] objects
holded by the `netifs` property of xref:db:structure.adoc#obj-nodetype[`NodeType`]
objects is represented by a `DBList`:

[source,python]
----
>>> type(db.types.nodes.first().netifs)
<class 'racksdb.generic.db.DBList'>
----
====

Compared to standard Python `list` type, `DBList` class notably adds support of
expandable objects. Typically, iterating over a `DBList` generates all objects
in range of expandable objects.

.Example
====
The list of xref:db:structure.adoc#obj-rack[`Rack`] objects holded by the
`racks` property of xref:db:structure.adoc#obj-racksrow[`RacksRow`] is a
`DBList`. Even if the list actually contains a folded range of racks, iterating
over the `DBList` generates all expanded members of the range:

[source,python]
----
>>> racks = db.datacenters["paris"].rooms["noisy"].rows.first().racks
>>> type(racks)
<class 'racksdb.generic.db.DBList'>
>>> len(racks)
1
>>> racks[0].name
R1-A[01-10]
>>> for rack in racks:
...   print(rack.name)
...
R1-A01
R1-A02
R1-A03
R1-A04
R1-A05
R1-A06
R1-A07
R1-A08
R1-A09
R1-A10
----
====

Also, the `len()` function on a `DBList` returns the number of potentially
expanded objects, not the number of actual members of the list.

.Example
====
Considering the previous example with the `racks` property of
xref:db:structure.adoc#obj-racksrow[`RacksRow`], the length of `DBList` as
reported by `len()` is different of the actual number of values in the list:

[source,python]
----
>>> racks[0].name
R1-A[01-10]
>>> type(racks)
<class 'racksdb.generic.db.DBList'>
>>> purelist = racks.copy()
>>> type(purelist)
<class 'list'>
>>> len(purelist)
1
>>> len(racks)
10
----
====

==== Methods

The `DBList` objects provide 2 methods:

* `filter()` method returns another `DBList` with a subset of all objects
  contained in the list that satisfy the criteria in arguments. This method must
  be supported by the specialized class to work properly or no filtering is
  performed. Please refer to the
  xref:#specializations[Classes Specializations section] to discover the
  classes supporting filtering.
* `itervalues()` method is a generator to iterate over folded values of the
  `DBList` without triggering automatic expansion.
+
--
.Example
====
Considering the previous example with the list of
xref:db:structure.adoc#obj-rack[`Rack`], iterating over the `DBList` with
`itervalues()` method generates only one folded object:

[source,python]
----
>>> racks = db.datacenters["paris"].rooms["noisy"].rows.first().racks
>>> for rack in racks.itervalues():
...   print(rack.name)
...
R1-A[01-10]
----
====
--

[#dict]
=== `DBDict`

The `DBDict` class extends standard Python `dict`. It is generally used to
represent values of properties with
xref:db:concepts.adoc#sequence[sequence attribute] whose contained objects have
xref:db:concepts.adoc#key[key property]. The values of the key properties are
the keys of the `DBDict`.

.Example
====
The list of xref:db:structure.adoc#obj-datacenter[Datacenter] objects holded by
the `datacenters` property of xref:db:structure.adoc#obj-root[database root]
object is represented by a `DBDict`:

[source,python]
----
>>> type(db.datacenters)
<class 'racksdb.generic.db.DBDict'>
----
====

Compared to standard Python dictionnaries, `DBDict` class notably adds support
of expandable objects. Typically, it is possible to use the subscript operator
(ie. `[]`) on any member of a range, even when this member is not a key of the
dictionnary.

.Example
====
The list of xref:db:structure.adoc#obj-node[Node] objects holded by the `nodes`
property of xref:#spec-racksdb[`RacksDB` class specialization] is a `DBDict`.
Even if `mecn0002` is not in its keys, `DBDict` is capable to figure out it is
a member the range `mecn[0001-0040]` and returns an instance of the appropriate
object:

[source,python]
----
>>> type(db.nodes)
<class 'racksdb.generic.db.DBDict'>
>>> db.nodes.keys()
dict_keys([mecn[0001-0040], mecn0200, mecn[0041-0060], mecn[0061-0116], mesrv[0001-0004]])
>>> db.nodes['mecn0002']
<racksdb.generic.db.RacksDBNode object at 0x7fd763b40f7>
>>> db.nodes['mecn0002'].type.id
'sm220bt'
----
====

While standard Python `dict` iterates over the list of keys, `DBDict` class
iterates over the list of potentially expanded values.

.Example
====
Considering the previous example with the `nodes` property of
xref:#spec-racksdb[`RacksDB` class specialization], iterations over this
`DBDict` generate the list of all expanded objects:

[source,python]
----
>>> db.nodes.keys()
dict_keys([mecn[0001-0040], mecn0200, mecn[0041-0060], mecn[0061-0116], mesrv[0001-0004]])
>>> for node in db.nodes:
...   print(node.name)
...
mecn0001
mecn0002
mecn0003
mecn0004
mecn0005
…
----
====

Also, the `len()` function on a `DBDict` returns the number of potentially
expanded objects, not the number of actual members of the dictionnary.

.Example
====
Considering the previous example with the `nodes` property of
xref:#spec-racksdb[`RacksDB` class specialization], the number of `DBDict`
members as reported by `len()` is different of the number of keys:

[source,python]
----
>>> len(db.nodes.keys())
5
>>> len(db.nodes)
121
----
====

==== Methods

The `DBDict` objects provide 2 methods:

* `filter()` method returns another `DBDict` with a subset of all objects
  contained in the dictionnary that satisfy the criteria in arguments. This
  method must be supported by the specialized class to work properly or no
  filtering is performed. Please refer to the
  xref:#specializations[Classes Specializations section] to discover the classes
  supporting filtering.
* `first()` method returns the first (potentially expanded) object contained in
  the `DBDict` object.
+
--
.Example
====
Get the first node type:

[source,python]
----
>>> type(db.types.nodes.first())
<class 'racksdb.generic.db.RacksDBNodeType'>
>>> db.types.nodes.first().id
'sm220bt'
----
====
--

[#specializations]
== Classes Specializations

Some specialized `DBObject` subclasses provide additional methods and
attributes, either for conveniency or to provide additional features. These
specializations are documented for each class.

[#spec-racksdb]
=== `RacksDB`

==== Attributes

The `RacksDB` class provides the following specialized attribute:

* `nodes`: the xref:#dict[`DBDict` object] containing all nodes of all
  infrastructures defined in RacksDB database.
+
--
.Example
====
Count the total number of nodes in the database:

[source,python]
----
>>> len(db.nodes)
196
----
====
--
* `racks`: the xref:#list[`DBList` object] containing all racks of all
  datacenter rooms defined in RacksDB database.
+
--
.Examples
====
Count the total number of racks in the database:

[source,python]
----
>>> len(db.racks)
53
----

Get the name of the first rack:

[source,python]
----
>>> db.racks[0].name
'R1-A01'
----
====
--

==== Methods

The `RacksDB` objects provide 1 method:

* `tags()` method returns a list of tags associated to objects. This method
  accepts 5 arguments:
+
--
* `node` (default: _None_): An optional name of node . When not _None_, return
   the list of tags associated to this node.
* `infrastructure` (default: _None_): An optional name of infrastructure. When
   not _None_, return the list of tags associated to this infrastructure. If
   `on_nodes` argument is _True_, returns all tags associated to nodes in this
   infrastructure instead.
* `datacenter` (default: _None_): An optional name of datacenter. When not
  _None_, return the list of tags associated to this datacenter. If `on_racks`
  argument is _True_, returns all tags associated to racks in this datacenter
  instead.
* `on_nodes` (default: _False_). When _True_ with an infrastructure name,
  return tags on nodes of this infrastructure.
* `on_racks` (default: _False_). When _True_ with a datacenter name, return
  tags on racks of this datacenter.

The method raises `RacksDBRequestError` when called with _None_ value for all
three `node`, `infrastructure` and `datacenter` arguments. It raises
`RacksDBNotFoundError` if either the node, infrastructure or datacenter cannot
be found with the provided name.
--

[#spec-racksdbdatacenter]
=== `RacksDBDatacenter`

==== Attributes

The `RacksDBDatacenter` class provides the following specialized attribute:

* `racks`: xref:#list[`DBList` object] of all racks in the datacenter.
+
--
.Example
====
Get the name of the first rack in _paris_ datacenter:

[source,python]
----
>>> db.datacenters['paris'].racks[0].name
'R1-A01'
----
====
--
* `racks_tags`: xref:#list[`DBList` object] of all tags associated to racks in
  the datacenter.
+
--
.Example
====
Get tags associated to racks in _paris_ datacenter:

[source,python]
----
>>> db.datacenters['paris'].racks_tags
['first', 'last']
----
====
--

==== Filtering

The `RacksDBDatacenter` class provides an implementation of the `_filter()`
method, for easy filtering of xref:#list[`DBList`] and xref:#dict[`DBDict`]
containing `RacksDBDatacenter` objects. It accepts the following arguments:

* `name`: the name of an infrastructure
* `tags`: a list of tags

A datacenter is selected only if it matches all criteria. If multiple tags
are provided, only the datacenters for which all the tags are applied are
selected.

.Examples
====
Get the datacenters named _paris_:

[source,python]
----
>>> db.datacenters.filter(name='paris')
----

Get all datacenters with tag _tier2_:

[source,python]
----
>>> db.datacenters.filter(tags=['tier2'])
----
====

[#spec-racksdbinfrastructure]
=== `RacksDBInfrastructure`

==== Attributes

The `RacksDBInfrastructure` class provides the following specialized attribute:

* `nodes`: the xref:#dict[`DBDict` object] containing all nodes of all layout
  parts of the infrastructure.
+
--
.Example
====
Count the total number of nodes in the _tiger_ infrastructure:

[source,python]
----
>>> len(db.infrastructures['tiger'].nodes)
75
----
====
--
* `nodes_tags`: xref:#list[`DBList` object] of all tags associated to nodes in
  the infrastructure.
+
--
.Example
====
Get tags associated to nodes in the _mercury_ infrastructure:

[source,python]
----
>>> db.infrastructures['mercury'].nodes_tags
['compute', 'servers', 'ia', 'gpu']
----
====
--

==== Filtering

The `RacksDBInfrastructure` class provides an implementation of the `_filter()`
method, for easy filtering of xref:#list[`DBList`] and xref:#dict[`DBDict`]
containing `RacksDBInfrastructure` objects. It accepts the following arguments:

* `name`: the name of an infrastructure
* `tags`: a list of tags

An infrastructure is selected only if it matches all criteria. If multiple tags
are provided, only the infrastructures for which all the tags are applied are
selected.

.Examples
====
Get the infrastructure named _mercury_:

[source,python]
----
>>> db.infrastructures.filter(name='mercury')
----

Get all infrastructures with tag _cluster_:

[source,python]
----
>>> db.infrastructures.filter(tags=['cluster'])
----
====

[#spec-racksdbnode]
=== `RacksDBNode`

[#spec-racksdbstorageequipment]
=== `RacksDBStorageEquipment`

[#spec-racksdbnetworkequipment]
=== `RacksDBNetworkEquipment`

[#spec-racksdbmiscequipment]
=== `RacksDBMiscEquipment`

==== Filtering

The `RacksDBNode`, `RacksDBStorageEquipment`, `RacksDBNetworkEquipment` and
`RacksDBMiscEquipment` classes provide an implementation of the `_filter()`
method, for easy filtering of xref:#list[`DBList`] and xref:#dict[`DBDict`]
containing these objects. It accepts the following arguments:

* `infrastructure`: the name of an infrastructure
* `name`: the name of a node
* `tags`: a list of tags

The equipment is selected only if it matches all criteria. If multiple tags are
provided, only the equipment for which all the tags are applied are selected.

.Examples
====
Get all nodes named _cn001_:

[source,python]
----
>>> db.nodes.filter(name='cn001')
----

Get all nodes of infrastructure _tiger_ with tag _compute_:

[source,python]
----
>>> db.nodes.filter(infrastructure='tiger', tags=['compute'])
----
====

[#spec-racksdbrack]
=== `RacksDBRack`

==== Filtering

The `RacksDBRack` class provides an implementation of the `_filter()` method,
for easy filtering of xref:#list[`DBList`] and xref:#dict[`DBDict`] containing
`RacksDBRack` objects. It accepts the following arguments:

* `name`: the name of a rack

A rack is selected only if it matches the name criteria.

.Example
====
Get rack named _R1-A02_:

[source,python]
----
>>> db.racks.filter(name='R1-A02')
----
====

== Database content

By combining the explanations about the xref:#model[Generic Object model], the
xref:db:structure.adoc[database structure] and the available
xref:#specializations[classes specializations], you get all theoretical
information required to explore database with RacksDB Python library. This
section provide some practical examples to illustrate the principles.

=== Types

The equipments types are available through the `types` attribute of `RacksDB`
object:

[source,python]
----
>>> db.types
<racksdb.generic.db.RacksDBTypes object at 0x7f5660345810>
----

.Examples
====
Print the height in meters of all types of racks:

[source,python]
----
>>> for rack in db.types.racks:
...   print(f"{rack.id}: {rack.height/10**3}m")
...
standard: 1.867m
half: 1.198m
----

Print the list of node types ID and models:

[source,python]
----
>>> for nodetype in db.types.nodes:
...   print(f"{nodetype.id}: {nodetype.model}")
...
sm220bt: SuperMicro A+ Server 2124BT-HTR
sm610u: SuperMicro Ultra SYS-610U-TNR
hpesyn480: HPE Synergy 480 Gen10 Compute Module
dellr550: Dell PowerEdge R550
----

Print the number of network interfaces, with their bandwidth in Gb/s, for each
network equipment type:

[source,python]
----
>>> for equipment in db.types.network:
...   print(f"{equipment.model} :")
...   for netif in equipment.netifs:
...     print(f" [{netif.type.upper()}] {netif.number}x{netif.bandwidth*8/10**9}Gb/s")
...
Cisco Catalyst 3650 switch :
 [ETHERNET] 48x1.0Gb/s
----

Print the raw capacity in TB of each storage equipment type:

[source,python]
----
>>> size = 0
>>> for equipment in db.types.storage:
...   for disk in equipment.disks:
...     size += disk.size * disk.number
...   print(f"{equipment.model}: {size/1024**4}TB")
...
QNAP TS-H1277XU-RP: 48.0TB
----
====

=== Datacenters

Datacenters are available through the `datacenters` attribute of `RacksDB`
object:

[source,python]
----
>>> db.datacenters
<racksdb.generic.db.DBList object at 0x7f5660344c10>
----

.Examples
====
Get the name of the first datacenter:

[source,python]
----
>>> db.datacenters.first().name
'paris'
----

Get the list of all datacenters rooms:

[source,python]
----
>>> [room.name for datacenter in db.datacenters for room in datacenter.rooms]
['noisy']
----

Print the list of racks by room per datacenter:

[source,python]
----
>>> for datacenter in db.datacenters:
...   for room in datacenter.rooms:
...       print(f"{datacenter.name}: {room.name}: "
...             f"racks: {[rack.name for row in room.rows for rack in row.racks]}")
...
paris: noisy: racks: ['R1-A01', 'R1-A02', 'R1-A03', … 'R7-A04', 'R7-A05', 'R7-A06']
----
====

=== Infrastructures

Infrastructures are available through the `infrastructures` attribute of
`RacksDB` object:

[source,python]
----
>>> db.infrastructures
<racksdb.generic.db.DBList object at 0x7f5660346890>
----

.Examples
====
Get the list of all infrastructure names:

[source,python]
----
>>> [infrastructure.name for infrastructure in db.infrastructures]
['tiger', 'mercury']
----

Print the list of nodes of the _tiger_ infrastructure:

[source,python]
----
>>> for node in db.infrastructures['tiger'].nodes:
...     print(node.name)
...
cn001
cn002
cn003
…
cn226
cn227
cn228
----

Print the list of racks and datacenter where the _tiger_ infrastructure is
located:

[source,python]
----
>>> for part in db.infrastructures['tiger'].layout:
...     print(f"{part.rack.name} ({part.rack.datacenter.name})")
...
R01 (paris)
R02 (paris)
----

The list of tags applied to node _srv001_ of infrastructure _tiger_:

[source,python]
----
>>> db.infrastructures['tiger'].nodes['srv001'].tags
['compute', 'servers']
----

The name of nodes in infrastructure _tiger_ with tag _servers_:

[source,python]
----
>>> for node in db.infrastructures['tiger'].nodes.filter(tags=['servers']):
...   node.name
...
'srv001'
'srv002'
----

The set of racks where are located all nodes with tag _compute_:

[source,python]
----
>>> set([node.rack.name for node in db.nodes.filter(tags=['compute'])])
{'R02', 'R1-A01', 'R01', 'R1-A02'}
----

The model names of nodes with tag _servers_:

[source,python]
----
>>> set([node.type.model for node in db.nodes.filter(tags=['servers'])])
{'Dell PowerEdge R550'}
----
====
