# Transactions

## One object

All the changes done using one object are applied in the order defined by the corresponding object class.

    eth0 = ndb.interfaces["eth0"]
    eth0.add_ip(address="10.0.0.1", prefixlen=24)
    eth0.set(state="up")
    eth0.set(mtu=1400)
    eth0.commit()

In the example above first the interface attributes like state, mtu, ifname etc. will be applied, and only then IP addresses, bridge ports and like that, regardless the order they are referenced before the commit() call.

The order is ok for most of cases. But if not, one can control it by calling commit() in the required places, breaking one transaction into several sequential transactions.

And since RTNL object methods return the object itself, it is possible to write chains with multiple commit():

    (
        ndb.interfaces
        .create(ifname="test", kind="dummy")
        .add_ip(address="10.0.0.1", prefixlen=24)
        .commit()
        .set(state="up")
        .commit()
    )

Here the order is forced by explicit commits.

## Multiple objects

An important functionality of NDB are rollbacks. And there is a way to batch changes on multiple objects so one failure will trigger rollback of all the changes on all the objects.

    ctx = ndb.begin()
    ctx.push(
        # first set up a bridge
        (
            ndb.interfaces
            .create(ifname="br0", kind="bridge")
            .add_port("eth0")
            .add_port("eth1")
            .set(state="up")
            .add_ip("10.0.0.2/24")
        ),
        # and only then create a route
        (
            ndb.routes
            .create(
                dst="192.168.0.0",
                dst_len=24,
                gateway="10.0.0.1"
            )
        )
    )
    ctx.commit()  # if something goes wrong, the whole batch will be reverted

## Ping a remote host

The simplest usecase for external checks is to test if a remote IP is still reachable after the changes are applied:

    from pyroute2.ndb.transaction import PingAddress

    ctx = ndb.begin()
    ctx.push(
        ndb.routes.create(dst="10.0.0.0", dst_len=24, gateway="172.16.0.1"),
        PingAddress("10.0.0.1")
    )
    ctx.commit()  # the route will be removed if ping fails

Or on the contrary, don’t run transaction if a remote IP is reachable:

    from pyroute2.ndb.transaction import Not, PingAddress

    ctx = ndb.begin()
    ctx.push(
        Not(PingAddress("10.0.0.1")),
        ndb.routes.create(dst="10.0.0.0", dst_len=24, gateway="172.16.0.1")
    )
    try:
        ctx.commit()
    except CheckProcessException:
        pass

In this example, the route will be added only if 10.0.0.1 is not reachable.

The default ping timeout is set to 1, but it is possible to customize it:

    PingAddress("10.0.0.1", timeout=10)

## Check an external processes

A more generic type of check is CheckProcess:

    from pyroute2.ndb.transaction import CheckProcess

    with ndb.begin() as ctx:
        ctx.push(ndb.routes.create(
            dst="10.0.0.0",
            dst_len=24,
            gateway="172.16.0.1"
        ))
        ctx.push(CheckProcess('/path/to/script.sh'))
        #
        # --> <-- the route will be removed if the script fails

CheckProcess is subprocess.Popen based, is not a shell call, thus no pipes or other shell syntax are allowed.

CheckProcess also accepts timeout argument:

    CheckProcess('/path/to/script.sh', timeout=10).commit()

If the subprocess doens’t finish within the timeout, it will be terminated with SIGTERM. SIGKILL is not used.

## Logging and debug

CheckProcess and PingAddress accept log as an argument:

    PingAddress("10.0.0.1", log=ndb.log.channel("util")).commit()
    CheckProcess("/path/to/script.sh", log=ndb.log.channel("util")).commit()

The check objects are thread safe and reusable, it is possible to run commit() on them multiple times. The subprocess’ stdout and stderr will be both logged and saved:

    check = CheckProcess("/path/to/script.sh")
    while True:
        check.commit()  # periodic check, the loop breaks on failure
        print(f'stdout: {check.out}')
        print(f'stderr: {check.err}')
        print(f'return code: {check.return_code}')
        time.sleep(10)

## Check negation

It is possible to negate the check for CheckProcess and child classes

    from pyroute2.ndb.transaction import Not, CheckProcess

    check = Not(CheckProcess('/path/to/script.sh'))
    check.commit()

## API

exception pyroute2.ndb.transaction.CheckProcessException

class pyroute2.ndb.transaction.CheckProcess(command, log=None, timeout=None)

  Run an external process on commit() and raise CheckProcessException if the return code is not 0.

  Objects of this class are thread safe and reusable.

class pyroute2.ndb.transaction.PingAddress(address, log=None, timeout=1)

class pyroute2.ndb.transaction.Not(transaction)

  Negate the CheckProcess results. If CheckProcess.commit() succeeds, raise CheckProcessException, and vice versa, if CheckProcess.commit() fails, return success.

class pyroute2.ndb.transaction.Transaction(log=None)

  Transaction class is an independent utility class. Being designed to be used with NDB object transactions, it may be used with any object implementing commit/rollback protocol, see commit() method.

  The class supports the context manager protocol and Transaction objects may be used in with statements:

      with Transaction() as tx:
          tx.push(obj0)  # enqueue objects
          tx.push(obj1)
          # --> <-- run commit() for every object in self.queue
          #
          # if any commit() fails, run rollback() for every
          # executed commit() in the reverse order

  NDB provides a utility method to create Transaction objects:

      with ndb.begin() as tx:
          tx.push(ndb.interfaces["eth0"].set(state="up"))
          tx.push(ndb.interfaces["eth1"].set(state="up"))

  push(*argv)

    Push objects to the transaction queue. One may use any number of positional arguments:

        tx.push(obj0)
        tx.push(obj0, obj1, obj2)
        tx.push(*[obj0, obj1, obj2])

  append(obj)

    Append one object to the queue.

  pop(index=-1)

    Pop an object from the queue. If an index is not specified, pop the last (the rightmost) object.

  insert(index, obj)

    Insert an object into the queue. The position index is required.

  cancel()

    Cancel the transaction and empty the queue.

  wait(timeout=None)

    Wait until the transaction to be successfully committed.

  done()

    Check if the done event is set.

  commit()

    Execute commit() for every queued object. If an execution fails, execute rollback() for every executed commit. All objects in the queue that follows the failed one will remain intact.

    Raises the original exception of the failed commit(). All the rollback() exceptions are ignored.