The Viper Python API¶
The Concept¶
Viper provides a powerful collection of data types such as Hosts
,
Runners
, Results
etc. and uses method chaining
to perform different operations. The viper.collections
module contains the
collection of such data types. These data types share some common properties as
all they inherit from the Collection
class.
Example: Method Chaining¶
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.
Unit vs Container Types¶
The above mentioned data types can be categorised as unit and container types.
The unit ones inherit from the Item
class, while the
container types inherit from Items
class.
Below are the list of unit types and their container type counterparts:
Unit Types | Container Types |
---|---|
Task |
|
Host |
Hosts |
Runner |
Runners |
Result |
Results |
Useful Common Properties & Abilities¶
The properties mentioned below are common to both unit and container type objects.
Immutable: All the datatypes are immutable i.e. they cannot be modified once initialized. This is to prevent any unexpected behaviour caused due to stateful-ness.
.from_json() and .to_json(): All the objects can be initialized from JSON texts using the
.from_json()
factory method and can be dumped back to JSON using the.to_json()
method. This enables the objects to use a wide range of mediums such as the Unix pipes..format(): The objects can be converted to a string with a custof format using the
.format()
method.Example:
host.format("{ip} {hostname} {meta.tag}")
Useful Abilities Common to the Unit Types¶
These abilities are common to Task
, Host
,
Runner
and Result
unit type objects.
- .from_dict() and .to_dict(): Helps representing the objects as Python dictionaries.
Useful Abilities Common to the Container Types¶
These abilities are common to Hosts
, Runners
and Results
container type objects.
.from_items() and .to_items(): The
.from_items()
factory method is the recommended way to initialize container type objects. Although it can be a little slower, it removes duplicate items and performs other important checks before initializing the object. It supports sequences, generators, unit objects or all at once.Attention
# Bad Hosts((host1, host2, host3)) # Good Hosts.from_items(host1, host2, host3)
The
.to_items()
or the alias.all()
returns the tuple of unit items back.Example:
Hosts.from_items( host1, host2 # Unit objects [host3, host4], # Sequence of objects (host for host in list_of_hosts) # Generator of objects ).to_items()
.from_file() and .to_file(): Container type objects can be initialized from text files and dumped back to text files with certain formats (currently supported json, yml and csv) using these methods.
Example:
Hosts.from_file("hosts.json").to_file("hosts.csv")
.from_list() and .to_list(): Similar to unit types’
.from_dict()
and.to_dict()
but operates with list of dictionaries that represent the unit type objects..count(): Returns the count of items it holds.
.head() and .tail(): Returns an instance of the same container type object containing first or last n items (n defaults to 10).
Example:
# Get the set of last 5 items from the set of first 10 items. hosts.head(10).tail(5)
.range(): Similar to
.head()
or.tail()
but enables us to define a range (like Python’slist[i:j]
indexing).Example:
# Exclude the last item (like like Python's list[0:-1]) hosts.range(0, -1)
.sort(): Similar to Python’s
list.sort()
but returns a new instance instead of making changes to the existing object (which is impossible because of immutability).Example:
# Reverse sort by IP, then by hostname hosts.sort(key=lambda host: [host.ip, host.hostname], reverse=True)
.order_by(): Similar to
.sort()
but expects the field names instead of a function. Inspired by SQL.Example:
# Reverse sort by ip, then by hostname hosts.order_by("ip", "hostname", reverse=True)
.filter(): Similar to Python’s
filter()
but returns an instance of the same container type object containing the filtered items.Example:
# Filter hosts where hostname starts with "foo" hosts.filter(lambda host: host.hostname.startswith("foo"))
.where(): Similar to filter, but expects the field name, the condition and the value instead of a function. Inspired by SQL.
Example:
# Filter hosts where the hostname starts with "foo" hosts.where( "hostname", WhereConditions.startswith, ["foo"] )
More on Task: Command Factories, Output Processors, Callbacks and …¶
The minimum requirements of defining a Task
is to pass
the task name and the command factory. Optionally, we can also pass the stdout and
stderr processors, and also the pre and post run callbacks.
The command factory expects a Host
object and returns a tuple of
string.
Example:
def ping_command(host):
return "ping", "-c", "1", host.ip
The stdout and stderr processors expect a string and return a string.
Example:
def strip_output(txt):
return txt.strip()
The pre run callback expects a Runner
object and doesn’t return
anything. While the post run callback expects a Result
object and
doesn’t return anything either.
Example:
import sys
def log_command_pre_run(runner):
command = runner.task.command_factory(runner.host, *runner.args)
print("Running command:", command, file=sys.stderr)
def log_result_post_run(result):
print("OK:" if result.ok() else "ERROR:", result.host.hostname, file=sys.stderr)
Note
Logs are being printed to stderr as stdout is for the JSON encoded
Results
object.
Attention
The arguments command_factory
, stdout_processor
, stderr_processor
,
pre_run
and post_run
callbacks expect normal functions, not lambdas.
# Bad
def ping():
return Task(
name="Ping once",
command_factory=lambda host: "ping", "-c", "1", host.ip,
stdout_processor=lambda txt: txt.strip(),
stderr_processor=lambda txt: txt.strip(),
pre_run=lambda runner: print(runner.to_dict(), file=sys.stderr),
post_run=lambda result: print(result.to_dict(), file=sys.stderr),
)
# Good
def ping():
return Task(
name="Ping once",
command_factory=ping_command,
stdout_processor=strip_output,
stderr_processor=strip_output,
pre_run=log_command_pre_run,
post_run=log_result_post_run,
)
Apart from these, a Task
also optionally expects timeout
,
retry
and meta
.
timeout: The execution will timeout after the specified seconds if timeout is defined.
The countdown doesn’t count the time spent on the pre and post run callbacks, neither the command factory invocation. It only counts time spent on executing the generated command.
retry: It defaults to 0. If more than 0, The runner will re-invoke the
run()
method with the updated retry value if the command execution fails. The results generated for these retries will be stored in DB and will be available in history. They will have the sametrigger_time
but differentstart
andend
time values.However, if the failure is caused by any reason other than the actual command execution, such as while invoking the command factory or output processors or pre/post run callbacks, a Python error will be raised which won’t be stored in DB. If any such error occurs while running the task in batch, it will be ignored with the traceback printed to stderr.
meta: It is the same as the
meta
field inHost
. The value should be generated only using theviper.meta()
function.Attention
# Bad def ping(): return Task( name="Ping once", command_factory=ping_command, meta={"tag": "foo"}, ) # Good def ping(): return Task( name="Ping once", command_factory=ping_command, meta=meta(tag="foo") )