From b26e1428638a309acb7a3744c831082e32085d39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Schuberg?= Date: Sat, 14 Feb 2015 12:54:31 +0100 Subject: vici: Add vici python protocol handler --- src/libcharon/plugins/vici/python/.gitignore | 1 + src/libcharon/plugins/vici/python/vici/__init__.py | 0 .../plugins/vici/python/vici/exception.py | 4 + src/libcharon/plugins/vici/python/vici/protocol.py | 194 +++++++++++++++++++++ 4 files changed, 199 insertions(+) create mode 100644 src/libcharon/plugins/vici/python/.gitignore create mode 100644 src/libcharon/plugins/vici/python/vici/__init__.py create mode 100644 src/libcharon/plugins/vici/python/vici/exception.py create mode 100644 src/libcharon/plugins/vici/python/vici/protocol.py (limited to 'src/libcharon/plugins/vici/python') diff --git a/src/libcharon/plugins/vici/python/.gitignore b/src/libcharon/plugins/vici/python/.gitignore new file mode 100644 index 000000000..0d20b6487 --- /dev/null +++ b/src/libcharon/plugins/vici/python/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/src/libcharon/plugins/vici/python/vici/__init__.py b/src/libcharon/plugins/vici/python/vici/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/libcharon/plugins/vici/python/vici/exception.py b/src/libcharon/plugins/vici/python/vici/exception.py new file mode 100644 index 000000000..25d73490b --- /dev/null +++ b/src/libcharon/plugins/vici/python/vici/exception.py @@ -0,0 +1,4 @@ +"""Exception types that may be thrown by this library.""" + +class DeserializationException(Exception): + """Encountered an unexpected byte sequence or missing element type.""" \ No newline at end of file diff --git a/src/libcharon/plugins/vici/python/vici/protocol.py b/src/libcharon/plugins/vici/python/vici/protocol.py new file mode 100644 index 000000000..fe4e5d7b5 --- /dev/null +++ b/src/libcharon/plugins/vici/python/vici/protocol.py @@ -0,0 +1,194 @@ +import io +import socket +import struct + +from collections import namedtuple + +from .exception import DeserializationException + + +class Transport(object): + HEADER_LENGTH = 4 + MAX_SEGMENT = 512 * 1024 + + def __init__(self, address="/var/run/charon.vici"): + self.address = address + self.socket = socket.socket(socket.AF_UNIX) + self.socket.connect(address) + + def send(self, packet): + self.socket.sendall(struct.pack("!I", len(packet)) + packet) + + def receive(self): + raw_length = self.socket.recv(self.HEADER_LENGTH) + length, = struct.unpack("!I", raw_length) + payload = self.socket.recv(length) + return payload + + def close(self): + self.socket.shutdown(socket.SHUT_RDWR) + self.socket.close() + + +class Packet(object): + CMD_REQUEST = 0 # Named request message + CMD_RESPONSE = 1 # Unnamed response message for a request + CMD_UNKNOWN = 2 # Unnamed response if requested command is unknown + EVENT_REGISTER = 3 # Named event registration request + EVENT_UNREGISTER = 4 # Named event de-registration request + EVENT_CONFIRM = 5 # Unnamed confirmation for event (de-)registration + EVENT_UNKNOWN = 6 # Unnamed response if event (de-)registration failed + EVENT = 7 # Named event message + + ParsedPacket = namedtuple( + "ParsedPacket", + ["response_type", "payload"] + ) + + ParsedEventPacket = namedtuple( + "ParsedEventPacket", + ["response_type", "event_type", "payload"] + ) + + @classmethod + def _named_request(cls, request_type, request, message=None): + payload = struct.pack("!BB", request_type, len(request)) + request + if message is not None: + return payload + message + else: + return payload + + @classmethod + def request(cls, command, message=None): + return cls._named_request(cls.CMD_REQUEST, command, message) + + @classmethod + def register_event(cls, event_type): + return cls._named_request(cls.EVENT_REGISTER, event_type) + + @classmethod + def unregister_event(cls, event_type): + return cls._named_request(cls.EVENT_UNREGISTER, event_type) + + @classmethod + def parse(cls, packet): + stream = FiniteStream(packet) + response_type, = struct.unpack("!B", stream.read(1)) + + if response_type == cls.EVENT: + length, = struct.unpack("!B", stream.read(1)) + event_type = stream.read(length) + return cls.ParsedEventPacket(response_type, event_type, stream) + else: + return cls.ParsedPacket(response_type, stream) + + +class Message(object): + SECTION_START = 1 # Begin a new section having a name + SECTION_END = 2 # End a previously started section + KEY_VALUE = 3 # Define a value for a named key in the section + LIST_START = 4 # Begin a named list for list items + LIST_ITEM = 5 # Define an unnamed item value in the current list + LIST_END = 6 # End a previously started list + + @classmethod + def serialize(cls, message): + def encode_named_type(marker, name): + name = str(name) + return struct.pack("!BB", marker, len(name)) + name + + def encode_blob(value): + value = str(value) + return struct.pack("!H", len(value)) + value + + def serialize_list(lst): + segment = str() + for item in lst: + segment += struct.pack("!B", cls.LIST_ITEM) + encode_blob(item) + return segment + + def serialize_dict(d): + segment = str() + for key, value in d.iteritems(): + if isinstance(value, dict): + segment += ( + encode_named_type(cls.SECTION_START, key) + + serialize_dict(value) + + struct.pack("!B", cls.SECTION_END) + ) + elif isinstance(value, list): + segment += ( + encode_named_type(cls.LIST_START, key) + + serialize_list(value) + + struct.pack("!B", cls.LIST_END) + ) + else: + segment += ( + encode_named_type(cls.KEY_VALUE, key) + + encode_blob(value) + ) + return segment + + return serialize_dict(message) + + @classmethod + def deserialize(cls, stream): + def decode_named_type(stream): + length, = struct.unpack("!B", stream.read(1)) + return stream.read(length) + + def decode_blob(stream): + length, = struct.unpack("!H", stream.read(2)) + return stream.read(length) + + def decode_list_item(stream): + marker, = struct.unpack("!B", stream.read(1)) + while marker == cls.LIST_ITEM: + yield decode_blob(stream) + marker, = struct.unpack("!B", stream.read(1)) + + if marker != cls.LIST_END: + raise DeserializationException( + "Expected end of list at {pos}".format(pos=stream.tell()) + ) + + section = {} + section_stack = [] + while stream.has_more(): + element_type, = struct.unpack("!B", stream.read(1)) + if element_type == cls.SECTION_START: + section_name = decode_named_type(stream) + new_section = {} + section[section_name] = new_section + section_stack.append(section) + section = new_section + + elif element_type == cls.LIST_START: + list_name = decode_named_type(stream) + section[list_name] = [item for item in decode_list_item(stream)] + + elif element_type == cls.KEY_VALUE: + key = decode_named_type(stream) + section[key] = decode_blob(stream) + + elif element_type == cls.SECTION_END: + if len(section_stack): + section = section_stack.pop() + else: + raise DeserializationException( + "Unexpected end of section at {pos}".format( + pos=stream.tell() + ) + ) + + if len(section_stack): + raise DeserializationException("Expected end of section") + return section + + +class FiniteStream(io.BytesIO): + def __len__(self): + return len(self.getvalue()) + + def has_more(self): + return self.tell() < len(self) \ No newline at end of file -- cgit v1.2.3 From 8c089cddef38217cf555aa8183d3621830bb6044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Schuberg?= Date: Sat, 14 Feb 2015 15:53:25 +0100 Subject: vici: Add a python vici command execution handler --- .../plugins/vici/python/vici/exception.py | 5 +- src/libcharon/plugins/vici/python/vici/session.py | 130 +++++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 src/libcharon/plugins/vici/python/vici/session.py (limited to 'src/libcharon/plugins/vici/python') diff --git a/src/libcharon/plugins/vici/python/vici/exception.py b/src/libcharon/plugins/vici/python/vici/exception.py index 25d73490b..89d76ab80 100644 --- a/src/libcharon/plugins/vici/python/vici/exception.py +++ b/src/libcharon/plugins/vici/python/vici/exception.py @@ -1,4 +1,7 @@ """Exception types that may be thrown by this library.""" class DeserializationException(Exception): - """Encountered an unexpected byte sequence or missing element type.""" \ No newline at end of file + """Encountered an unexpected byte sequence or missing element type.""" + +class SessionException(Exception): + """Session request exception.""" \ No newline at end of file diff --git a/src/libcharon/plugins/vici/python/vici/session.py b/src/libcharon/plugins/vici/python/vici/session.py new file mode 100644 index 000000000..ef708feaf --- /dev/null +++ b/src/libcharon/plugins/vici/python/vici/session.py @@ -0,0 +1,130 @@ +import collections + +from .exception import SessionException +from .protocol import Packet, Message + + +class SessionHandler(object): + """Handles client command execution requests over vici.""" + + def __init__(self, transport): + self.transport = transport + self.log_events = collections.deque() + + def _communicate(self, packet): + """Send packet over transport and parse response. + + :param packet: packet to send + :type packet: :py:class:`vici.protocol.Packet` + :return: parsed packet in a tuple with message type and payload + :rtype: :py:class:`collections.namedtuple` + """ + self.transport.send(packet) + return self._read() + + def request(self, command, message=None): + """Send command request with an optional message. + + :param command: command to send + :type command: str + :param message: message (optional) + :type message: str + :return: command result + :rtype: dict + """ + if message is not None: + message = Message.serialize(message) + packet = Packet.request(command, message) + response = self._communicate(packet) + + if response.response_type != Packet.CMD_RESPONSE: + raise SessionException( + "Unexpected response type {type}, " + "expected '{response}' (CMD_RESPONSE)".format( + type=response.response_type, + response=Packet.CMD_RESPONSE + ) + ) + + return Message.deserialize(response.payload) + + def streamed_request(self, command, event_stream_type, message=None): + """Send command request and collect and return all emitted events. + + :param command: command to send + :type command: str + :param event_stream_type: event type emitted on command execution + :type event_stream_type: str + :param message: message (optional) + :type message: str + :return: a pair of the command result and a list of emitted events + :rtype: tuple + """ + result = [] + + if message is not None: + message = Message.serialize(message) + + # subscribe to event stream + packet = Packet.register_event(event_stream_type) + response = self._communicate(packet) + + if response.response_type != Packet.EVENT_CONFIRM: + raise SessionException( + "Unexpected response type {type}, " + "expected '{confirm}' (EVENT_CONFIRM)".format( + type=response.response_type, + confirm=Packet.EVENT_CONFIRM, + ) + ) + + # issue command, and read any event messages + packet = Packet.request(command, message) + self.transport.send(packet) + response = self._read() + while response.response_type == Packet.EVENT: + result.append(Message.deserialize(response.payload)) + response = self._read() + + if response.response_type == Packet.CMD_RESPONSE: + response_message = Message.deserialize(response.payload) + else: + raise SessionException( + "Unexpected response type {type}, " + "expected '{response}' (CMD_RESPONSE)".format( + type=response.response_type, + response=Packet.CMD_RESPONSE + ) + ) + + # unsubscribe from event stream + packet = Packet.unregister_event(event_stream_type) + response = self._communicate(packet) + if response.response_type != Packet.EVENT_CONFIRM: + raise SessionException( + "Unexpected response type {type}, " + "expected '{confirm}' (EVENT_CONFIRM)".format( + type=response.response_type, + confirm=Packet.EVENT_CONFIRM, + ) + ) + + return (response_message, result) + + def _read(self): + """Get next packet from transport. + + :return: parsed packet in a tuple with message type and payload + :rtype: :py:class:`collections.namedtuple` + """ + raw_response = self.transport.receive() + response = Packet.parse(raw_response) + + # FIXME + if response.response_type == Packet.EVENT and response.event_type == "log": + # queue up any debug log messages, and get next + self.log_events.append(response) + # do something? + self._read() + else: + return response -- cgit v1.2.3 From 6a31a0f60cb25b633a7ccbcce0a02f096d2664c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Schuberg?= Date: Sun, 15 Feb 2015 16:13:44 +0100 Subject: vici: Introduce main API Session class in python package --- src/libcharon/plugins/vici/python/vici/session.py | 245 +++++++++++++++++++++- 1 file changed, 244 insertions(+), 1 deletion(-) (limited to 'src/libcharon/plugins/vici/python') diff --git a/src/libcharon/plugins/vici/python/vici/session.py b/src/libcharon/plugins/vici/python/vici/session.py index ef708feaf..dd0249318 100644 --- a/src/libcharon/plugins/vici/python/vici/session.py +++ b/src/libcharon/plugins/vici/python/vici/session.py @@ -1,7 +1,250 @@ import collections from .exception import SessionException -from .protocol import Packet, Message +from .protocol import Transport, Packet, Message + + +CommandResult = collections.namedtuple( + "CommandResult", + ["success", "errmsg", "log"] +) + + +class Session(object): + def __init__(self, address="/var/run/charon.vici"): + self.handler = SessionHandler(Transport(address)) + + def version(self): + """Retrieve daemon and system specific version information. + + :return: daemon and system specific version information + :rtype: dict + """ + return self.handler.request("version") + + def stats(self): + """Retrieve IKE daemon statistics and load information. + + :return: IKE daemon statistics and load information + :rtype: dict + """ + return self.handler.request("stats") + + def reload_settings(self): + """Reload strongswan.conf settings and any plugins supporting reload. + + :return: result of command, with `errmsg` given on failure + :rtype: :py:class:`vici.session.CommandResult` + """ + return self._result(self.handler.request("reload-settings")) + + def initiate(self, sa): + """Initiate an SA. + + :param sa: the SA to initiate + :type sa: dict + :return: logs emitted by command, with `errmsg` given on failure + :rtype: :py:class:`vici.session.CommandResult` + """ + response = self.handler.streamed_request("initiate", "control-log", sa) + return self._result(*response) + + def terminate(self, sa): + """Terminate an SA. + + :param sa: the SA to terminate + :type sa: dict + :return: logs emitted by command, with `errmsg` given on failure + :rtype: :py:class:`vici.session.CommandResult` + """ + response = self.handler.streamed_request("terminate", "control-log", sa) + return self._result(*response) + + def install(self, policy): + """Install a trap, drop or bypass policy defined by a CHILD_SA config. + + :param policy: policy to install + :type policy: dict + :return: result of command, with `errmsg` given on failure + :rtype: :py:class:`vici.session.CommandResult` + """ + return self._result(self.handler.request("install", policy)) + + def uninstall(self, policy): + """Uninstall a trap, drop or bypass policy defined by a CHILD_SA config. + + :param policy: policy to uninstall + :type policy: dict + :return: result of command, with `errmsg` given on failure + :rtype: :py:class:`vici.session.CommandResult` + """ + return self._result(self.handler.request("uninstall", policy)) + + def list_sas(self, filters=None): + """Retrieve active IKE_SAs and associated CHILD_SAs. + + :param filters: retrieve only matching IKE_SAs (optional) + :type filters: dict + :return: list of active IKE_SAs and associated CHILD_SAs + :rtype: list + """ + _, sa_list = self.handler.streamed_request("list-sas", + "list-sa", filters) + return sa_list + + def list_policies(self, filters=None): + """Retrieve installed trap, drop and bypass policies. + + :param filters: retrieve only matching policies (optional) + :type filters: dict + :return: list of installed trap, drop and bypass policies + :rtype: list + """ + _, policy_list = self.handler.streamed_request("list-policies", + "list-policy", filters) + return policy_list + + def list_conns(self, filters=None): + """Retrieve loaded connections. + + :param filters: retrieve only matching configuration names (optional) + :type filters: dict + :return: list of connections + :rtype: list + """ + _, connection_list = self.handler.streamed_request("list-conns", + "list-conn", filters) + return connection_list + + def get_conns(self): + """Retrieve connection names loaded exclusively over vici. + + :return: connection names + :rtype: dict + """ + return self.handler.request("get-conns") + + def list_certs(self, filters=None): + """Retrieve loaded certificates. + + :param filters: retrieve only matching certificates (optional) + :type filters: dict + :return: list of installed trap, drop and bypass policies + :rtype: list + """ + _, cert_list = self.handler.streamed_request("list-certs", + "list-cert", filters) + return cert_list + + def load_conn(self, connection): + """Load a connection definition into the daemon. + + :param connection: connection definition + :type connection: dict + :return: result of command, with `errmsg` given on failure + :rtype: :py:class:`vici.session.CommandResult` + """ + return self._result(self.handler.request("load-conn", connection)) + + def unload_conn(self, name): + """Unload a connection definition. + + :param name: connection definition name + :type name: dict + :return: result of command, with `errmsg` given on failure + :rtype: :py:class:`vici.session.CommandResult` + """ + return self._result(self.handler.request("unload-conn", name)) + + def load_cert(self, certificate): + """Load a certificate into the daemon. + + :param certificate: PEM or DER encoded certificate + :type certificate: dict + :return: result of command, with `errmsg` given on failure + :rtype: :py:class:`vici.session.CommandResult` + """ + return self._result(self.handler.request("load-cert", certificate)) + + def load_key(self, private_key): + """Load a private key into the daemon. + + :param private_key: PEM or DER encoded key + :return: result of command, with `errmsg` given on failure + :rtype: :py:class:`vici.session.CommandResult` + """ + return self._result(self.handler.request("load-key", private_key)) + + def load_shared(self, secret): + """Load a shared IKE PSK, EAP or XAuth secret into the daemon. + + :param secret: shared IKE PSK, EAP or XAuth secret + :type secret: dict + :return: result of command, with `errmsg` given on failure + :rtype: :py:class:`vici.session.CommandResult` + """ + return self._result(self.handler.request("load-shared", secret)) + + def clear_creds(self): + """Clear credentials loaded over vici. + + Clear all loaded certificate, private key and shared key credentials. + This affects only credentials loaded over vici, but additionally + flushes the credential cache. + + :return: result of command, with `errmsg` given on failure + :rtype: :py:class:`vici.session.CommandResult` + """ + return self._result(self.handler.request("clear-creds")) + + def load_pool(self, pool): + """Load a virtual IP pool. + + Load an in-memory virtual IP and configuration attribute pool. + Existing pools with the same name get updated, if possible. + + :param pool: virtual IP and configuration attribute pool + :type pool: dict + :return: result of command, with `errmsg` given on failure + :rtype: :py:class:`vici.session.CommandResult` + """ + return self._result(self.handler.request("load-pool", pool)) + + def unload_pool(self, pool_name): + """Unload a virtual IP pool. + + Unload a previously loaded virtual IP and configuration attribute pool. + Unloading fails for pools with leases currently online. + + :param pool_name: pool by name + :type pool_name: dict + :return: result of command, with `errmsg` given on failure + :rtype: :py:class:`vici.session.CommandResult` + """ + return self._result(self.handler.request("unload-pool", pool_name)) + + def get_pools(self): + """Retrieve loaded pools. + + :return: loaded pools + :rtype: dict + """ + return self.handler.request("get-pools") + + def _result(self, command_response, log=None): + """Create a CommandResult for a request response. + + :param command_response: command request response + :type command_response: dict + :param log: list of log messages (optional) + :type log: list + :return: a CommandResult containing any given log messages + :rtype: :py:class:`vici.session.CommandResult` + """ + if command_response["success"] == "yes": + return CommandResult(True, None, log) + else: + return CommandResult(False, command_response["errmsg"], log) class SessionHandler(object): -- cgit v1.2.3 From b269c1a89abc215b0b264a8c31f1e700d04061f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Schuberg?= Date: Sun, 15 Feb 2015 19:18:52 +0100 Subject: vici: Expose Session as a top-level symbol in python package --- src/libcharon/plugins/vici/python/vici/__init__.py | 1 + 1 file changed, 1 insertion(+) (limited to 'src/libcharon/plugins/vici/python') diff --git a/src/libcharon/plugins/vici/python/vici/__init__.py b/src/libcharon/plugins/vici/python/vici/__init__.py index e69de29bb..d314325b6 100644 --- a/src/libcharon/plugins/vici/python/vici/__init__.py +++ b/src/libcharon/plugins/vici/python/vici/__init__.py @@ -0,0 +1 @@ +from .session import Session -- cgit v1.2.3 From 163e15a57196244f1eca7d2eb09fdaa87d2cb5ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Schuberg?= Date: Mon, 16 Feb 2015 00:17:00 +0100 Subject: vici: Add python package MIT license --- src/libcharon/plugins/vici/python/LICENSE | 19 +++++++++++++++++++ src/libcharon/plugins/vici/python/MANIFEST.in | 1 + 2 files changed, 20 insertions(+) create mode 100644 src/libcharon/plugins/vici/python/LICENSE create mode 100644 src/libcharon/plugins/vici/python/MANIFEST.in (limited to 'src/libcharon/plugins/vici/python') diff --git a/src/libcharon/plugins/vici/python/LICENSE b/src/libcharon/plugins/vici/python/LICENSE new file mode 100644 index 000000000..111523ca8 --- /dev/null +++ b/src/libcharon/plugins/vici/python/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015 Björn Schuberg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/libcharon/plugins/vici/python/MANIFEST.in b/src/libcharon/plugins/vici/python/MANIFEST.in new file mode 100644 index 000000000..1aba38f67 --- /dev/null +++ b/src/libcharon/plugins/vici/python/MANIFEST.in @@ -0,0 +1 @@ +include LICENSE -- cgit v1.2.3 From 2c8c52c4e2f36fe035cd23dda6d55280ed535fb9 Mon Sep 17 00:00:00 2001 From: Martin Willi Date: Wed, 25 Feb 2015 16:04:57 +0100 Subject: vici: Include python package in distribution --- src/libcharon/plugins/vici/python/Makefile.am | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/libcharon/plugins/vici/python/Makefile.am (limited to 'src/libcharon/plugins/vici/python') diff --git a/src/libcharon/plugins/vici/python/Makefile.am b/src/libcharon/plugins/vici/python/Makefile.am new file mode 100644 index 000000000..f8575eb21 --- /dev/null +++ b/src/libcharon/plugins/vici/python/Makefile.am @@ -0,0 +1,5 @@ +EXTRA_DIST = LICENSE MANIFEST.in \ + vici/__init__.py \ + vici/exception.py \ + vici/protocol.py \ + vici/session.py -- cgit v1.2.3 From 1e2ec9f96a010e0e74d88ace7859ff5289736e49 Mon Sep 17 00:00:00 2001 From: Martin Willi Date: Wed, 25 Feb 2015 16:18:29 +0100 Subject: vici: Generate a version specific setup.py for setuptools installation --- src/libcharon/plugins/vici/python/.gitignore | 4 ++++ src/libcharon/plugins/vici/python/Makefile.am | 6 ++++++ src/libcharon/plugins/vici/python/setup.py.in | 31 +++++++++++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 src/libcharon/plugins/vici/python/setup.py.in (limited to 'src/libcharon/plugins/vici/python') diff --git a/src/libcharon/plugins/vici/python/.gitignore b/src/libcharon/plugins/vici/python/.gitignore index 0d20b6487..5c4589841 100644 --- a/src/libcharon/plugins/vici/python/.gitignore +++ b/src/libcharon/plugins/vici/python/.gitignore @@ -1 +1,5 @@ *.pyc +build +dist +vici.egg-info +setup.py diff --git a/src/libcharon/plugins/vici/python/Makefile.am b/src/libcharon/plugins/vici/python/Makefile.am index f8575eb21..1c77ee5c7 100644 --- a/src/libcharon/plugins/vici/python/Makefile.am +++ b/src/libcharon/plugins/vici/python/Makefile.am @@ -1,5 +1,11 @@ EXTRA_DIST = LICENSE MANIFEST.in \ + setup.py.in \ vici/__init__.py \ vici/exception.py \ vici/protocol.py \ vici/session.py + +setup.py: $(srcdir)/setup.py.in + $(AM_V_GEN) sed \ + -e "s:@EGG_VERSION@:$(PACKAGE_VERSION):" \ + $(srcdir)/setup.py.in > $@ diff --git a/src/libcharon/plugins/vici/python/setup.py.in b/src/libcharon/plugins/vici/python/setup.py.in new file mode 100644 index 000000000..9b8556595 --- /dev/null +++ b/src/libcharon/plugins/vici/python/setup.py.in @@ -0,0 +1,31 @@ +from setuptools import setup + + +long_description = ( + "The strongSwan VICI protocol allows external application to monitor, " + "configure and control the IKE daemon charon. This python package provides " + "a native client side implementation of the VICI protocol, well suited to " + "script automated tasks in a reliable way." +) + +setup( + name="vici", + version="@EGG_VERSION@", + description="Native python interface for strongSwan VICI", + author="Bjorn Schuberg", + url="https://wiki.strongswan.org/projects/strongswan/wiki/Vici", + license="MIT", + packages=["vici"], + long_description=long_description, + include_package_data=True, + classifiers=( + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Programming Language :: Python :: 2.7", + "Topic :: Security", + "Topic :: Software Development :: Libraries", + ) +) -- cgit v1.2.3 From 358793389a98d29887a6bb72f9b48166f6dcb197 Mon Sep 17 00:00:00 2001 From: Martin Willi Date: Wed, 25 Feb 2015 16:20:10 +0100 Subject: vici: Add python egg setuptools building and installation using easy_install An uninstall target is currently not supported, as there is no trivial way with either plain setuptools or with easy_install. pip would probably be the best choice, but we currently don't depend on it. --- src/libcharon/plugins/vici/python/Makefile.am | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'src/libcharon/plugins/vici/python') diff --git a/src/libcharon/plugins/vici/python/Makefile.am b/src/libcharon/plugins/vici/python/Makefile.am index 1c77ee5c7..94ecb1f48 100644 --- a/src/libcharon/plugins/vici/python/Makefile.am +++ b/src/libcharon/plugins/vici/python/Makefile.am @@ -9,3 +9,18 @@ setup.py: $(srcdir)/setup.py.in $(AM_V_GEN) sed \ -e "s:@EGG_VERSION@:$(PACKAGE_VERSION):" \ $(srcdir)/setup.py.in > $@ + +all-local: dist/vici-$(PACKAGE_VERSION)-py$(PYTHON_VERSION).egg + +dist/vici-$(PACKAGE_VERSION)-py$(PYTHON_VERSION).egg: $(EXTRA_DIST) setup.py + (cd $(srcdir); $(PYTHON) setup.py bdist_egg \ + -b $(shell readlink -f $(builddir))/build \ + -d $(shell readlink -f $(builddir))/dist) + +clean-local: setup.py + $(PYTHON) setup.py clean -a + rm -rf vici.egg-info dist setup.py + +install-exec-local: dist/vici-$(PACKAGE_VERSION)-py$(PYTHON_VERSION).egg + $(EASY_INSTALL) $(PYTHONEGGINSTALLDIR) \ + dist/vici-$(PACKAGE_VERSION)-py$(PYTHON_VERSION).egg -- cgit v1.2.3 From 61fb10c8cf2fd432b1f154d8c5b1aaa2c5b4c7a1 Mon Sep 17 00:00:00 2001 From: Martin Willi Date: Fri, 27 Feb 2015 13:59:23 +0100 Subject: vici: Support non-Unix sockets for vici connections using Python --- src/libcharon/plugins/vici/python/vici/protocol.py | 8 +++----- src/libcharon/plugins/vici/python/vici/session.py | 8 ++++++-- 2 files changed, 9 insertions(+), 7 deletions(-) (limited to 'src/libcharon/plugins/vici/python') diff --git a/src/libcharon/plugins/vici/python/vici/protocol.py b/src/libcharon/plugins/vici/python/vici/protocol.py index fe4e5d7b5..60b94ede9 100644 --- a/src/libcharon/plugins/vici/python/vici/protocol.py +++ b/src/libcharon/plugins/vici/python/vici/protocol.py @@ -11,10 +11,8 @@ class Transport(object): HEADER_LENGTH = 4 MAX_SEGMENT = 512 * 1024 - def __init__(self, address="/var/run/charon.vici"): - self.address = address - self.socket = socket.socket(socket.AF_UNIX) - self.socket.connect(address) + def __init__(self, sock): + self.socket = sock def send(self, packet): self.socket.sendall(struct.pack("!I", len(packet)) + packet) @@ -191,4 +189,4 @@ class FiniteStream(io.BytesIO): return len(self.getvalue()) def has_more(self): - return self.tell() < len(self) \ No newline at end of file + return self.tell() < len(self) diff --git a/src/libcharon/plugins/vici/python/vici/session.py b/src/libcharon/plugins/vici/python/vici/session.py index dd0249318..cffac6a8d 100644 --- a/src/libcharon/plugins/vici/python/vici/session.py +++ b/src/libcharon/plugins/vici/python/vici/session.py @@ -1,4 +1,5 @@ import collections +import socket from .exception import SessionException from .protocol import Transport, Packet, Message @@ -11,8 +12,11 @@ CommandResult = collections.namedtuple( class Session(object): - def __init__(self, address="/var/run/charon.vici"): - self.handler = SessionHandler(Transport(address)) + def __init__(self, sock=None): + if sock is None: + sock = socket.socket(socket.AF_UNIX) + sock.connect("/var/run/charon.vici") + self.handler = SessionHandler(Transport(sock)) def version(self): """Retrieve daemon and system specific version information. -- cgit v1.2.3 From 305023a27d97653b67510035edc95ccf26599ede Mon Sep 17 00:00:00 2001 From: Martin Willi Date: Fri, 27 Feb 2015 14:30:34 +0100 Subject: vici: Use OrderedDict to handle vici responses in Python library The default Python dictionaries are unordered, but order is important for some vici trees (for example the order of authentication rounds). --- src/libcharon/plugins/vici/python/vici/protocol.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src/libcharon/plugins/vici/python') diff --git a/src/libcharon/plugins/vici/python/vici/protocol.py b/src/libcharon/plugins/vici/python/vici/protocol.py index 60b94ede9..88e1c3470 100644 --- a/src/libcharon/plugins/vici/python/vici/protocol.py +++ b/src/libcharon/plugins/vici/python/vici/protocol.py @@ -3,6 +3,7 @@ import socket import struct from collections import namedtuple +from collections import OrderedDict from .exception import DeserializationException @@ -150,13 +151,13 @@ class Message(object): "Expected end of list at {pos}".format(pos=stream.tell()) ) - section = {} + section = OrderedDict() section_stack = [] while stream.has_more(): element_type, = struct.unpack("!B", stream.read(1)) if element_type == cls.SECTION_START: section_name = decode_named_type(stream) - new_section = {} + new_section = OrderedDict() section[section_name] = new_section section_stack.append(section) section = new_section -- cgit v1.2.3 From 90e16837bae939875a01be55e7c350bb83c66019 Mon Sep 17 00:00:00 2001 From: Martin Willi Date: Mon, 2 Mar 2015 15:19:32 +0100 Subject: vici: Raise a Python CommandException instead of returning a CommandResult --- .../plugins/vici/python/vici/exception.py | 5 +- src/libcharon/plugins/vici/python/vici/session.py | 119 +++++++-------------- 2 files changed, 42 insertions(+), 82 deletions(-) (limited to 'src/libcharon/plugins/vici/python') diff --git a/src/libcharon/plugins/vici/python/vici/exception.py b/src/libcharon/plugins/vici/python/vici/exception.py index 89d76ab80..36384e556 100644 --- a/src/libcharon/plugins/vici/python/vici/exception.py +++ b/src/libcharon/plugins/vici/python/vici/exception.py @@ -4,4 +4,7 @@ class DeserializationException(Exception): """Encountered an unexpected byte sequence or missing element type.""" class SessionException(Exception): - """Session request exception.""" \ No newline at end of file + """Session request exception.""" + +class CommandException(Exception): + """Command result exception.""" diff --git a/src/libcharon/plugins/vici/python/vici/session.py b/src/libcharon/plugins/vici/python/vici/session.py index cffac6a8d..172252065 100644 --- a/src/libcharon/plugins/vici/python/vici/session.py +++ b/src/libcharon/plugins/vici/python/vici/session.py @@ -1,16 +1,10 @@ import collections import socket -from .exception import SessionException +from .exception import SessionException, CommandException from .protocol import Transport, Packet, Message -CommandResult = collections.namedtuple( - "CommandResult", - ["success", "errmsg", "log"] -) - - class Session(object): def __init__(self, sock=None): if sock is None: @@ -36,53 +30,44 @@ class Session(object): def reload_settings(self): """Reload strongswan.conf settings and any plugins supporting reload. - - :return: result of command, with `errmsg` given on failure - :rtype: :py:class:`vici.session.CommandResult` """ - return self._result(self.handler.request("reload-settings")) + self.handler.request("reload-settings") def initiate(self, sa): """Initiate an SA. :param sa: the SA to initiate :type sa: dict - :return: logs emitted by command, with `errmsg` given on failure - :rtype: :py:class:`vici.session.CommandResult` + :return: logs emitted by command + :rtype: list """ - response = self.handler.streamed_request("initiate", "control-log", sa) - return self._result(*response) + return self.handler.streamed_request("initiate", "control-log", sa) def terminate(self, sa): """Terminate an SA. :param sa: the SA to terminate :type sa: dict - :return: logs emitted by command, with `errmsg` given on failure - :rtype: :py:class:`vici.session.CommandResult` + :return: logs emitted by command + :rtype: list """ - response = self.handler.streamed_request("terminate", "control-log", sa) - return self._result(*response) + return self.handler.streamed_request("terminate", "control-log", sa) def install(self, policy): """Install a trap, drop or bypass policy defined by a CHILD_SA config. :param policy: policy to install :type policy: dict - :return: result of command, with `errmsg` given on failure - :rtype: :py:class:`vici.session.CommandResult` """ - return self._result(self.handler.request("install", policy)) + self.handler.request("install", policy) def uninstall(self, policy): """Uninstall a trap, drop or bypass policy defined by a CHILD_SA config. :param policy: policy to uninstall :type policy: dict - :return: result of command, with `errmsg` given on failure - :rtype: :py:class:`vici.session.CommandResult` """ - return self._result(self.handler.request("uninstall", policy)) + self.handler.request("uninstall", policy) def list_sas(self, filters=None): """Retrieve active IKE_SAs and associated CHILD_SAs. @@ -92,9 +77,7 @@ class Session(object): :return: list of active IKE_SAs and associated CHILD_SAs :rtype: list """ - _, sa_list = self.handler.streamed_request("list-sas", - "list-sa", filters) - return sa_list + return self.handler.streamed_request("list-sas", "list-sa", filters) def list_policies(self, filters=None): """Retrieve installed trap, drop and bypass policies. @@ -104,9 +87,8 @@ class Session(object): :return: list of installed trap, drop and bypass policies :rtype: list """ - _, policy_list = self.handler.streamed_request("list-policies", - "list-policy", filters) - return policy_list + return self.handler.streamed_request("list-policies", "list-policy", + filters) def list_conns(self, filters=None): """Retrieve loaded connections. @@ -116,9 +98,8 @@ class Session(object): :return: list of connections :rtype: list """ - _, connection_list = self.handler.streamed_request("list-conns", - "list-conn", filters) - return connection_list + return self.handler.streamed_request("list-conns", "list-conn", + filters) def get_conns(self): """Retrieve connection names loaded exclusively over vici. @@ -136,58 +117,46 @@ class Session(object): :return: list of installed trap, drop and bypass policies :rtype: list """ - _, cert_list = self.handler.streamed_request("list-certs", - "list-cert", filters) - return cert_list + return self.handler.streamed_request("list-certs", "list-cert", filters) def load_conn(self, connection): """Load a connection definition into the daemon. :param connection: connection definition :type connection: dict - :return: result of command, with `errmsg` given on failure - :rtype: :py:class:`vici.session.CommandResult` """ - return self._result(self.handler.request("load-conn", connection)) + self.handler.request("load-conn", connection) def unload_conn(self, name): """Unload a connection definition. :param name: connection definition name :type name: dict - :return: result of command, with `errmsg` given on failure - :rtype: :py:class:`vici.session.CommandResult` """ - return self._result(self.handler.request("unload-conn", name)) + self.handler.request("unload-conn", name) def load_cert(self, certificate): """Load a certificate into the daemon. :param certificate: PEM or DER encoded certificate :type certificate: dict - :return: result of command, with `errmsg` given on failure - :rtype: :py:class:`vici.session.CommandResult` """ - return self._result(self.handler.request("load-cert", certificate)) + self.handler.request("load-cert", certificate) def load_key(self, private_key): """Load a private key into the daemon. :param private_key: PEM or DER encoded key - :return: result of command, with `errmsg` given on failure - :rtype: :py:class:`vici.session.CommandResult` """ - return self._result(self.handler.request("load-key", private_key)) + self.handler.request("load-key", private_key) def load_shared(self, secret): """Load a shared IKE PSK, EAP or XAuth secret into the daemon. :param secret: shared IKE PSK, EAP or XAuth secret :type secret: dict - :return: result of command, with `errmsg` given on failure - :rtype: :py:class:`vici.session.CommandResult` """ - return self._result(self.handler.request("load-shared", secret)) + self.handler.request("load-shared", secret) def clear_creds(self): """Clear credentials loaded over vici. @@ -195,11 +164,8 @@ class Session(object): Clear all loaded certificate, private key and shared key credentials. This affects only credentials loaded over vici, but additionally flushes the credential cache. - - :return: result of command, with `errmsg` given on failure - :rtype: :py:class:`vici.session.CommandResult` """ - return self._result(self.handler.request("clear-creds")) + self.handler.request("clear-creds") def load_pool(self, pool): """Load a virtual IP pool. @@ -209,10 +175,8 @@ class Session(object): :param pool: virtual IP and configuration attribute pool :type pool: dict - :return: result of command, with `errmsg` given on failure - :rtype: :py:class:`vici.session.CommandResult` """ - return self._result(self.handler.request("load-pool", pool)) + return self.handler.request("load-pool", pool) def unload_pool(self, pool_name): """Unload a virtual IP pool. @@ -222,10 +186,8 @@ class Session(object): :param pool_name: pool by name :type pool_name: dict - :return: result of command, with `errmsg` given on failure - :rtype: :py:class:`vici.session.CommandResult` """ - return self._result(self.handler.request("unload-pool", pool_name)) + self.handler.request("unload-pool", pool_name) def get_pools(self): """Retrieve loaded pools. @@ -235,21 +197,6 @@ class Session(object): """ return self.handler.request("get-pools") - def _result(self, command_response, log=None): - """Create a CommandResult for a request response. - - :param command_response: command request response - :type command_response: dict - :param log: list of log messages (optional) - :type log: list - :return: a CommandResult containing any given log messages - :rtype: :py:class:`vici.session.CommandResult` - """ - if command_response["success"] == "yes": - return CommandResult(True, None, log) - else: - return CommandResult(False, command_response["errmsg"], log) - class SessionHandler(object): """Handles client command execution requests over vici.""" @@ -270,7 +217,7 @@ class SessionHandler(object): return self._read() def request(self, command, message=None): - """Send command request with an optional message. + """Send request with an optional message. :param command: command to send :type command: str @@ -293,7 +240,16 @@ class SessionHandler(object): ) ) - return Message.deserialize(response.payload) + command_response = Message.deserialize(response.payload) + if "success" in command_response: + if command_response["success"] != "yes": + raise CommandException( + "Command failed: {errmsg}".format( + errmsg=command_response["errmsg"] + ) + ) + + return command_response def streamed_request(self, command, event_stream_type, message=None): """Send command request and collect and return all emitted events. @@ -334,7 +290,7 @@ class SessionHandler(object): response = self._read() if response.response_type == Packet.CMD_RESPONSE: - response_message = Message.deserialize(response.payload) + Message.deserialize(response.payload) else: raise SessionException( "Unexpected response type {type}, " @@ -356,7 +312,8 @@ class SessionHandler(object): ) ) - return (response_message, result) + return result + def _read(self): """Get next packet from transport. -- cgit v1.2.3 From a47e431ba9c8b88a7a2244b7793afd29d72bd729 Mon Sep 17 00:00:00 2001 From: Martin Willi Date: Mon, 2 Mar 2015 15:25:55 +0100 Subject: vici: Return a Python generator instead of a list for streamed responses In addition that it may reduce memory usage and improve performance for large responses, it returns immediate results. This is important for longer lasting commands, such as initiate/terminate, where immediate log feedback is preferable when interactively calling such commands. --- src/libcharon/plugins/vici/python/vici/session.py | 64 ++++++++--------------- 1 file changed, 21 insertions(+), 43 deletions(-) (limited to 'src/libcharon/plugins/vici/python') diff --git a/src/libcharon/plugins/vici/python/vici/session.py b/src/libcharon/plugins/vici/python/vici/session.py index 172252065..65b89b5ba 100644 --- a/src/libcharon/plugins/vici/python/vici/session.py +++ b/src/libcharon/plugins/vici/python/vici/session.py @@ -38,8 +38,8 @@ class Session(object): :param sa: the SA to initiate :type sa: dict - :return: logs emitted by command - :rtype: list + :return: generator for logs emitted as dict + :rtype: generator """ return self.handler.streamed_request("initiate", "control-log", sa) @@ -48,8 +48,8 @@ class Session(object): :param sa: the SA to terminate :type sa: dict - :return: logs emitted by command - :rtype: list + :return: generator for logs emitted as dict + :rtype: generator """ return self.handler.streamed_request("terminate", "control-log", sa) @@ -74,8 +74,8 @@ class Session(object): :param filters: retrieve only matching IKE_SAs (optional) :type filters: dict - :return: list of active IKE_SAs and associated CHILD_SAs - :rtype: list + :return: generator for active IKE_SAs and associated CHILD_SAs as dict + :rtype: generator """ return self.handler.streamed_request("list-sas", "list-sa", filters) @@ -84,8 +84,8 @@ class Session(object): :param filters: retrieve only matching policies (optional) :type filters: dict - :return: list of installed trap, drop and bypass policies - :rtype: list + :return: generator for installed trap, drop and bypass policies as dict + :rtype: generator """ return self.handler.streamed_request("list-policies", "list-policy", filters) @@ -95,8 +95,8 @@ class Session(object): :param filters: retrieve only matching configuration names (optional) :type filters: dict - :return: list of connections - :rtype: list + :return: generator for loaded connections as dict + :rtype: generator """ return self.handler.streamed_request("list-conns", "list-conn", filters) @@ -114,8 +114,8 @@ class Session(object): :param filters: retrieve only matching certificates (optional) :type filters: dict - :return: list of installed trap, drop and bypass policies - :rtype: list + :return: generator for loaded certificates as dict + :rtype: generator """ return self.handler.streamed_request("list-certs", "list-cert", filters) @@ -203,7 +203,6 @@ class SessionHandler(object): def __init__(self, transport): self.transport = transport - self.log_events = collections.deque() def _communicate(self, packet): """Send packet over transport and parse response. @@ -214,7 +213,7 @@ class SessionHandler(object): :rtype: :py:class:`collections.namedtuple` """ self.transport.send(packet) - return self._read() + return Packet.parse(self.transport.receive()) def request(self, command, message=None): """Send request with an optional message. @@ -260,11 +259,9 @@ class SessionHandler(object): :type event_stream_type: str :param message: message (optional) :type message: str - :return: a pair of the command result and a list of emitted events - :rtype: tuple + :return: generator for streamed event responses as dict + :rtype: generator """ - result = [] - if message is not None: message = Message.serialize(message) @@ -284,10 +281,12 @@ class SessionHandler(object): # issue command, and read any event messages packet = Packet.request(command, message) self.transport.send(packet) - response = self._read() - while response.response_type == Packet.EVENT: - result.append(Message.deserialize(response.payload)) - response = self._read() + while True: + response = Packet.parse(self.transport.receive()) + if response.response_type == Packet.EVENT: + yield Message.deserialize(response.payload) + else: + break if response.response_type == Packet.CMD_RESPONSE: Message.deserialize(response.payload) @@ -311,24 +310,3 @@ class SessionHandler(object): confirm=Packet.EVENT_CONFIRM, ) ) - - return result - - - def _read(self): - """Get next packet from transport. - - :return: parsed packet in a tuple with message type and payload - :rtype: :py:class:`collections.namedtuple` - """ - raw_response = self.transport.receive() - response = Packet.parse(raw_response) - - # FIXME - if response.response_type == Packet.EVENT and response.event_type == "log": - # queue up any debug log messages, and get next - self.log_events.append(response) - # do something? - self._read() - else: - return response -- cgit v1.2.3 From 90c5b48c96d7d34fbc446660449892f3bd3b9040 Mon Sep 17 00:00:00 2001 From: Martin Willi Date: Mon, 9 Mar 2015 12:06:38 +0100 Subject: vici: Catch Python GeneratorExit to properly cancel streamed event iteration --- src/libcharon/plugins/vici/python/vici/session.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'src/libcharon/plugins/vici/python') diff --git a/src/libcharon/plugins/vici/python/vici/session.py b/src/libcharon/plugins/vici/python/vici/session.py index 65b89b5ba..da79ecd64 100644 --- a/src/libcharon/plugins/vici/python/vici/session.py +++ b/src/libcharon/plugins/vici/python/vici/session.py @@ -281,10 +281,16 @@ class SessionHandler(object): # issue command, and read any event messages packet = Packet.request(command, message) self.transport.send(packet) + exited = False while True: response = Packet.parse(self.transport.receive()) if response.response_type == Packet.EVENT: - yield Message.deserialize(response.payload) + if not exited: + try: + yield Message.deserialize(response.payload) + except GeneratorExit: + exited = True + pass else: break -- cgit v1.2.3 From b5d17e55d78c513ba0642c96f32685297d82b24c Mon Sep 17 00:00:00 2001 From: Martin Willi Date: Mon, 9 Mar 2015 12:16:10 +0100 Subject: vici: Evaluate Python streamed command results, and raise CommandException --- src/libcharon/plugins/vici/python/vici/session.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) (limited to 'src/libcharon/plugins/vici/python') diff --git a/src/libcharon/plugins/vici/python/vici/session.py b/src/libcharon/plugins/vici/python/vici/session.py index da79ecd64..9f4dc5fa7 100644 --- a/src/libcharon/plugins/vici/python/vici/session.py +++ b/src/libcharon/plugins/vici/python/vici/session.py @@ -295,7 +295,7 @@ class SessionHandler(object): break if response.response_type == Packet.CMD_RESPONSE: - Message.deserialize(response.payload) + command_response = Message.deserialize(response.payload) else: raise SessionException( "Unexpected response type {type}, " @@ -316,3 +316,12 @@ class SessionHandler(object): confirm=Packet.EVENT_CONFIRM, ) ) + + # evaluate command result, if any + if "success" in command_response: + if command_response["success"] != "yes": + raise CommandException( + "Command failed: {errmsg}".format( + errmsg=command_response["errmsg"] + ) + ) -- cgit v1.2.3 From 9b97029a5fb1130398add7487eecf17accb81ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Schuberg?= Date: Mon, 9 Mar 2015 11:12:30 +0100 Subject: vici: Add test of Message (de)serialization in python library --- src/libcharon/plugins/vici/python/Makefile.am | 2 + .../plugins/vici/python/vici/test/__init__.py | 0 .../plugins/vici/python/vici/test/test_protocol.py | 98 ++++++++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 src/libcharon/plugins/vici/python/vici/test/__init__.py create mode 100644 src/libcharon/plugins/vici/python/vici/test/test_protocol.py (limited to 'src/libcharon/plugins/vici/python') diff --git a/src/libcharon/plugins/vici/python/Makefile.am b/src/libcharon/plugins/vici/python/Makefile.am index 94ecb1f48..098e91ed8 100644 --- a/src/libcharon/plugins/vici/python/Makefile.am +++ b/src/libcharon/plugins/vici/python/Makefile.am @@ -1,5 +1,7 @@ EXTRA_DIST = LICENSE MANIFEST.in \ setup.py.in \ + vici/test/__init__.py \ + vici/test/test_protocol.py \ vici/__init__.py \ vici/exception.py \ vici/protocol.py \ diff --git a/src/libcharon/plugins/vici/python/vici/test/__init__.py b/src/libcharon/plugins/vici/python/vici/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/libcharon/plugins/vici/python/vici/test/test_protocol.py b/src/libcharon/plugins/vici/python/vici/test/test_protocol.py new file mode 100644 index 000000000..30a6b8382 --- /dev/null +++ b/src/libcharon/plugins/vici/python/vici/test/test_protocol.py @@ -0,0 +1,98 @@ +import pytest + +from ..protocol import Message, FiniteStream +from ..exception import DeserializationException + + +class TestMessage(object): + """Message (de)serialization test.""" + + # data definitions for test of de(serialization) + # serialized messages holding a section + ser_sec_unclosed = b"\x01\x08unclosed" + ser_sec_single = b"\x01\x07section\x02" + ser_sec_nested = b"\x01\x05outer\x01\x0asubsection\x02\x02" + + # serialized messages holding a list + ser_list_invalid = b"\x04\x07invalid\x05\x00\x02e1\x02\x03sec\x06" + ser_list_0_item = b"\x04\x05empty\x06" + ser_list_1_item = b"\x04\x01l\x05\x00\x02e1\x06" + ser_list_2_item = b"\x04\x01l\x05\x00\x02e1\x05\x00\x02e2\x06" + + # serialized messages with key value pairs + ser_kv_pair = b"\x03\x03key\x00\x05value" + ser_kv_zero = b"\x03\x0azerolength\x00\x00" + + # deserialized messages holding a section + des_sec_single = { "section": {} } + des_sec_nested = { "outer": { "subsection": {} } } + + # deserialized messages holding a list + des_list_0_item = { "empty": [] } + des_list_1_item = { "l": [ b"e1" ] } + des_list_2_item = { "l": [ b"e1", b"e2" ] } + + # deserialized messages with key value pairs + des_kv_pair = { "key": b"value" } + des_kv_zero = { "zerolength": b"" } + + def test_section_serialization(self): + assert Message.serialize(self.des_sec_single) == self.ser_sec_single + assert Message.serialize(self.des_sec_nested) == self.ser_sec_nested + + def test_list_serialization(self): + assert Message.serialize(self.des_list_0_item) == self.ser_list_0_item + assert Message.serialize(self.des_list_1_item) == self.ser_list_1_item + assert Message.serialize(self.des_list_2_item) == self.ser_list_2_item + + def test_key_serialization(self): + assert Message.serialize(self.des_kv_pair) == self.ser_kv_pair + assert Message.serialize(self.des_kv_zero) == self.ser_kv_zero + + def test_section_deserialization(self): + single = Message.deserialize(FiniteStream(self.ser_sec_single)) + nested = Message.deserialize(FiniteStream(self.ser_sec_nested)) + + assert single == self.des_sec_single + assert nested == self.des_sec_nested + + with pytest.raises(DeserializationException): + Message.deserialize(FiniteStream(self.ser_sec_unclosed)) + + def test_list_deserialization(self): + l0 = Message.deserialize(FiniteStream(self.ser_list_0_item)) + l1 = Message.deserialize(FiniteStream(self.ser_list_1_item)) + l2 = Message.deserialize(FiniteStream(self.ser_list_2_item)) + + assert l0 == self.des_list_0_item + assert l1 == self.des_list_1_item + assert l2 == self.des_list_2_item + + with pytest.raises(DeserializationException): + Message.deserialize(FiniteStream(self.ser_list_invalid)) + + def test_key_deserialization(self): + pair = Message.deserialize(FiniteStream(self.ser_kv_pair)) + zerolength = Message.deserialize(FiniteStream(self.ser_kv_zero)) + + assert pair == self.des_kv_pair + assert zerolength == self.des_kv_zero + + def test_roundtrip(self): + message = { + "key1": "value1", + "section1": { + "sub-section": { + "key2": b"value2", + }, + "list1": [ "item1", "item2" ], + }, + } + serialized_message = FiniteStream(Message.serialize(message)) + deserialized_message = Message.deserialize(serialized_message) + + # ensure that list items and key values remain as undecoded bytes + deserialized_section = deserialized_message["section1"] + assert deserialized_message["key1"] == b"value1" + assert deserialized_section["sub-section"]["key2"] == b"value2" + assert deserialized_section["list1"] == [ b"item1", b"item2" ] -- cgit v1.2.3 From c193b5947ab85093d8272286076149be04246ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Schuberg?= Date: Mon, 9 Mar 2015 11:20:02 +0100 Subject: vici: Add test of Packet layer in python library --- .../plugins/vici/python/vici/test/test_protocol.py | 48 +++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) (limited to 'src/libcharon/plugins/vici/python') diff --git a/src/libcharon/plugins/vici/python/vici/test/test_protocol.py b/src/libcharon/plugins/vici/python/vici/test/test_protocol.py index 30a6b8382..a1f202d79 100644 --- a/src/libcharon/plugins/vici/python/vici/test/test_protocol.py +++ b/src/libcharon/plugins/vici/python/vici/test/test_protocol.py @@ -1,9 +1,55 @@ import pytest -from ..protocol import Message, FiniteStream +from ..protocol import Packet, Message, FiniteStream from ..exception import DeserializationException +class TestPacket(object): + # test data definitions for outgoing packet types + cmd_request = b"\x00\x0c" b"command_type" + cmd_request_msg = b"\x00\x07" b"command" b"payload" + event_register = b"\x03\x0a" b"event_type" + event_unregister = b"\x04\x0a" b"event_type" + + # test data definitions for incoming packet types + cmd_response = b"\x01" b"reply" + cmd_unknown = b"\x02" + event_confirm = b"\x05" + event_unknown = b"\x06" + event = b"\x07\x03" b"log" b"message" + + def test_request(self): + assert Packet.request("command_type") == self.cmd_request + assert Packet.request("command", b"payload") == self.cmd_request_msg + + def test_register_event(self): + assert Packet.register_event("event_type") == self.event_register + + def test_unregister_event(self): + assert Packet.unregister_event("event_type") == self.event_unregister + + def test_parse(self): + parsed_cmd_response = Packet.parse(self.cmd_response) + assert parsed_cmd_response.response_type == Packet.CMD_RESPONSE + assert parsed_cmd_response.payload.getvalue() == self.cmd_response + + parsed_cmd_unknown = Packet.parse(self.cmd_unknown) + assert parsed_cmd_unknown.response_type == Packet.CMD_UNKNOWN + assert parsed_cmd_unknown.payload.getvalue() == self.cmd_unknown + + parsed_event_confirm = Packet.parse(self.event_confirm) + assert parsed_event_confirm.response_type == Packet.EVENT_CONFIRM + assert parsed_event_confirm.payload.getvalue() == self.event_confirm + + parsed_event_unknown = Packet.parse(self.event_unknown) + assert parsed_event_unknown.response_type == Packet.EVENT_UNKNOWN + assert parsed_event_unknown.payload.getvalue() == self.event_unknown + + parsed_event = Packet.parse(self.event) + assert parsed_event.response_type == Packet.EVENT + assert parsed_event.payload.getvalue() == self.event + + class TestMessage(object): """Message (de)serialization test.""" -- cgit v1.2.3 From c7e3c5943fc152dd02ea156221b8d7b30c238921 Mon Sep 17 00:00:00 2001 From: Martin Willi Date: Wed, 11 Mar 2015 10:18:56 +0100 Subject: vici: Execute python tests during "check" if py.test is available --- src/libcharon/plugins/vici/python/Makefile.am | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src/libcharon/plugins/vici/python') diff --git a/src/libcharon/plugins/vici/python/Makefile.am b/src/libcharon/plugins/vici/python/Makefile.am index 098e91ed8..7be733e25 100644 --- a/src/libcharon/plugins/vici/python/Makefile.am +++ b/src/libcharon/plugins/vici/python/Makefile.am @@ -26,3 +26,7 @@ clean-local: setup.py install-exec-local: dist/vici-$(PACKAGE_VERSION)-py$(PYTHON_VERSION).egg $(EASY_INSTALL) $(PYTHONEGGINSTALLDIR) \ dist/vici-$(PACKAGE_VERSION)-py$(PYTHON_VERSION).egg + +if USE_PY_TEST + TESTS = $(PY_TEST) +endif -- cgit v1.2.3 From 2e74aa0a91e6f8e949a98f6069e210bcfbfdbd19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Schuberg?= Date: Mon, 9 Mar 2015 12:28:02 +0100 Subject: vici: Add support for python 3 --- src/libcharon/plugins/vici/python/Makefile.am | 1 + src/libcharon/plugins/vici/python/setup.py.in | 3 +++ src/libcharon/plugins/vici/python/vici/compat.py | 14 ++++++++++++++ src/libcharon/plugins/vici/python/vici/protocol.py | 15 +++++++++------ src/libcharon/plugins/vici/python/vici/session.py | 4 ++-- 5 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 src/libcharon/plugins/vici/python/vici/compat.py (limited to 'src/libcharon/plugins/vici/python') diff --git a/src/libcharon/plugins/vici/python/Makefile.am b/src/libcharon/plugins/vici/python/Makefile.am index 7be733e25..f51737870 100644 --- a/src/libcharon/plugins/vici/python/Makefile.am +++ b/src/libcharon/plugins/vici/python/Makefile.am @@ -3,6 +3,7 @@ EXTRA_DIST = LICENSE MANIFEST.in \ vici/test/__init__.py \ vici/test/test_protocol.py \ vici/__init__.py \ + vici/compat.py \ vici/exception.py \ vici/protocol.py \ vici/session.py diff --git a/src/libcharon/plugins/vici/python/setup.py.in b/src/libcharon/plugins/vici/python/setup.py.in index 9b8556595..0e4ad8236 100644 --- a/src/libcharon/plugins/vici/python/setup.py.in +++ b/src/libcharon/plugins/vici/python/setup.py.in @@ -25,6 +25,9 @@ setup( "License :: OSI Approved :: MIT License", "Natural Language :: English", "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.2", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", "Topic :: Security", "Topic :: Software Development :: Libraries", ) diff --git a/src/libcharon/plugins/vici/python/vici/compat.py b/src/libcharon/plugins/vici/python/vici/compat.py new file mode 100644 index 000000000..b5f46992e --- /dev/null +++ b/src/libcharon/plugins/vici/python/vici/compat.py @@ -0,0 +1,14 @@ +# Help functions for compatibility between python version 2 and 3 + + +# From http://legacy.python.org/dev/peps/pep-0469 +try: + dict.iteritems +except AttributeError: + # python 3 + def iteritems(d): + return iter(d.items()) +else: + # python 2 + def iteritems(d): + return d.iteritems() diff --git a/src/libcharon/plugins/vici/python/vici/protocol.py b/src/libcharon/plugins/vici/python/vici/protocol.py index 88e1c3470..855a7b2e2 100644 --- a/src/libcharon/plugins/vici/python/vici/protocol.py +++ b/src/libcharon/plugins/vici/python/vici/protocol.py @@ -5,6 +5,7 @@ import struct from collections import namedtuple from collections import OrderedDict +from .compat import iteritems from .exception import DeserializationException @@ -51,6 +52,7 @@ class Packet(object): @classmethod def _named_request(cls, request_type, request, message=None): + request = request.encode() payload = struct.pack("!BB", request_type, len(request)) + request if message is not None: return payload + message @@ -93,22 +95,23 @@ class Message(object): @classmethod def serialize(cls, message): def encode_named_type(marker, name): - name = str(name) + name = name.encode() return struct.pack("!BB", marker, len(name)) + name def encode_blob(value): - value = str(value) + if not isinstance(value, bytes): + value = str(value).encode() return struct.pack("!H", len(value)) + value def serialize_list(lst): - segment = str() + segment = bytes() for item in lst: segment += struct.pack("!B", cls.LIST_ITEM) + encode_blob(item) return segment def serialize_dict(d): - segment = str() - for key, value in d.iteritems(): + segment = bytes() + for key, value in iteritems(d): if isinstance(value, dict): segment += ( encode_named_type(cls.SECTION_START, key) @@ -134,7 +137,7 @@ class Message(object): def deserialize(cls, stream): def decode_named_type(stream): length, = struct.unpack("!B", stream.read(1)) - return stream.read(length) + return stream.read(length).decode() def decode_blob(stream): length, = struct.unpack("!H", stream.read(2)) diff --git a/src/libcharon/plugins/vici/python/vici/session.py b/src/libcharon/plugins/vici/python/vici/session.py index 9f4dc5fa7..dee58699d 100644 --- a/src/libcharon/plugins/vici/python/vici/session.py +++ b/src/libcharon/plugins/vici/python/vici/session.py @@ -241,7 +241,7 @@ class SessionHandler(object): command_response = Message.deserialize(response.payload) if "success" in command_response: - if command_response["success"] != "yes": + if command_response["success"] != b"yes": raise CommandException( "Command failed: {errmsg}".format( errmsg=command_response["errmsg"] @@ -319,7 +319,7 @@ class SessionHandler(object): # evaluate command result, if any if "success" in command_response: - if command_response["success"] != "yes": + if command_response["success"] != b"yes": raise CommandException( "Command failed: {errmsg}".format( errmsg=command_response["errmsg"] -- cgit v1.2.3