Native modules developer's guide


Table of Contents

License
Acknowledgements
Native modules developer's guide
Preface
Hello, world!
Class declaration
Command description
Parameters description
Class methods
Creation of a command hierarchy
Nested commands on the file system
Nested commands, described in one class
Command “acquisition
Template commands
Module structure reference
Immediate command execution
Deferred command execution
Debugging

List of Examples

1. Module hello.py, strings are numbered
2. Nested commands on the file system
3. The listing of hello.py
4. The listing of sample.py
5. The Connexion core start
6. A client session
7. The listing of network.py
8. Running the core with the base dictionary
9. A session using network.py
10. A command acquisition sample
11. Session example
12. Class factory in the system.py module (ncsh modules)

License

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].

Acknowledgements

  • Zalewsky Denis — documentation proofreading.

Native modules developer's guide

Preface

This document implies that the developer has initial knowledge of Python programming language and understanding of object-oriented programming.

Hello, world!

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
				

Class declaration

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

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 description

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.

Class methods

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")

Creation of a command hierarchy

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.

Nested commands on the file system

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.

Example 2. Nested commands on the file system

$ tree
 .
 |--hello
 |  `--sample.py
 `--hello.py

				  

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

#
				  

Example 6. A client session

$ connexion-cli
xshell > hello sample
hello from sample module!

				  

Nested commands, described in one class

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.

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

 				  

Template commands

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,))


				  

Module structure reference

TODO

Immediate command execution

TODO

Deferred command execution

TODO

Debugging

TODO