Welcome to PacketWeaver’s documentation!¶
Get started with PacketWeaver! Users, developers and contributors are all welcome!
Presentation¶
PacketWeaver (pw) is a Python script organization framework. It provides a nice development workflow for quickly creating re-usable Python scripts.
PacketWeaver is a Python framework, which helps build, organize and reuse your Python scripts. It is the most efficient when used in network script development projects.
It sets you up with a simple boilerplate to help building reusable scripts with a clear self-checked input interface and the ability for scripts to be chained between each others. These scripts can be organized into importable packages. This way they can be grouped by projects or topics and easily shared alongside the framework.
In addition to that, it provides a convenient interactive command line interface (CLI) to search, configure and run your scripts and a CLI to run your scripts from your favorite shell and other tools.
It’s all about abilities¶
When using PacketWeaver, your script source code is placed in an object
inherinting from the AbilityBase
class. Thus, your scripts are refered to as
Abilities during the remaining of this documentation.
Your scripts can be arbitrarily complex. They just all starts from the main()
function that must be overidden. From there, you can import and run other abilities,
build complex pipelines for information that goes through several abilities
before rendering a result. That is completely up to you.
Every ability share at least a small common code structure. The first common element is metadata. These metadata uniquely identifies your ability within its package. They also provide additional information such as tags for search purposes, author contact info, and the list of the other abilities that might be invoked/imported from this one.
The second common element is the list of input parameters. These parameters, called options, can be any one from several high-level data types, like an IP address, a boolean, a network interface name or an enumeration.
Using these options offers several benefits. Firstly, you can assign them from the interactive CLI before running an Ability. The value assignment is eased thanks to smart autocompletion of the possible values, whenever possible. Also, values are automatically checked against a set of built-in or customizable constraints before their assignment. This alleviates the need for your script to perform the parsing and the verification of the input values.
For example, we could specify an option that must contain the file system path of a valid executable program:
PathOpt("cmd_name", default="/bin/ls", executable=True)
Also, some special keyword values can be assigned. These keyword values trigger special handlers that might generated random values for your script to use. Various random value generators are available, including the ability to generate randomly IP address within a network subnet without ever returning twice the same IP address. For instance, the following option might be assigned any integer, but by default, it will return a random integer between 0 and 255 when your script will ask for its value:
NumOpt("priority", default="RandByte")
Finally, the last common element of all Abilities is the main
function,
whose purpose is similar to that of the C main()
function. Your script
entry point is this function, and your script will end when this function
returns or an unhandled exception bubbles up from it.
Within the main
function and, in fact, anywhere within the object that
contains it, you may take advantage of any of the helpers that are provided by
PacketWeaver, including:
- access to your input parameters as instance attributes, with
self.cmd_name
orself.priority
. - use of the integrated display/log engine to organize and colorize your script outputs, e.g
self._view.success('My log')
- call other Abilities and orchestrate their collaboration using the pipe notation:
ability1 | ablitiy2
As such, the boilerplate of your Abilities is as short as:
from packetweaver.core.ns import *
class Ability(AbilityBase):
_info = AbilityInfo(name='Say hello')
_option_list = []
def main(self):
self._view.success('Hello, world')
Developer oriented¶
PacketWeaver is a framework meant for you to develop new Abilities, build some from existing scripts and reuse them afterwards.
Thus the development workflow has been designed to ease the progressive writing
and testing of your Abilities. As such, the PacketWeaver interactive CLI
enables you to select any Ability, configure it with its input parameters and
test the latest version of your code and parameters with the run
command. If
the source code changes while PacketWeaver is running, the latest version is
automatically reloaded upon restart of the Ability, while keeping the Ability
configuration across the reload. In fact, you can even edit your Ability source
code right from the interactive CLI using the editor
command.
A typical PacketWeaver could be as follows:
pw> list
1 Ping a target -- []
2 Ping a prefix -- []
pw> use 1
pw (Ping a target)> set ip_dst=192.0.2.1
pw (Ping a target)> run
192.0.2.1 is DOWN
pw (Ping a target)> editor
... edit the source code to write DOWN in lowercase ...
pw (Ping a target)> run
192.0.2.1 is down
During this session, we listed the available Abilities within the currently loaded packages. We then selected the first Ability listed. We set the IP address to ping to 192.0.2.1. We ran the Ability, and saw that the IP address did not answer to our ping. We launched the default text editor to change the source code of our Ability, and we ran the ability once more, automatically using the updated source code.
A generic tool¶
Thanks to its generic design, PacketWeaver is not restricted to a specific use case. Mainly used with Scapy hitherto, the framework features are built to be as generic as possible.
The package mechanism and the use of the Python programming language make it a good option to develop specific tools for a wide variety of use cases. Here are some usage examples where PacketWeaver could fit especially well:
- In the educational/research field by:
- building fill-in-the-blank autonomous exercises;
- creating standalone demonstrations;
- demonstrating some network protocol mechanisms;
- automating network devices testing.
- During a network security audit by:
- building a bank of your favorite packages
- taking advantage of advanced Abilities, developed in research phases
- reuse content from one assignment to another, and progressively improve it
Usage¶
This first section presents the basic interactions with PacketWeaver:
Installation¶
All you need to do is getting the source code using git.
Note
The framework was tested under Ubuntu 18.04 LTS (amd64).
There are no distribution/pip packages of this framework available yet, but this definitely is on our todo-list.
Sources and dependencies¶
Browse to your favorite git cloning destination folder (e.g. ~/git/) and clone the PacketWeaver directory:
git clone https://github.com/ANSSI-FR/packetweaver
cd packetweaver
Packetweaver has no strong external dependencies. This means you should be ready to go, if you have just the Python3 interpreter and the Python standard library. If you ever ran Packetweaver that way, however, you would miss quite a bunch of helpers that would be automatically disabled.
To enable these helpers, you might want to install some or all of the following dependencies:
- pyroute2
- gmpy2
- pcapy
The easiest way to install them is to use a combination of your package manager and pip3:
sudo apt install python3 python3-dev build-essential libpcap-dev python3-pip libgmp-dev libmpfr-dev libmpc-dev
sudo pip3 install pyroute2 gmpy pcapy pyroute2
Packages might require additional dependencies. Please refer to their documentation.
See also
More dependencies are required if you wish to build this documentation offline or run the framework automated tests.
Run it¶
Once PacketWeaver retrieved from git, you may run it from shell using the run_pw
script:
./run_pw
You might need to make some minor adjustments to the configuration file:
vim packetweaver/pw.ini
If PacketWeaver displayed its banner and the pw>
prompt, you are now good to go.
You can now hit “ctrl+d” or type in the exit
command to quit PacketWeaver and start browsing the next section of this documentation!
Shell and interactive command line¶
PacketWeaver gives you two ways of interacting with your Abilities (see Introduction for explanations).
The pw interactive CLI¶
The interactive CLI is the main way of interacting with the framework. It is composed of two levels: one that enables you to browse and to select available Abilities, and another one to configure and run a selected Ability.
The interactive CLI offers command history features similar to that of usual
shells, such as history search and command recall, using ctrl+p
, ctrl+n
and ctrl+r
.
To cancel a command, you may use ctrl+c
. ctrl+d
and exit
may be
used to return to unselect an Ability, or exit the framework. Finally, tab
can be used to trigger autocompletion of the current command line, whenever
available.
Ability browsing and selection¶
To launch the interactive CLI, you may run the run_pw
script located at the
root of the PacketWeaver git repository:
./run_pw
This CLI understands a bunch of commands whose list you can obtain by typing
the help
command or ?
for short. Help of any of the listed commands can
be obtained by using the help
command followed by the name of a command:
pw> help list
For instance, the list
command displays a list of all the available standalone
Abilities that are loaded from the configured packages.
Each list entry is indexed by a number, so that you can refer to this Ability
by its associated number. For instance, to use the Ping a target
Ability,
you would type list
and then use 1
to select the Ability for use,
because it is the first listed:
pw> list
1 Ping a target -- []
2 Ping a prefix -- []
3 DNSProxy -- ['Application_Layer', 'Threaded', 'DNS']
pw> use 1
pw (Ping a target)>
While list
enumerates all loaded standalone Abilities, this might not be
convient if you loaded a very large package containing tons of abilities. You
may use the search
command to find any Ability that would be listed by
list
. Search results are indexed, just like listed Abilities are.
search
matches Ability names by default, using a case-insensitive
comparison:
pw> search ping
1 ping a target --
2 ping a prefix --
pw> search dns
1 dnsProxy -- Application_Layer, Threaded, DNS
You can also search by tags:
pw> search app
No matching ability found.
pw> search -a application_layer
1 DNSProxy -- application_layer, Threaded, DNS
Searching by tags is eased by autocompletion that will provide you with a list of all the tags that are registered by currently available Abilities.
Searches for tags may use multiple tags simultaneously. When multiple tags using a logical operation
between tags (and or or) may be specified with flags. The -o
flag
indicates an OR, while -a
indicates an AND.
For instance, you could search for all Abilities that are related to DNS and to MITM:
pw> search -a dns mitm
No matching ability found.
You may also want to list all Abilities that are either related to DNS or to HTTP:
pw> search -o dns http
1 DNSProxy -- application_layer, Threaded, DNS
Any of these flags can be used if you are filtering with only one tag.
Note
A default index is built when the framework starts. You can quickly re-open you current in-development Ability across
framework restart by recalling the last use
command to access it directly.
After selecting an index, if no errors are displayed, you now are interacting with a CLI specific to the selected Ability. This CLI is described in details in the next chapter.
Configuring, editing and running an Ability¶
Once more, the help
command is available to list the commands that are
available with this new interactive CLI, and the help
command may be used
to obtain further details about the listed commands.
You may also get more details about the selected Ability, with the info
command:
pw (DNSProxy)> info
------------------------------ [ DNSProxy ] -------------------------------
type: Standalone
description: Replacement for DNSProxy
authors: Florian Maury
tags: Application_Layer, Threaded, DNS
reliability: Incomplete
---------------------------------------------------------------------------
Details about the role and the usage of the selected Ability may also be
obtained with the howto
command:
pw (DNSProxy)> howto
This DNS proxy intercepts DNS requests at OSI layer 2.
...
the real DNS server is connected to a different card than the victim.
The options
command (or its alias ls
) lists the Ability parameters that may be set before
running that Ability:
pw (DNSProxy)> options
------------------------------- [ Options ] -------------------------------
fake_zone = /bin/true
policy_zone = /bin/true
ip_src (Optional) = None
ip_dst (Optional) = None
port_dst (Optional) = 53
interface = None
outerface (Optional) = None
quiet = True
---------------------------------------------------------------------------
Options may be set to user values or reset to their default values using the
set
and clear
commands. For instance, let’s set some option value:
pw (DNSProxy)> set ip_dst=8.8.8.8
pw (DNSProxy)> set interface=eth0
pw (DNSProxy)> set ip_src=192.0.2.1
pw (DNSProxy)> options
------------------------------- [ Options ] -------------------------------
fake_zone = /bin/true
policy_zone = /bin/true
ip_src (Optional) = 192.0.2.1
ip_dst (Optional) = 8.8.8.8
port_dst (Optional) = 53
interface = eth0
outerface (Optional) = None
quiet = True
---------------------------------------------------------------------------
Note
Some input parameters (such as IpOpt) support value generators. You can get
a list of them using the autocompletion when trying to set a new value
(set ip_dst=
+ Tab Tab
) to them.
For instance, you may set an IP address to the special value RandIP4
. A
new IP address will be generated each time the Abliity is run:
pw (DNSProxy)> set ip_dst=RandIP4
pw (DNSProxy)> options
------------------------------- [ Options ] -------------------------------
fake_zone = /bin/true
policy_zone = /bin/true
ip_src (Optional) = 192.0.2.1
ip_dst (Optional) = RandIP4
port_dst (Optional) = 53
interface = eth0
outerface (Optional) = None
quiet = True
---------------------------------------------------------------------------
You may also use this special keyword as a function, to set a random value to the variable. This random value won’t change across runs:
pw (DNSProxy)> set ip_dst=RandIP4()
pw (DNSProxy)> options
------------------------------- [ Options ] -------------------------------
fake_zone = /bin/true
policy_zone = /bin/true
ip_src (Optional) = 192.0.2.1
ip_dst (Optional) = 198.51.100.42
port_dst (Optional) = 53
interface = eth0
outerface (Optional) = None
quiet = True
---------------------------------------------------------------------------
Note
If you try to set a invalid value for an option, you will receive an error message and the stored option value will remain unchanged. Each type of options is designed with a set of constraints, including some customizable ones, to validate the values that may be assigned to it.
Now let’s reset the source IP address to its default value:
pw (DNSProxy)> clear ip_src
pw (DNSProxy)> options
------------------------------- [ Options ] -------------------------------
fake_zone = /bin/true
policy_zone = /bin/true
ip_src (Optional) = None
ip_dst (Optional) = 8.8.8.8
port_dst (Optional) = 53
interface = eth0
outerface (Optional) = None
quiet = True
---------------------------------------------------------------------------
Now let’s reset all options to their default value:
pw (DNSProxy)> clear
pw (DNSProxy)> options
------------------------------- [ Options ] -------------------------------
fake_zone = /bin/true
policy_zone = /bin/true
ip_src (Optional) = None
ip_dst (Optional) = None
port_dst (Optional) = 53
interface = None
outerface (Optional) = None
quiet = True
---------------------------------------------------------------------------
Once you and your options are all set, you may run the Ability with the run
command:
pw (DNSProxy)> run
Abilities may run until they are done with their tasks. In that case, they will give back control to the CLI once they terminated. Other Abilities may start some services and are designed to run until interrupted by the SIGINT signal (ctrl+c). Of course, you may interrupt any running Ability, in case it went into an infinite loop of sorts, with the same key sequence.
If you are satisfied by the results of the Ability that you just run, you may want to save the parameter values that you used. This enables you to reload them, during a future session of PacketWeaver, or to back them up for a future audit report, for instance.
To save the current parameter values, you may use the save
command:
pw (DNSProxy)> save /path/to/file.ini
To reload them, you may use the load
command:
pw (DNSProxy)> load /path/to/file.ini
At some point, you may feel the need to make some minor code adjustments in the
Ability you are about to run (e.g. a bug fix…). You don’t need to exit
PacketWeaver for this. The editor
command will open the source code file
of the selected Ability and of all other Abilities that take part in the
selected Ability operations. Which source code editor is run is configured
within PacketWeaver configuration file.
Finally, once configured you may ask of PacketWeaver to automatically generate a shell command line that will run this Ability with the current configuration from shell:
pw (DNSProxy)> cmd
export PW_OPT_FAKE_ZONE='/bin/true'
export PW_OPT_POLICY_ZONE='/bin/true'
export PW_OPT_IP_SRC='None'
export PW_OPT_IP_DST='None'
export PW_OPT_PORT_DST='53'
export PW_OPT_INTERFACE='None'
export PW_OPT_OUTERFACE='None'
export PW_OPT_QUIET='True'
export PYTHONPATH='/opt/pw/pw/packetweaver:/usr/local/lib/python2.7/dist-packages/python_twitter-1.0-py2.7.egg:/usr/local/lib/python2.7/dist-packages/oauth2-1.5.211-py2.7.egg:/usr/local/lib/python2.7/dist-packages/pympress-0.3-py2.7.egg:/opt/pw/pw:/usr/lib/python2.7:/usr/lib/python2.7/plat-x86_64-linux-gnu:/usr/lib/python2.7/lib-tk:/usr/lib/python2.7/lib-old:/usr/lib/python2.7/lib-dynload:/usr/local/lib/python2.7/dist-packages:/usr/lib/python2.7/dist-packages:/usr/lib/python2.7/dist-packages/PILcompat:/usr/lib/python2.7/dist-packages/gst-0.10:/usr/lib/python2.7/dist-packages/gtk-2.0:/usr/lib/python2.7/dist-packages/ubuntu-sso-client:/usr/lib/python2.7/dist-packages/ubuntuone-client:/usr/lib/python2.7/dist-packages/ubuntuone-couch:/usr/lib/python2.7/dist-packages/ubuntuone-storage-protocol:/opt/pw/scapy'
python /opt/pw/pw/packetweaver/pw.py use -p base -a DNSProxy --fake_zone=${PW_OPT_FAKE_ZONE} --policy_zone=${PW_OPT_POLICY_ZONE} --ip_src=${PW_OPT_IP_SRC} --ip_dst=${PW_OPT_IP_DST} --port_dst=${PW_OPT_PORT_DST} --interface=${PW_OPT_INTERFACE} --outerface=${PW_OPT_OUTERFACE} --quiet=${PW_OPT_QUIET}
This command line can be copy/pasted in a shell console to start the ability with these options.
Warning
This command line generation is experimental and might quickly evolve.
Note
You can also use the cmd oneline
option to get a bash oneline command that can be directly tested.
Configuration¶
PacketWeaver can be configured with an .ini file. The default config file is
located in PacketWeaver installation directory. This is the one used when
running the run_pw
shell script. However, you may specify another location
using the command line option --config
or -c
for short. In that case,
the syntax is:
$ ./run_pw --config=/path/to/my/pw.ini interactive
The configuration file is composed of four sections: * Dependencies * Packages * Tools * Internals
Each of these sections are described herebelow.
Note
Make sure to restart the framework after a modification to take the new parameters into account.
Dependencies Configuration Section¶
The Dependencies section contains a list of Python module paths that will be added to your PYTHONPATH environment variable during PacketWeaver execution. Its only purpose is to spare you from setting the PYTHONPATH environment variable before each run of PacketWeaver. For this reason, the section only need to contain path of dependencies that are not already in the Python default import paths and in the PYTHONPATH environment variable that you may have set otherwise.
Key names does not matter in this section and only need to be unique.
For example, if you want to use the lastest Scapy
version, you may clone it whatever git directory you use (e.g. ~/git) and set
your Dependencies
section as follows:
[Dependencies]
scapy=/absolute/path/to/scapy/
If you want to use a relative a path, the path must be relative to the directory of your packetweaver/pw.py file. If Scapy repository was cloned in the same directory as your PacketWeaver repository, you would set your configuration like this:
[Dependencies]
scapy=../../scapy/
Finally several paths can be specified, using two different keys:
[Dependencies]
scapy=../../scapy/
yourlib=../../yourlib/
Packages Configuration Section¶
PacketWeaver enables you to file your Abilities into seperate logical groups of Abilities. These groups of Abilities are called Packages in the PacketWeaver lingo. You may want to organize your Abilities like this because they all share a common set of dependencies. Or maybe there are all interdependent. Or maybe they share a common goal or purpose. Or maybe you are the type to label everything. No judge.
Packetweaver loads at startup all packages that are declared in this
configuration section. The more packages you have, the more results are
displayed by the list
interactive command line command and the more
Abilities can be found with the search
command. The more packages are
loaded, the slower Packetweaver is to startup.
Note
In the Packages configuration section, key names are crucial, and
renaming them might be expensive. Indeed, there might be some source code
references to that name, when declaring interdependencies between Abilities.
Thus, it is advised to use virtually unique package names. For instance one
could use a prefix to namespace the package names:
mycompanyname_dancing_monkey
.
The entry values of this section are paths to Python modules that are valid
PacketWeaver packages. Please, refer to the Writing a package
section of this documentation for more information about Packages declaration.
Paths may be absolute paths on the filesystem, or paths relative to the
pw.py
file, just like for the Dependencies configuration section.
For example, your Packages configuration section might look like this:
[Packages]
base=abilities/
mycompany_flying_monkeys=/opt/pw_circus/
mycompany_ducking_ducks=../../pkgs/missing_animals/abilities/
Tools Configuration Section¶
This section only contains one configuration key hitherto: editor
.
Editor¶
The Editor option lets you select your favorite text editor. It will be used by
the editor
command available in the interactive CLI, after you selected an
Ability. For instance, you would configure this option to be:
your default system editor (probably graphical):
[Tools] editor=xdg-open
a text mode editor:
[Tools] editor=vim
a text mode editor using options:
[Tools] editor=emacs -nw
Internals configuration section¶
This section only contains one configuration key hitherto: HistFile.
History¶
The PacketWeaver interactive CLI offers command history. This history is saved
to a dedicated file. You can customize its name and location by editing the
HistFile
configuration key. To store it in your home directory, you may
specify:
[Internals]
HistFile=~/.pwhistory
Paths are expanded if need be.
Packages¶
Functionally speaking, a PacketWeaver package is a set of Abilities, grouped by purpose, topic, authors, interdependencies, version control access-control or whatever other reasons you might think of. Having this package feature enables you to share/publish only parts of your scripts. It also introduces namespaces, because Abilities must have a unique name within a same package, but a same name can be reused across packages.
Python-wise, a PacketWeaver package is a Python module, whose __init__.py
file contains a global variable called exported_abilities
. This variable
contains a list of class objects inheriting (directly or indirectly) from the
AbilityBase
class.
Basic structure¶
We suggest that you structure your PacketWeaver packages as follows:
pw-pkg-test/
├── doc/
└── abilities/
├── demo/
│ ├── demo_app.py
│ └── __init__.py
├── __init__.py
└── test_app.py
The abilities
folder is a Python module, whose __init__.py
contains a
list of the activated abilities:
import test_app
from demo import demo_app
exported_abilities = [
test_app.Ability,
demo_app.Ability,
]
In that case, test_app.py
and demo_app.py
both contain a Python class
called Ability
that herites from AbilityBase
. Abilities must be
declared in this list to be usable by other Abilities or to be listed by the
PacketWeaver CLI.
Package usage¶
To use a package, you must declare it in PacketWeaver configuration file, as described in the configuration file section.
Naming convention¶
Most Abilities are stored in separate Python files, each containing an Ability
called Ability
.
PacketWeaver package directories are generally named pw-pkg-<your_pkg_name>.
People will most probably import your package by configuring whatever you
defined as your_pkg_name
as this package key for the PacketWeaver
configuration file Package section. Since PacketWeaver package names must be
unique in a configuration file, it is advised that you prefix your package
name, for instance with the name of your company or group.
Developing Abilities¶
This section describes the writing of an Ability.
First, we will take a look at the boilerplate, which is common to all Abilities.
Then, we will look at the way to insert your code into this boilerplate, and how to take advantage of the predefined builtin parameter types.
We will also cover how to handle external library imports with regards to PacketWeaver built-in mechanism to detect missing requirements.
Then, we will cover the topic of nested Abilities, and how to send parameters and get results from an Ability.
Finally, we will study how to start parallel Abilities, how they communicate, and how to interact with them.
Basic ability writing¶
To write an Ability, you need to use a very short boilerplate:
# coding: utf8
from packetweaver.core.ns import *
class Ability(AbilityBase):
_info = AbilityInfo(
name='Insert your Ability name here',
)
def main(self):
# Insert your code here
return 0
To get started, you need to:
- copy/paste that boilerplate into a Python file;
- change the name in the AbilityInfo instantiation. This must be a name unique to the package that contains that Ability;
- import this Python file from the
__init__.py
file of the package that contains your Ability and add a reference to your Ability class object to theexported_abilities
of the package that contains your Ability. This is described in details in the configuration section of this documentation; - stick some code into the
main
mathod.
To check that you got that right, you can simply have the main
method
return a constant and try and run that Ability from the PacketWeaver
interactive CLI.
For example, let’s pretend that you did not change the name of the template
Ability, then you can test it like this:
./run_pw
pw> list
pw> search insert
1 insert your Ability name here
pw> use 1
pw (Insert your Ability name here)> run
0
pw>
The above template will return 0, and this will be displayed by the CLI, upon completion of this Ability.
Tip
Once you have your Ability selected in the interactive CLI, remember that
you can type the editor
command to edit its source code. Every changes will
be reloaded automatically, so that you need to type run
again to see the
result of your update.
Warning
Whenever you copy/paste an existing Ability or the template Ability,
remember to change the name
value. Failing to do so will break
PacketWeaver import mechanism
Complete the meta-data¶
The Ability name is not the only information that you may set up.
Some information are just for display purposes, when you enter the info
command after selecting an Ability in the interactive CLI. This is the case of:
- description;
- authors;
- references;
- diffusion;
- reliability.
Two other pieces of information have special purposes.
The tags are a list of strings, some predefined and standard in PacketWeaver,
some custom. Any tag can be searched (and autocompleted) when using the
search
interactive CLI command.
The type values either AbilityType.STANDALONE
or
AbilityType.COMPONENT
. A component Ability is an Ability that can only be
used as part of nested Abilities. Conversely, A standalone Ability can be used
both as a component of nested Abilities, and can be run directly from the CLI.
The rationale is that you may not want to polute your Ability listing in the CLI
with all Abilities, including some that are relatively abstract and not meant to
be run directly.
Here is a complete example to illustrate them:
_info = AbilityInfo(
name='Ability basics',
description='Demonstrate a basic ability',
tags=['myproject', Tag.EXAMPLE],
authors=['pw-team',],
references=[['PacketWeaver online documentation', 'packetweaver.readthedocs.io'],],
diffusion='public',
reliability=Reliability.RELIABLE,
type=AbilityType.STANDALONE
)
Defined constants¶
To help building a consistent searchable database of Abilities, some constants were defined. This is the case for the tags or the reliability information.
Note
Using the built-in values is not mandatory (except for the type
): you
can replace them by any string value. Just make sure that it does not make your
Ability more difficult to find by adding tags very similar to the default ones.
Contributors are encouraged to suggest new built-in tags.
All the built-in values are defined in the packetweaver/core/models/status.py source file.
The howto
method¶
The main
method is not the only method that is common to all Abilities and
that Ability developers are meant to override. The howto
method is called by
the howto
command that users may enter after selecting an Ability. While the
exact behavior is up to Ability developers, this method is meant to display
some kind of message for the user better grasp how to use the Ability they just
selected.
Feel free to provide step-by-step descriptions, to add interactions or to provide comprehensive guidance on how to use your ability.
Adding parameters to your Ability¶
Several types of parameters may be passed to an Ability. Parameters may be set directly from the CLI, or they may be passed by another Ability in case of nested Abilities.
These parameters are strongly-typed: values are automatically checked upon assignment, with an AttributeError exception being raised if the value is inappropriate. These parameters may also contain special values, which triggers value generation at running time.
To add parameters to your Ability, you need to set a class property named
_option_list
containing, as the name implies, a list of options instances.
Here follows a example of such an option list:
_option_list = [
PathOpt('path', default='/bin/true', comment='mon exe', executable=True),
PathOpt('path', default=None, comment='path to nowhere', optional=True, executable=False),
IpOpt('mon_ip', default='RandIP', comment='mon ip'),
IpOpt(ns.OptNames.IP_DST, default='127.0.0.1', comment='IP of the target'),
MacOpt(ns.OptNames.MAC_SRC, default='RandMac', comment='Mac of the sender'),
StrOpt('data', default='useful string', comment='Some data'),
NumOpt('number', default=0, comment='A number (like port, counters...'),
ChoiceOpt('action', ['run', 'stop', 'reboot'], comment='performed on the dstIp '),
]
Parameters cover various data types such as IP and MAC addresses, strings, numbers, network cards, IP subnets, booleans, file system paths or choice options.
All parameter types may also contain “None”, which can be assigned to parameters that are optional.
Parameter constructors all receive a name as a first parameter. This name is used to set and get this option value, both from the command line and from the code.
While developing an Ability, you may obtain the current parameter value
from any method of the Ability, by accessing it as a attribute from that Ability
class instance. For instance, to access the value of a BoolOpt, representing a
boolean, called my_option_name
, you may simply write:
self.my_option_name
Note
While parameter naming is free of constraints, you might want to use
some of the built-in names, that are listed in the OptNames
class in
packetweaver/core/models/status.py. Using these names in your Ability
creates a sense of consistency that makes the user safe.
Warning
Please keep in mind that if you want to access your option value using the attribute syntax, you need to keep your names within the boundaries of the Python variable naming constraints. If you want to use hyphen, spaces or whatever other invalid characters, you will need to access the parameter value using the following syntax:
self.get_opt('my name, containing spaces and punctuation')
When reading the value of a parameter containing one of the special expressions that generate data, the latest generated value is cached, so that multiple read yield the same result:
a = self.my_option_name
b = self.my_option_name
assert(a == b)
You may force the generation of a new value by asking for a cache bypass. For
this, there is no direct read of the attribute. Instead, you need to use the
get_opt
method, inherited from AbilityBase
:
a = self.my_option_name
b = self.get_opt('my_option_name', bypass_cache=True)
assert(a != b) # Most probably different, if the RNG God is nice with us :)
Boolean parameters¶
Booleans are represented by the BoolOpt
class.
Values that can be successfully assigned to a BoolOpt
are:
True
;False
;"True"
;"False"
;None
or"None"
if theBoolOpt
is optional.
You may define a default value using the default
keyword argument, when
declaring the BoolOpt
:
BoolOpt('my_bool', default=False)
The default default value is False
.
String parameters¶
Strings are represented by the StrOpt
class.
Any string may be assigned to such a parameter, except "None"
and
RandString
. The former can be assigned when the StrOpt
is
optional. The latter is a special keyword, which indicates that when
reading the parameter value, strings of random length and content must be
generated.
As for the others, the default
keyword argument enables you to define a
default value for this parameter. The default default value is "RandString"
.
Number parameters¶
Numbers (both integers anf floats) are represented by the NumOpt
class.
Values that can be successfully assigned to a NumOpt
are:
- any integer or float in Python int/float format
- any string that can be parsed by Python standard library into an integer or float
None
or"None"
if theNumOpt
is optional;"RandByte"
to generate a random integer between 0 and 2**8;"RandSByte"
to generate a random integer between -2**7 and 2**7 - 1;"RandShort"
to generate a random integer between 0 and 2**16;"RandSShort"
to generate a random integer between -2**15 and 2**15 - 1;"RandInt"
to generate a random integer between 0 and 2**32;"RandSInt"
to generate a random integer between -2**31 and 2**31 - 1;"RandLong"
to generate a random integer between 0 and 2**64;"RandSLong"
to generate a random integer between -2**63 and 2**63 - 1.
As for the others, the default
keyword argument enables you to define a
default value for this parameter. The default default value is "RandByte"
.
IP address parameters¶
IP addresses are represented by the IpOpt
class. The class may store any IP
address, be it in IPv4 or IPv6.
Values that can be successfully assigned to a IpOpt
are:
- any IPv4 in quad-dotted format;
- any IPv6, compressed or uncompressed;
"RandIP4"
to generate a random IPv4 address, which might be anywhere in the address space, including private networks, class D and E, and loopback;"RandIP6"
to generate completely random IPv6 address, with no guarantee that the address will be valid"RandIP_classA"
to generate a random IP within the IPv4 class A"RandIP_classB"
to generate a random IP within the IPv4 class B"RandIP_classC"
to generate a random IP within the IPv4 class C"RandIP_classD"
to generate a random IP within the IPv4 class D"RandIP_classE"
to generate a random IP within the IPv4 class E
Note
A more complete syntax is on our TODO-list to enable you to define random ranges (e.g. 192.168.10-20.*).
As for the others, the default
keyword argument enables you to define a
default value for this parameter. The default default value is "RandIP4"
.
Warning
IpOpt
value validation are using either the Python
ipaddress
module or the netaddr
module. If you do not have any of
these, then the validation will not be performed and just about any value
will be tolerated.
IP subnet/prefix parameters¶
IP prefixes are represented by the PrefixOpt
class. This class may store any
IP prefix, be it in IPv4 or IPv6.
This parameter type is meant to enable you to walkthrough the prefix, by generating each and every one IP address of the specified prefix. Generation by either be ordered, from the first address to the last address of the prefix (excluding the network address and the broadcast address).
As for the others, the default
keyword argument enables you to define a
default value for this parameter. The default default value is
"127.0.0.0/8"
.
This parameter constructor also has a ordered
keyword argument, which values
False
by default. If True
, the generation of the IP address of the
prefix will be from the first address to the last one. If False
and if
prerequisites are met, the IP address will be generated randomly inside the
prefix without ever generating the same address twice. This might come in handy
when scanning large networks, if you do not want to indirectly harass a
middlebox such as a firewall that is on path with many scanned endpoints inside
a subnet.
When all IP addresses of the specified prefix are generated, the next
cache bypass raises a StopIteration
exception.
Here follows an exemple of a PrefixOpt
instantiation:
PrefixOpt('MyPrefix', default='192.0.2.0/29', ordered=True)
And the usage of such an option could be:
try:
while True:
print(self.get_opt('MyPrefix', cache_bypass=True))
except StopIteration:
pass
Caution
This parameter does not work with /31 and /32 prefixes, and it will not work either with /127 and /128 prefixes.
Warning
This parameter is heavily based on the Python ipaddress
or
netaddr
module, so you will need them to get anywhere with this option.
Also, you might need the Python gmpy2
module to have random IP address
generation from this parameter.
MAC address parameters¶
Physical addresses (MAC addresses) are represented by the MacOpt
class.
Values that can be successfully assigned to a MacOpt
are:
- any well-formated MAC address, as a string;
"Mac00"
as a shorthand for the null MAC address;"MacFF"
as s shorthand for the broadcast MAC address;"RandMac"
to generate a random MAC address;"RandMulticastMac"
to generate a random Multicast MAC address from the IPv4 multicast associated MAC address range;None
or"None"
if theMacOpt
is optional.
As for the others, the default
keyword argument enables you to define a
default value for this parameter. The default default value is "RandMac"
.
Choice parameters¶
Choice parameters represent alternatives from which you can select one value.
The set of available choices is up to Ability developers, who must list them at
instantiation of the ChoiceOpt
class:
ChoiceOpt('favorite_food', ['pizza', 'beer', 'greenStuff'])
As for the others, the default
keyword argument enables you to define a
default value for this parameter. The default default value is the first choice
in the specified list.
If the choice parameter is optional, the special "None"
value may be
assigned too.
Port number parameters¶
Port numbers are represented by the PortOpt
.
Values that can be successfully assigned to a PortOpt
are:
- any port number between 0 and 65535 inclusive as a Python integer or a Python string;
"RandPort"
to generate a random port number between 1 and 65535 inclusive;"RandPrivilegedPort"
to generate a random port number between 1 and 1023 inclusive.
As for the others, the default
keyword argument enables you to define a
default value for this parameter. The default default value is "RandPort"
.
Network card parameters¶
Network card names are represented by the NicOpt
.
Valid values for NicOpt
are all network device name on the current computer,
be it the name of a network card, a bridge, a virtual Ethernet adapter, or any
other types of network devices really.
As an exception, this parameter accepts None
and "None"
even if this
parameter is not optionnal. The rationale is that network device
names vary between platform and distros and having a non-None default value
would break on random platforms. As such, the default default value is None
and it is advised not to override it.
Warning
Validation of NicOpt values is performed using the Python
pyroute2
library. If this library is missing, any value will be
accepted.
Filesystem path parameters¶
Filesystem paths are represented by PathOpt
.
Valid values for this parameter type are all strings. The valid paths may however be constrained even further using a set of keyword arguments at instanciation time. Here follows the list of the various constraints that may be specified:
must_exist
: ifTrue
, the value must be the path of an existing file; ifFalse
, the file must not exist at the time of check.None
means “do not care”;readable
: ifTrue
, the value must be an existing file and the user running PacketWeaver must have read access on that file; ifFalse
, the user running PacketWeaver must not have read access on that file.None
means “do not care”;writable
: ifTrue
, the value must be an existing file and the user running PacketWeaver must have write access on that file; ifFalse
, the user running PacketWeaver must not have write access on that file.None
means “do not care”;executable
: ifTrue
, the value must be an existing file and the user running PacketWeaver must have execute access on that file; ifFalse
, the user running PacketWeaver must not have execute access on that file.None
means “do not care”;is_dir
: ifTrue
, the path must be one of an existing directory. IfFalse
, the path may be a directory or not.
Warning
must_exist
is subject to race conditions. This constraint is
not for security purposes.
So, basically, if you want to create a PathOpt to write a log file, you might want to be sure that you are not overwriting any existing file:
PathOpt('log_file', default='/var/log/mylog.txt', must_exist=False)
Caution
must_exist=False
is incompatible with readable
,
writable
and executable
, because the file does not exist, and does
not have any ACL (yet).
Paths can be specified in an absolute or relative manner, the latter being interpreted starting from the ability’s ability package root path.
Text output¶
To help you highlight your code output, the _view
object is available to
display colored messages and block structures.
Here are some examples you may use:
self._view.success('Display in green')
self._view.delimiter('A dashed line with title') # with a fixed len
self._view.delimiter() # a dashed line with the same length
self._view.warning('Display in yellow')
self._view.error('Display in red')
self._view.fail('Display in cyan')
self._view.progress('Display in blue')
self._view.debug('Display in purple')
self._view.success('Display in your default terminal color')
Advanced Ability Writing¶
Using another Ability¶
About the Ability Types¶
Simple Abilities might be self-contained, and self-sufficient. However, to improve reusability, one might want to split code into multiple Abilities that may or may not be run independently from the CLI.
There are actually two types of Abilities: standalone ones, and components.
A component is an Ability that can only be called from an other Ability. They are not listed in the CLI. Conversely, a standalone Ability is listed in the CLI and might be used by itself, although it might also be called from an other Ability.
The type of Ability is defined with the type
parameter of an AbilityInfo
instance:
from packetweaver.core.ns import *
class ComponentAbility(AbilityBase):
_info = AbilityInfo(name='Example', type=AbilityType.COMPONENT)
...
class StandaloneAbility(AbilityBase):
_info = AbilityInfo(name='AnotherExemple', type=AbilityType.STANDALONE)
Declaring a Dependency to another Ability¶
An Ability using other Abilities must declare them as dependencies. This is done
by defining a class property called _dependencies
. It contains a list of
strings or tuples describing each dependency.
A string may be used when using some of the builtin Abilities from PacketWeaver. For instance, an Ability using the built-in Man-in-the-middle Ability, may declare:
class Ability(AbilityBase):
_dependencies = [ 'mitm' ]
In the general case, though, tuples are used. Each tuple is composed of three elements:
- the pet name that you want to use to refer to that dependency within your Ability;
- the name of the package (as defined in the PacketWeaver configuration file) that contains the Ability that your Ability relies on;
- the name of the Ability that your Ability relies on, as declared in that AbilityInfo name attribute.
For instance, if an Ability uses another Ability named Test your might
stored in the package TestPackage, that Ability _dependencies
declaration
could be:
_dependencies = [('mytest', 'TestPackage', 'Test your might')]
Getting an Instance of another Ability¶
Once an Ability declared as a dependency, you may obtain an instance of that
Ability using the built-in get_dependency
method:
class Ability(AbilityBase):
_dependencies = [('mytest', 'TestPackage', 'Test your might')]
def main(self):
instance = self.get_dependency('mytest')
If the dependency has input parameters declared through a _option_list
class
property, you may set them using keyword arguments, during the
get_dependency
call. For instance, if the Test your might Ability defined
a NumOpt
called skill_level
, one could define the argument like this:
class Ability(AbilityBase):
_dependencies = [('mytest', 'TestPackage', 'Test your might')]
def main(self):
instance = self.get_dependency('mytest', skill_level=9000)
Configuring an Ability Instance¶
Parameters of Abilities that are not yet started may be set either at
instantiation time, with the keyword arguments of the get_dependency
call or
by directly setting them as attributes of the object instance of that Ability.
Said otherwise, this line:
inst = self.get_dependency('mytest', skill_level=9000)
is equivalent to:
inst = self.get_dependency('mytest')
inst.skill_level = 9000
and this line:
inst = self.get_dependency('mytest', skill_level=9000, other_stat=10)
is equivalent to:
inst = self.get_dependency('mytest')
inst.set_opts(skill_level=9000, other_stat=10)
Note
If your parameter name contains characters that are invalid for a
Python attribute name, you may set it using set_opt
:
inst.set_opt('skill_level++', 9000)
Once started, trying to alter a parameter value leads to an Exception being raised.
Starting the Dependency¶
Once you have a reference to an object instance representing another Ability,
you may run it by calling the start
method on it:
class Ability(AbilityBase):
_dependencies = [('mytest', 'TestPackage', 'Test your might')]
def main(self):
instance = self.get_dependency('mytest')
instance.start()
If arguments are passed to the start invocation, they are passed as is to the
main
method of that Ability. For instance, let’s assume that the Test your
might Ability main
method is declared as:
def main(self, arg1, arg2=True, arg3="Mighty"):
One could call that Ability with arguments like this:
instance.start("arg1value", arg3="Weak")
Whether to use arguments with the start
method or using PacketWeaver
_option_list
parameters is up to the Ability developer. One case where using
the start
argument is convenient is when one want to pass a data type that
is not declared as a PacketWeaver option type, or when the value is an arbitrary
mutable Python object reference. In the latter case, a special argument should
be passed during start
invokation, to prevent deepcopy of the parameter
value:
instance.start({'mutable': 'array'}, deepcopy=False)
About Multi-threaded Abilities¶
All Abilities must inherit directly or indirectly from the AbilityBase
class.
Abilities inheriting directly from AbilityBase
are synchronous. That means
that when started, they take control over either the CLI or the calling Ability,
and they give control back, once they are done with their tasks.
Abilities may however inherit from ThreadedAbilityBase
instead of
AbilityBase
. In that case, PacketWeaver automatically generates a thread to
handle the tasks. That means that when started, these Abilities will execute a
separate control flow. An Ability inheriting from ThreadedAbilityBase
that
is run from the CLI executes until the main
method returns or an unhandled
exception bubbles up. It may however call the _wait
method, to wait for a
PacketWeaver stop signal. A stop signal is sent to a threaded Ability when the
ctrl+c control sequence is entered or when the stop
method is called from
the calling Ability.
For instance, let two threaded Abilities ABC and XYZ, defined as:
class Ability(ThreadedAbilityBase):
_info = AbilityInfo(name='ABC', type=AbilityType.STANDALONE)
_dependencies = [('xyz', 'MyPkg', 'XYZ')]
def main(self):
print "Getting instance of XYZ"
xyz_instance = self.get_dependency('xyz')
print "Starting XYZ"
xyz_instance.start()
print "Control is immediately given back here, because XYZ is threaded"
print "Let's now wait for the stop signal from a Ctrl+C"
self._wait()
print "Ctrl+C received, let's propagate the stop signal to our dependencies"
xyz_instance.stop()
print "Let's now wait for the dependency to terminate"
xyz_instance.join()
return 0
class Ability(ThreadedAbilityBase):
_info = AbilityInfo(name='XYZ', type=AbilityType.COMPONENT)
def main(self):
print "Started, let's wait for the stop signal"
self._wait()
print "Stop signal received. Let's add some delay"
time.sleep(10)
return 0
The ABC example Ability gets an instance of the XYZ Ability. It starts it, waits from a stop signal, propagates that stop signal to its instance of XYZ, waits for it to exit, and finally exits itself.
Note
The _wait
method is implemented using condition variables, so that
it puts the thread to sleep without having a busy loop to check for the stop
signal.
Note
To emulate ThreadedAbilityBase
subclasses, classes inheriting from
AbilityBase
also implements a stop
and a join
.
Warning
Ability developers should always call stop
and join
on
Abilities object that they get an instance of. Even though PacketWeaver
implements a sort of reaper that cleans up incorrectly handled
ThreadedAbilityBase subclasses, one should always clean after themself.
When developing an Ability that subclasses ThreadedAbilityBase
, the “parent”
Ability may send a stop signal at any moment. While it is possible to forcefully
terminate a thread in Python, PacketWeaver Abilities should be polite and
responsive to stop signals. As such, long-blocking syscalls should be avoided
and as well as infinite loops. One should regularly check if the signal stop was
sent by calling self.is_stopped()
, which returns True
if the current
Ability should exit as quickly as possible.
Obtaining Results¶
Ability main
method may return a value. When a standalone Ability run
from the interactive CLI returns a result, the string representation of this
value is printed on console.
When a standalone or a component Ability returns a value, the result
method
may be called after the join
method returns.
Let a component Ability be defined as:
import random
from packetweaver.core.ns import *
class Ability(AbilityBase):
_info = AbilityInfo(name='DoSmth', type=AbilityType.COMPONENT)
def main(self):
return random.randint(0, 10)
The returned value may be obtained this way:
from packetweaver.core.ns import *
class Ability(AbilityBase):
_info = AbilityInfo(name='main ability')
_dependencies = [('smth', 'demo', 'DoSmth')]
def main(self):
inst = self.get_dependency('smth')
inst.start()
inst.stop()
inst.join()
# Now that join returned, it is safe to call result()
self._view.success(inst.result())
Starting, Waiting and Stopping Multiple Abilities¶
A helper method exists if you need to start a bunch of Abilities object, wait for the stop signal, then propagate that stop signal to all those abilities.
This helper, called _start_wait_and_stop
, is a method of any AbilityBase
subclass instance. It receives a list of AbilityBase
subclass instances:
inst1 = self.get_dependency('example', port=8080)
inst2 = self.get_dependency('example', port=8081)
self._start_wait_and_stop([inst1, inst2)
If more flexibility is needed, a _start_many
and a _stop_many
method are
also available.
On the use of third-party libraries¶
Simple Abilities are self-contained and rely on the standard Python library. You may, however, need to write some that import third-party libraries and these third-party libraries may not be installed on every system.
The try and forgive approach of Python means the Python module containing your Ability must try to import the third-party libraries and an exception will be raised if a library is unavailable. While we could live with an ImportError exception bubbling up and killing PacketWeaver, we found that this is suboptimal and not very user-friendly.
The traditional way of handling this situation in PacketWeaver is to try to
import the library, and set a boolean to True
on success and False
on
failure:
try:
import third_party_lib
HAS_THIRD_PARTY_LIB = True
except ImportError:
HAS_THIRD_PARTY_LIB = False
This boolean may then be used in a special PacketWeaver class method called
check_preconditions
. This class method purpose is to check for the
availability of all prerequisites for the current Ability to work. If something
is missing, this method must return a list of strings explaining in a
user-friendly way, what is broken and what needs fixing. If all preconditions
are met, an empty list must be returned. This list is notably used by the
interactive CLI to display Abilities that cannot be run in red to indicate that
some requirements are unmet.
Here follows an example of such a check_preconditions
class method:
class Ability(...):
@classmethod
def check_preconditions(cls, module_factory):
l = []
if not HAS_THIRD_PARTY_LIB:
l.append('Third party library XYZ support missing or broken.')
l += super(Ability, cls).check_preconditions(module_factory)
return l
As you can see, in this example, the class method does what is needed regarding
the current Ability, and then calls the super class method. This super class
method will work recursively across all nested Abilities that your Ability may
depend on. Thus, if any Ability that your current Ability relies on has a
missing dependency, the appropriate error messages will be displayed. It is
strongly advised to always perform this super call when you override
check_preconditions
.
Flow-based Programming and Ability writing¶
Abilities might be run in parallel, using ThreadedAbilityBase
subclasses.
Yet, they run unbeknownst to each other, save for the parent Ability that know
them all.
PacketWeaver offers syntactic sugars for you to write Abilities that communicate with each others, improving on the classic shell pipe syntax.
Doing it all by hand¶
You may add by hand input and output pipes to any ThreadedAbilityBase subclass
instance. These pipes are multiprocessing.Pipe
instances. As such, adding
such pipes may be done like this:
inst = self.get_dependency('example')
outputp, inputp = multiprocessing.Pipe()
outputp2, inputp2 = multiprocessing.Pipe()
inst.add_in_pipe(inputp)
inst.add_out_pipe(outputp2)
self._start_wait_and_stop([inst])
Once set up like this, the parent Ability may write into outputp
and read
from inputp2
.
Warning
you cannot add pipes to an already started Ability hitherto.
Reading and writing from an Ability¶
Reading¶
For the point of view of the Ability refered to by the pet name example, input
data may be read from the standard input by calling the built-in _recv()
instance method:
def main(self):
p = self._recv()
The _recv
method consumes the input data as datagrams, not streams.
Moreover, you may receive all types of pickable data, which means that in the
previous example, p
might be a full-fledged Python object!
Warning
the _recv
method is blocking, kernel-wise. This is a problem
because well-written Abilities must keep aware of the stop signal. For this
reason, the standard way of reading the standard input of an Ability is to
write a code similar to this one:
def main(self):
try:
while not self._is_stopped():
if self._poll(0.1):
p = self._recv()
# Do something with p
except (EOFError, IOError):
pass
The _poll
method is similar to the Kernel poll syscall. It monitors whether
there is a datagram to be read on the standard input, and it times out after a
certain delay (100 miliseconds in the previous example). The previous example
code thus ensures that this Ability checks at least every 100 ms that a stop
signal was received.
Writing¶
Writing to the standard output of an Ability is relatively simple. You may
simply call the _send
method with any Pickable Python object as argument:
def main(self):
self._send('abc')
Caution
_send
might be blocking at times, which is in violations of the
code of conduct of well-written Abilities… This is a known limitation.
Using the Pipe Syntactic Sugar¶
Whenever an Ability purpose is to orchestrate multiple Abilities, it owns the references to multiple Abilities objects:
inst1 = self.get_dependency('example1')
inst2 = self.get_dependency('example2')
inst3 = self.get_dependency('example3')
Configuring the pipes between these Abilities by hand might be cumbersome. For this reason, PacketWeaver ships a syntactic sugar similar to shell pipes.
To pipe the standard output of the first Ability to the standard input of the second Ability, one can write the following Python expression:
inst1 | inst2
We use the fluent design pattern, so that one may write:
inst1 | inst2 | inst3 | inst1
Note
In the previous example, inst1
is listed twice into the pipeline.
This is the same instance of the same Ability. This line means that the
standard output of inst1
is piped into inst2
, and that the standard
output of inst3
is piped into the standard input of inst1
. This
enables developers to write pipelines of Abilities that are cyclic in a
much easier way that it is generally possible in shell (e.g. netcat
pipelines with named FIFOs)
Multiple Inputs and Outputs¶
While in shell, it is possible to pipe multiple scripts output into a script standard input, it requires some tricks, for instance using named FIFOs. With Packetweaver, multiple inputs and multiple outputs are seemless:
def main(self):
inst1 = self.get_dependency('example1')
inst2 = self.get_dependency('example2')
inst3 = self.get_dependency('example3')
inst4 = self.get_dependency('example4')
inst5 = self.get_dependency('example5')
inst1 | inst2 | inst3
inst4 | inst2 | inst5
self._start_wait_and_stop([inst1, inst2, inst3, inst4, inst5])
In the previous example, inst2
is part of two pipeline declarations. However
Packetweaver interprets this as: inst2
standard input is composed of a
round robin read from inst1
and inst4
, and inst2
standard output
is broadcast to inst3
and inst5
.
On Detecting Source and Sink Conditions¶
An Ability may dynamically discover if there are other Abilities that are piped
to its standard input by calling self._is_source()
. If True
, this
Ability has currently no input pipes.
Similarly, an Ability may discover if other Abilities subscribed to its standard
output by calling self._is_sink()
.
Transfering Pipes¶
Sometimes, the sole purpose of some component Abilities is to instanciate other
component Abilities and set up the pipelines. If that orchestrating Ability has
standard input and standard output pipes, it would be cumbersome to transfer by
hand all input and output messages to the pipeline. For this reason,
Packetweaver enables an Ability developer to specify that all input or output
pipes are to be transfered from the current Ability to another Ability. This is
performed using the self._transfer_in(otherAbilityInstance)
and
self._transfer_out(otherAbilityInstance)
methods.
The following Ability sets up such a pipeline:
def main(self):
inst1 = self.get_dependency('example1')
inst2 = self.get_dependency('example2')
inst3 = self.get_dependency('example3')
inst1 | inst2 | inst3
self._transfer_in(inst1)
self._transfer_out(inst3)
self._start_wait_and_stop([inst1, inst2, inst3])
Contribute¶
Documentation¶
All the documentation is available in the packetweaver/doc folder. Written in ReStructuredText, you may build it locally using Sphinx.
If you wish to contribute to the documentation, either by editing the Python docstring in the source code or the documentation pages themselves, this a convenient way to visualize what your modifications will look like.
Documentation toolset installation¶
To install the documentation toolset, you may run the following commands:
# pip3 install sphinx pylint
# apt install graphviz
Sphinx is the main library that builds the documentation, and pylint is used for its library pyreverse that draw UML graphs from Python source code. Graphviz is installed to give pyreverse access to the png export format.
If you want to use the ReadTheDocs theme, you may install it using:
pip3 install sphinx_rtd_theme
It will be automatically used by the Sphinx configuration file conf.py.
The different actions to build the documentation are part of the Sphinx Makefile.
To use it, you may browse to the documentation folder (cd packetweaver/doc/
)
and use one of the make <target>
commands.
Warning
As some Makefile targets use relative paths, please always use it while being in the same directory.
Building the API documentation¶
Sphinx apidoc is used to build a documentation of the source code files using their docstring content. To generate the corresponding RestructuredText documentation files, you may run:
make apidoc
It will automatically delete the previous built file and generate new ones from scratch.
Building the complete documentation¶
To build only the basic documentation content and view it with your default web browser, you may use:
make html
make show
html is a Sphinx target that specifies an output format. You may use other options such as the single page html version of it with:
make singlehtml
xdg-open build/singlehtml/index.html
Building UML diagrams¶
You may build a UML representation of PacketWeaver core module using pyreverse. Similarly, the Makefile target is:
make uml-core
xdg-open build/classes_pw_core.png
xdg-open build/packages_pw_core.png
Building and cleaning everything¶
Finally, the all target performs all the previous tasks:
make clean
make all
make show
The clean target will clear everything except the files generated by the apidoc target, which lives in the source/ folder. To fully clean the doc/ folder, you could run:
make clean
rm source/apidoc/*
Blog and changelog¶
Release v0.3¶
This release is identified by the “v0.3” git tag.
Main changes: * Python3.6+ is the only version supported by the framework * Ubuntu 18.04 is taken as the reference GNU/Linux distribution for testing * Vi is now the default editor in pw.ini * Tox is used to run unit tests and pep8 compliance tests
New features: * A Dockerfile has been added * “ls” can be used as an alias of the “option” command
API evolution: * A basic logging system has been added to track the framework internal activity * A _start_many() and _stop_many() methods has been added to Ability class * A “get_local_net_conf” method is available to get the local machine network configuration
Release v0.2¶
This release is identified by the “v0.2” git tag.
New features: * Paths in interactive shell and abilities are now relative to the ability package * Add of generic example abilities * New commands at the ability selection CLI:
- “conf” can be used to edit on place the pw.ini
- “editor” can display source code of abilities, even if they cannot be selected (missing dependency, coding mistakes)
API evolution: * Add possibility to set several parameters at once when configuring nested abilities * Text coloring API can be call by log severity name (‘success’, ‘error’…)
Fixes and minor improvements: * Display warnings for invalid configuration when:
- a package does not provide a proper “exported_ablities” module
- if two abilities have the same name
- pw.ini parameters are invalid
- Enhance the interactive CLI “info” command by displaying references/authors information in list
- Display stack trace when an ability crashes due to an non identified error
- Silent exit of the framework
Indices and tables¶
The following links provide you with a set of tools to browse the source code documentation.