The Viper Command-line Interface¶
The Concept¶
After we define the tasks, actions, jobs etc. in the workspace, we need a way to execute them. Dropping into a Python shell and using the Python API is one way to do that. However, that might not be the most preferred approach for everyone.
Viper provides a command-line interface through the viper
command to interact with the Python API without dropping into
a Python shell.
Similarity Between the Python API and Command-line Interface¶
The CLI interface closely follows the Python API. Each of the
subcommands with a colon (:
) represents a method of a class or
object. When we run viper --help
, we can see the signatures of
the methods/subcommands.
For example, the subcommand viper hosts:from-file
represents the
class method viper.Hosts.from_file()
,
In the help menu the signature of this subcommand is defined as
[Hosts]
which means that it returns a text (JSON)
representation of a Hosts
, object which can be
passed (piped) to another subcommand that expects the same via
standard input.
On the other hand, the results:order-by
has the signature
[Results -> Results]
. The subcommand represents the method
viper.Results.order_by()
and the signature
[Results -> Results]
means that the subcommand expects the text
(JSON) representation of a Results
object.
Example: Output Piping as Method Chaining¶
viper hosts:from-file("hosts.csv") \
| viper hosts:task task.ping \
| viper runners:run --max-workers 50 \
| viper results:final \
| viper results:order-by host.hostname host.ip \
| viper results:to-file results.csv \
| viper results:format "{host.hostname}: {stdout}"
In the above example, following things are happening:
- The
hosts:from-file
subcommand with signature[Hosts]
returns the text representation of aHosts
object. hosts:task
reads the output ofhosts:from-file
from standard input as it has the signature of[Hosts -> Runners]
and returnsRunners
.- Then the
runners:run
subcommand with signature[Runners -> Results]
reads the output ofhosts:task
from standard input and returnsResults
. - Finally
results:format
with signature[Results -> str]
turns theResults
into a string which cannot be passed (piped) to any further subcommand.
The data flow diagram:
hosts:from-file
->Hosts
|hosts:task
->Runners
|runners:run
->Results
|results:final
->Results
|results:order-by
->Results
|results:to-file
->Results
|results:format
-> str
The above CLI example is equivalent to the following Python example:
from viper import Hosts
import task
print(
Hosts.from_file("hosts.csv")
.task(task.ping())
.run(max_workers=50)
.final()
.order_by("host.hostname", "host.ip")
.to_file("results.csv")
.format("{host.hostname}: {stdout}")
)
Tip
Refer to Getting Started to see how task.ping
and hosts.csv
are written.
Defining Actions¶
Actions are simple Python functions that can be invoked using the viper lets
subcommand.
Example:
Define an action in action.py
:
cat > action.py << EOF
def add_them(a, b):
return int(a) + int(b)
EOF
Now invoke the action:
viper lets action.add_them 5 10
Output:
15
Defining Viper Objects: Hosts, Task¶
Similar to actions, we can also define functions that return an instance of
Task
or Hosts
. The *:from-func
subcommands will invoke the function to get the object it returns.
Example: Define a host group in hosts.py
cat > hosts.py << EOF
from viper import Hosts, Host
def group1():
return Hosts.from_items(
Host("192.168.0.11", hostname="host11"),
Host("192.168.0.12", hostname="host12"),
Host("192.168.0.13", hostname="host13"),
Host("192.168.0.14", hostname="host14"),
Host("192.168.0.15", hostname="host15"),
)
EOF
Get the hosts count in terminal:
viper hosts hosts.group1 | viper hosts:count
Output:
5
Note
viper hosts
is an alias of viper hosts:from-func
.
Similarly, viper task
is an alias of viper task:from-func
.
However, viper results
is an alias of viper results:from-history
as there’s no reason to write results ourselves. It should come from
the database.
So there’s no results:from-func
, neither runners:from-func
and so on.
Tip
Refer to Getting Started to find the example of task and job definitions.
Defining Utilities: Handlers, Filters, Sort Keys¶
Defining handlers, filters and sort keys are similar to defining actions but the first argument of the defined function is reserved for an instance of viper data type which it receives from the standard input.
Example:
Define a general handler in handler.py
that operates on
all Items
instances:
cat > handler.py << EOF
import sys
def log_count(items, arg1):
print(f"There are {items.count()} {arg1}.", file=sys.stderr)
return items
EOF
Use the handler:
viper hosts hosts.group1 \
| viper hosts:pipe handler.log_count hosts \
| viper hosts:count
Output:
There are 5 hosts.
5
Note
Here arg1
recieves the second argument passed to hosts:pipe
i.e. “hosts”.
Similarly filters and sort keys can be defined using functions having
the first argument reserved for the object it will operate on, and the
subsequent arguments for the variables that will be passed while invoking
the *:filter
and *:sort
subcommands.
However, we hardly will need to really define filters and sort keys like this
as most of the requirements of sorting and filtering should be satisfied with
the *:order-by
and *:where
subcommands respectively.