Table of Contents
List of Examples
hello.py
, strings are numberedhello.py
sample.py
network.py
network.py
system.py
module (ncsh modules)Copyright (c) 2007 Connexion project, Peter V. Saveliev.
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license can be found on the GNU site[1].
This document implies that the developer has initial knowledge of Python programming language and understanding of object-oriented programming.
The Connexion core works with a tree of commands but modules where commands are
defined can be represented with several hierarchies in a file system. To plug a
module one should use -w
and/or -W
parameters in the format name:path
, e.g.
connexion -x -w commands:../modules -W base:../basedict.
It is valid to specify several hierarchies, one by one. The first hierarchy in the list
will be the main one: its name will be returned to a client interface as a command prompt.
Modules defined by parameter -W
are privileged and they gain
access to some Connexion internal data structures and can control Connexion operations.
As well, one can start a Connexion instance with -w
pointing to
a development module hierarchy. It is possible to start several Connexion instances (see
-s
— different Connexion instances must work with different sockets)
and use them independently. In the simplest case, a command should be defined in a separate
file. We'll begin with traditional hello command. Lets create a new working
directory, e.g. ~/test/
, and file called hello.py
in
this directory.
Example 1. Module hello.py
, strings are numbered
1 from command import command,swrapper 2 3 class hello(command): 4 ''' 5 Description 6 ''' 7 parameters = { 8 "$1": { 9 "description": "a parameter description: what to 'hello'?", 10 "type": "string", 11 "name": "hello", 12 }, 13 } 14 15 16 def postUp(self,opts): 17 s = swrapper() 18 s.add_command("message","Hello, %s!\n" % (opts.envl.hello)) 19 return s
Line 1: two classes is imported. Class command
are mandatory in all
modules. Any command must directly or indirectly inherit from this class (see line 3).
To define a command the module must contain a class with the same name as file
(without .py
suffix), or class Default
otherwise.
Command description must be done in a “docstring” or in doc
attribute
(as a string), e.g. doc = "Description string"
. At the moment, it excludes simple
i10n and thus will be fixed in further releases.
Parameters are described in parameters
attribute in a dict form (see
program listing, line 7). The dictionary keys are parameters names, values — descriptions
of each parameter (also dicts). E.g., if the parameters
dict contains
two keys — “src” and “dst” — then the Connexion interpreter
will expect an input string like that: command src x dst y. Such parameters
are becalled “named”. The order of named parameters in a command string is
unimportant.
The behaviour of positional parameters is completely different. To describe a positional,
one should use names like $x
, where x — position number. Such parameters
must reside in predefined positions in the command string. E.g., in the listing (line 8)
one positional ($1
) is described. The interpreter will expect it in
the first position in the command string.
In the parameter description following fields are used:
description
Parameter description, it'll be used in the autocompletion list in cli and can be used as a tip in gui or web. The same note about i10n is valid for this description as well as for the command description.
type
Parameter type. This type will be checked by the Connexion core.
There is no way to define custom types yet. The list of available
types is in state/syntax.py
file.
The string
type is special. 'Cause there is
no way to determine string borders in the Connexion core, thus
the value of a string parameter is a substring from the current position
and till the end of command string.
name
The base class, command
, beside of other stuff, fills
the opts
structure, in particular, opts.envl
,
where local variables are stored. If you want a positional to have a normal
name (such as “hello”), then you are to define name
field.
The example uses only one method, postUp()
; but with it one
can review the work of all methods of command
class, that are
available for overloading. The difference between the methods is in the time
when the Connexion core calls them.
The listing contains main entities that a developer will work with: input data
(opts
structure) and a result of a method (an instance of
swrapper
class). The opts
structure
contains many fields, that will be described below. For the example understanding
only one field is important now, opts.envl
. This field contains
local variables that are gathered after parameters
description.
Because we defined a name from the first positional (hello
), thus
it will be available as opts.envl.hello
.
An instance of swrapper
class contains an array of service commands.
Each array partition has a form of ("service name","command")
. One can
setup the array with swrapper
methods: swrapper.add_command(service_name,command)
and swrapper.add_commands(service_name,[command,command,...])
.
The first command adds command
for service service_name
,
whilst the latter adds several commands in one call.
Some of available services:
message
A message to a user, will be printed by a use interface. An
example: s.add_command("message","Hey, chief, I see ya!\n")
exec
Exec a command and redirect it's output into an interface. An
example: s.add_command("exec","/sbin/ip route show")
system
Execute a command in a subshell and redirect it's output into an
interface. The main difference from the previous service is that
the command will be executed not in a fork-and-exec way, but with
system() call. This will be slower than exec, but there will be
available shell redirections. An example:
s.add_command("system","ps aux | grep ^root")
At the beginning, two words about terminology. The “command” is
a command as it thinks the Connexion core. Each command results either in
an immediate effect (ip ping ya.ru
) or in an internal
tree node. The “module” is a file where the “command”
is defined as a class. Thus, the hello.py
from the example
above is a module that implements command hello.
The Connexion commands form an internal tree with one root. The tree root is
hidden and one should not specify it explicitly. A command can be a tree node
or a tree leaf. A command can become a node only if it's mask contains bit
flags.Begin
.
This part of the guide describes different ways of command implementations and building modules hierarchies. Hierarchies are useful for two tasks. The first is that it helps to an interface to aggregate commands in autocompletion listings. The second task is to allow code reuse.
The simplest way to create nested commands can be illustrated
with the hiearchy listing. All modules that resides in the
hello
directory will be automatically
treated as hello command descendants.
But one should help the Connexion core to work with this hierarchy. The core
assumes that a command forms a node only when there is a bit Begin
in the command mask (see an example below).
The second bit, Bypass
, is essential to run nested
commands immediately, e.g. hello sample
Example 3. The listing of hello.py
from command import command
from utils import flags
class hello (command):
"""
Test hello module
"""
mask = flags.Begin | flags.Bypass
The sample.py
module content is quite trivial,
there is nothing new relative to the described above. The only difference
is that sample.py
is a nested module.
Example 4. The listing of sample.py
from command import command,swrapper class sample (command): """ Test sub-module """ def postUp(self,opts): s = swrapper() s.add_command("message","hello from sample module!\n") return s
So, what we have got so far:
Example 5. The Connexion core start
$ connexion -x -w xshell:~/test
Debug shell started. Autocomplete does not work. See 'help' for details
Executable file was started from ./connexion.py
#
We'll continue with a simple but useful example: a network setup. The second way to describe nested commands is to define them as nested classes:
Example 7. The listing of network.py
from command import command,swrapper from utils import flags class network (command): """ A network configuration """ mask = flags.Begin modules = [ "address", ] class address (command): """ Configure a network address """ parameters = { "$1": { "type": "ipaddr", "description": "An IP address", "name": "address", } } def postUp(self,opts): s = swrapper() s.add_command("message", "configure an IP address %s\n" % (opts.envl.address) ) return s
Pay attention to the modules
list in the network
class.
It contains which nested classes must be treated as nested commands.
Also, as in the hello sample example above, class netowrk
has Begin
flag. Nested class, address
, defined in
the same way as it's parent: there is one parameter with type “ipaddr” and
name “address”. Thus, it will be available in the class methods
as opts.envl.address
.
But the network
class does not have Bypass
flag,
and it means that it is the time to become acquainted with deferred execution of
Connexion commands. Deferred execution is provided to help operators to review
changes before actually applying them. One should use commit command
to apply all pending changes or abort to refuse them. The tree
prints out the current state of the Connexion commands tree and transaction
command prints pending changes. All these commands are also defined in respective modules,
and these modules are part of the Connexion base dictionary. So, this is the our new
example session.
Example 8. Running the core with the base dictionary
$ connexion -x -w xshell:~/test -W internal:/usr/share/connexion-modules/basedict/
Debug shell started. Autocomplete does not work. See 'help' for details
Executable file was started from ./connexion.py
#
Example 9. A session using network.py
$ connexion-cli xshell > network network > address 10.0.0.1/24 network > commit configure an IP address 10.0.0.1/24 network > tree ! network address 10.0.0.1/24
It should be noticed that it is up to the developer, whether to place nested modules in separate files or in one file. The difference between this two methods is in the way of command acquisition. So, lets continue with command acquisition.
It is a frequent case, when several object needs similar setup, but each has an unique characteristic. For example, network interfaces. Many of them can have an IP address, some can be used for traffic shaping. On ethernet devices, one can force level 2 protocol (10/100/1000Mbit), on WiFi — encryption algorithms. And nevertheless, all these objects are network interfaces.
The simplest way is to use object-oriented style, when there is one common class, say,
aNetworkInterface
, and all network interface commands inherit this class. The different way
is to define all possible network interface methods and parameters in a library, and
compose it later for each network interface separately. The Connexion permits both
ways. Here is an example of simple commands acquisition. Take a notice of the export
list
of network
class and the acquire
list of interface
class.
Example 10. A command acquisition sample
from command import command,swrapper from utils import flags from string import Template class network (command): """ A network configuration """ mask = flags.Begin modules = [ "interface", ] export = [ "address", ] class interface (command): """ Configure an ethernet interface """ mask = flags.Begin parameters = { "$1": { "type": "interface", "description": "Interface number", "name": "interface", "format": Template('eth$value') } } acquire = [ "address", ] class address (command): """ Configure a network address """ parameters = { "$1": { "type": "ipaddr", "description": "An IP address", "name": "address", } } def postUp(self,opts): s = swrapper() s.add_command("message", "configure an IP address %s at %s\n" % (opts.envl.address, opts.envl.interface) ) return s
Example 11. Session example
$ connexion-cli xshell > network interface 0 interface 0 > address 10.0.0.1/24 interface 0 > commit configure an IP address 10.0.0.1/24 at eth0 interface 0 > tree ! network ! interface 0 address 10.0.0.1/24
Sometimes it is easier to create commands “on-demand”, than
to define them in the sources. E.g., it is so for sysctl
commands, and here is an example of metaclass programming in terms of
Connexion environment. For class creation, one should have a container class (to
attach children to), a template class and a serialisation function. A container
class will create children from a templte and will aggregate produced classes.
A template class will work as a mould for the classes. A
serialisation function will return a list of dictionaries.
Each list position will define a new class.
Example 12. Class factory in the system.py
module (ncsh modules)
from command import command,swrapper from utils import flags from utils.utils import Executor from utils.exceptions import Dump from copy import copy ### # This variable stores sysctl commands, it is # a cache. ### svariants = [] def serialize(): global svariants if not svariants: ### # Executor — a class from Connexion utilities. # It executes a command and stores the output in .data # (single string) and .lines (lines list) ### e = Executor("/sbin/sysctl -N -a") svariants = copy(e.lines) result = [] for i in svariants: i = i.strip() ### # The r dictionary will describe, what and how to # change in the template class. Here we change only # name attribute. ### r = {} r["name"] = i result.append(r) ### # So, finally we'll get a list: # [ # {"name": "..."}, # {"name": "..."}, # ... # ] ### return result class sysctl(command): """ A template class for sysctl commands """ ### # Each command will be unique. ### mask = flags.Unique parameters = { "$1": { "description": "a sysctl variable value", "type": "string", "name": "value", } } def postUp(self,opts): s = swrapper() s.add_command("exec", "/sbin/sysctl -w %s=%s" % (self.name, opts["envl"]["value"]) ) return s class system (command): """ Configure system parameters """ mask = flags.Begin ### # This one is the container class. To create a class factory, the # class should have a "template" attribute. It is a tuple: # ( # serialisation function, # ( parent classes list ) # ) ### template = (serialize,(sysctl,))