Руководство разрaботчика нативных модулей


Содержание

Руководство разработчика нативных модулей
License
Предисловие
Hello, world!
Объявление класса
Описание команды
Описание праметров
Методы класса
Создание иерархии команд
Вложенные команды на файловой системе
Вложенные команды в одном классе
Заимствование команд
Команды, созданные по шаблону
Полное описание структур модуля
Немедленное исполнение модулей
Отложенное исполнение модулей
Механизмы отладки

Список примеров

1. Модуль hello.py, строки пронумерованы
2. Вложенные команды на файловой системе
3. Листинг модуля hello.py
4. Листинг модуля sample.py
5. Запуск ядра Connexion
6. Сессия клиента
7. Листинг модуля network.py
8. Запуск ядра с базовым словарём
9. Сессия с использованием network.py
10. Модуль network.py
11. Пример сессии
12. Фабрика классов в модуле system.py (из набора ncsh)

Руководство разработчика нативных модулей

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

Предисловие

Этот документ подразумевает, что разработчик имеет начальные знания языка Python и общее представление о концепции объектно-ориентированного программирования.

Hello, world!

Ядро Connexion работает с одним деревом модулей, однако сами модули могут быть предоставлены несколькими иерархиями на файловой системе. Для того, чтобы подключить модули к Connexion, необходимо использовать параметры -w или -W в формате имя:путь, например, connexion -x -w commands:../modules -W base:../basedict. Можно указывать несколько иерархий, одну за другой. Иерархия, которая задана в параметрах первой, будет основной: поиск модулей будет осуществляться сначала в ней и её имя будет выдано клиенту в качестве строки для приглашения при старте сессии. Модули иерархий, заданных через параметр -W, будут являться привилегированными в том смысле, что получат доступ к некоторым внутренним структурам ядра Connexion и смогут управлять его работой.

Точно также экземпляр Connexion можно запустить с параметром -w, указывающим на некоторую иерархию разрабатываемых модулей. Это позволяет отлаживать и использвать отдельные наборы модулей независимо. В простейшем случае, команда должна быть описана в отдельном файле. Начнём, как принято, с команды hello. Для этого создадим новый рабочий каталог, например, ~/test/, а в нём — файл hello.py следующего содержания:

Пример 1. Модуль hello.py, строки пронумерованы

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
				

Объявление класса

Строка 1 импортирует два класса. Класс command обязателен для импорта во всех модулях. Именно ему должна наследовать любая команда, прямо или косвенно (см. строку 3). Для объявления команды модуль должен нести в себе класс, названный по имени файла (без суффикса .py), либо с названием Default.

Описание команды

Описание команды может быть дано в т.н. «docstring» или в атрибуте doc (в виде строки), например, doc = "Description". На данный момент это исключает нормальную интернационализацию и в дальнейшем будет исправлено.

Описание праметров

Описание параметров даётся в атрибуте parameters в виде словаря (листинг, строка 7). Ключами словаря являются имена параметров команды, значениями — словари описаний отдельных параметров. Например, если в таком словаре описаны параметры с именами «src» и «dst», то интерпретатор будет ожидать строку вида command src x dst y. Такие параметры называются именованными. Порядок следования именованных параметров не важен.

Совсем иначе ведут себя позиционные параметры. Для описания позиционных параметров нужно использовать специальные имена вида $x, где x — номер позиции. Такие параметры должны находиться в командной строке в определённой позиции. Например, в листинге (строка 8) описана один позиционный параметр ($1). Интерпретатор будет ожидать его в первой позиции в командной строке.

В описании параметра в листинге использованы следующие поля:

description

Описание параметра, оно будет использовано при отображении автодополнений в интерфейсе командной строки и может быть использовано как подсказка в графическом или веб-интерфейсах. То же замечание про интернационализацию, что и для описания класса в целом.

type

Тип параметра. Проверкой соответствия введённого значения указанному типу занимается ядро системы. Определение собственных типов на данный момент не допускается. Список уже определённых типов см. в коде ядра в файле state/syntax.py.

Особым типом является тип string. Так как границы строки не определены, то значением строкового параметра считается вся командная строка от текущего положения и до конца.

name

Базовый класс command занимается, помимо прочего, заполнением некоторых полей структуры opts, в частности, opts.envl, отвечающей за хранение и передачу локальных переменных. Если вы хотите, чтобы переменная, созданная из позиционного параметра, имела обычное имя (вроде того же «hello»), то нужно указать поле name.

Методы класса

В примере использован всего один метод, postUp(); но на его примере можно рассмотреть работу всех доступных для перегрузки методов класса command. Отличия между разными методами, как будет описано ниже, заключается лишь в том, когда ядро их вызывает.

В листинге показаны основные сущности, с которыми разработчику придётся иметь дело: входные параметры (структура opts) и результат, возвращаемый методом класса (экземпляр swrapper()). Структура opts несёт множество полей, которые будут описаны ниже. Для понимания примера важно пока только одно поле, opts.envl. Это поле несёт в себе значения всех локальных переменных, автоматически собранных из параметров, переданных команде. Поскольку мы объявили имя для позиционного параметра (hello), то его значение будет нам доступно через opts.envl.hello.

Экземпляр swrapper() несёт в себе внутренний массив команд сервисам. Каждая ячейка массива имеет формат ("имя сервиса","команда"). Управлять массивом можно через методы swrapper.add_command(service_name,command) и swrapper.add_commands(service_name,[command,command,...]). Соответственно, первый добавляет команду command сервису service_name, а второй добавляет несколько команд (что может быть лаконичнее, чем использовать циклы).

Вот некоторые из доступных сервисов:

message

Сообщение пользователю, будет выведено в интерфейс. Пример: s.add_command("message","Ой, шеф, а я Вас вижу!\n")

exec

Выполнить команду, вывод перенаправить в интерфейс. Пример: s.add_command("exec","/sbin/ip route show")

system

Выполнить команду в shell, вывод перенаправить в интерфейс. От предыдущего отличается тем, что команда будет выполнена не через fork-and-exec, а в shell, и, соответственно, будут доступны механизмы shell вроде перенаправлений, конвейеров и т.п. Пример: s.add_command("system","ps aux | grep ^root")

Создание иерархии команд

Вначале два слова про терминологию. «Команда» — это команда в понимании ядра системы. «Модуль» — это файл, в котором определён класс, реализующий команду или набор команд. Таким образом, рассмотренный в примере файл hello.py — это модуль, который реализует команду hello. Команды Connexion всегда образуют иерархию с одним корнем, этот корень скрыт от пользователя и явно указывать его не нужно. Команды могут образовывать «узлы» дерева и его «листья». Команда образует узел в том случае, если она несёт в себе дочерние команды, команды-потомки. Если команда не может иметь потомков, она может образовывать только листья. Термин «потомки» применительно к командам не имеет ничего общего с терминологией наследования в ООП, это всего лишь показатель относительного положения на дереве.

В этом разделе речь пойдёт про разные способы реализации команд в модулях и составления иерархий. Иерархии нужны для решения двух задач. Первая задача относится к пользовательскому интерфейсу, это всего лишь агрегация команд в отдельные ветви дерева. Эта задача сама по себе простая и не требует никакой дополнительной логики исполнения, кроме того, что вызов некоторых команд влечёт за собой переход по узлам дерева и, соответственно, изменение списка доступных команд.

Вторая задача — это комбинирование различных параметров для одного узла. Например, можно однажды описать модуль обработки адреса на интерфейсе, а затем использовать на всех типах интерфейсов, сокращая время и силы на разработку.

Вложенные команды на файловой системе

Самый простой способ создания вложенных команд можно продемонстрировать на примере листинга директории с тестовыми модулями. Все модули, помещённые в директорию hello будут автоматически помещены в иерархию и их команды будут считаться потомками команды hello.

Пример 2. Вложенные команды на файловой системе

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

				  

Чтобы автоматика отработала, однако, надо приложить усилия. Ядро считает, что команда образует узел только в том случае, если его маска несёт битовый флаг с названием Begin, что видно на примере модуля hello.py. Второй флаг (Bypass) нам необходим, так как мы ещё не освоили перемещения по дереву модулей; он поволит нам запускать ветвь команд hello sample как одну команду.

Пример 3. Листинг модуля hello.py

from command import command
from utils import flags

class hello (command):
	"""
	Test hello module
	"""
	mask = flags.Begin | flags.Bypass

				  

Содержимое модуля sample.py совершенно обычное, ничего нового относительно уже описанного ранее. Единственное отличие лишь в положении модуля: теперь команда является потомку другого узла.

Пример 4. Листинг модуля 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
				  

Итак, что у нас получилось:

Пример 5. Запуск ядра Connexion

$ connexion -x -w xshell:~/test

Debug shell started. Autocomplete does not work. See 'help' for details
Executable file was started from ./connexion.py

#
				  

Пример 6. Сессия клиента

$ connexio
xshell > hello sample
hello from sample module!

				  

Вложенные команды в одном классе

Продолжим мы на простом, но более жизненном примере: простейшая настройка сети. Вторая возможность определять вложенные команды — это описать их в пределах одного класса:

Пример 7. Листинг модуля 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
 				  

Обратите внимание на список modules класса network. В нём описано, какие дочерние классы необходимо использовать как отдельные модули для команд-потомков. Также, как в пример hello sample, класс network несёт флаг Begin, свидетельствующий, что это узел. Дочерний класс, address, построен по тому же принципу, что и самы первый пример: объявлен один параметр команды, тип параметра — «ipaddr», имя параметра «address». Под этим именем он и будет доступен в структуре opts.envl, т.е. opts.envl.address.

Однако, класс network не несёт флага Bypass, а это значит, что пришла пора ознакомиться с отложенным выполнением команд в Connexion. Для того, чтобы можно было за один проход обрабатывать большие деревья, описывающие множество параметров, предусмотрено «отложенное выполнение команд»: когда команда, введённая оператором, не интерпретируется сразу (как было в hello sample), и лишь по команде commit всё дерево введённых команд отправляется на интерпретацию. Проверить список команд, ожидающих выполнения, можно по команде transaction. Дерево выполненных команд показывает команда tree. Все эти служебные команды также являются модулями, и составляют «базовый словарь» Connexion. Без них ядро Connexion полезно только в качестве простого интерпретатора одноуровневой иерархии модулей. Итак, сессия нашего нового примера.

Пример 8. Запуск ядра с базовым словарём

$ 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

# 				  

Пример 9. Сессия с использованием 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
 				  

Надо заметить, что как размещать модули: в отдельных ли файлах или в одном — решать разработчику. Различие этих способов важно только в случае заимствования (acquisition) команд, к рассмотрению которого мы и приступим.

«Заимствование» команд

Достаточно часто встречаются иерархии, где несколько объектов частично требуют одинаковой настройки, но при этом каждый отличается от другого чем-либо. Типичный пример — это сетевые интерфейсы. Многие из них могут нести IP-адреса, некоторые могут быть использованы для шейпинга траффика, на отдельных интерфейсах можно устанавливать скорость передачи аппаратного контроллера, на других нужно применять алгоритмы шифрации беспроводного траффика. Все они при этом являются сетевыми интерфейсами.

Очевидным путём может являться подход ООП, когда для интерфейсов описывается общий класс-предок, от которого ведётся наследование. Другим путём является описание возможных команд-потомков в узле network, если мы решим, что узел interface будет его потомком, а затем заимствование команд разными типами интерфейсами по мере необходимости. Возможно и совмещение этих вариантов, как и реализовано в модулях ncsh. Здесь мы покажем только простую иллюстрацию заимствования команд. Обратите внимание на список export класса network и список acquire класса interface.

Пример 10. Модуль network.py

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

 				  

Пример 11. Пример сессии

$ 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

 				  

Команды, созданные по шаблону

Иногда бывает проще создавать команды «на лету», чем описывать их в исходниках. Например, это справедливо для команд sysctl, и на это примере мы сейчас и разберём коцепцию метаклассов применительно к Connexion. Для того, чтобы создавать классы, нам потребуется класс-родитель (в терминах дерева, не ООП), класс-шаблон (или несколько) и функция-сериализатор. Класс-родитель будет выполнять функции агрегации, а также именно он будет отвечать за создание потмков из шаблона. Класс-шаблон будет служить основой для потомков. А функция-сериализатор должна будет вернуть список словарей. Каждая позиция списка создаст своего потомка, причём словарь послужит картой: какие атрибуты класса-шаблона мы должны будем заменить и как.

Пример 12. Фабрика классов в модуле system.py (из набора ncsh)

from command import command,swrapper
from utils import flags
from utils.utils import Executor
from utils.exceptions import Dump
from copy import copy

###
# Эта переменная будет хранить список вариантов sysctl,
# чтобы не получать его каждый раз заново
###
svariants = []

def serialize():
	global svariants
	if not svariants:
		###
		# Executor — класс из утилит Connexion,
		# выполняет команду и хранит её вывод в атрибутах .line
		# (одной строкой) и .lines (списком строк)
		###
		e = Executor("/sbin/sysctl -N -a")
		svariants = copy(e.lines)
	result = []
	for i in svariants:
		i = i.strip()
		###
		# Этот словарь, r, и будет описывать, что и как нужно
		# поменять в шаблонном классе. Здесь мы меняем только
		# атрибут name.
		###
		r = {}
		r["name"] = i
		result.append(r)
	###
	# То есть, итоговый список будет таков:
	# [
	#	{"name": "..."},
	#	{"name": "..."},
	#	...
	# ]
	###
	return result

class sysctl(command):
	"""
	A template class for sysctl commands
	"""
	###
	# Каждая команда будет уникальна, ведь у нас не может быть двух
	# net.ipv4.ip_forward :)
	###
	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
	###
	# Это и есть класс-предок. Для того, чтобы создать фабрику классов,
	# он должен нести атрибут template, список, состоящий из двух позиций:
	# (
	#	функция-сериализатор,
	#	( список классов-предков )
	# )
	###
	template = (serialize,(sysctl,))


				  

Полное описание структур модуля

TODO

Немедленное исполнение модулей

TODO

Отложенное исполнение модулей

TODO

Механизмы отладки

TODO