diff options
author | Joerg Steffens <joerg.steffens@bareos.com> | 2016-06-01 17:28:53 +0300 |
---|---|---|
committer | Joerg Steffens <joerg.steffens@bareos.com> | 2016-06-24 18:34:13 +0300 |
commit | 52708427992ef1a516e5f394a37b2b29cf3c3274 (patch) | |
tree | a68c92f6dbaaf19eee4a301979fcdb5b939dcd4c | |
parent | 907f1c1c2a4cd2104026cf31dd8eb4c1233f6457 (diff) |
bareos-fd-connect: improved integration
bareos-fd-connect now works with all FD commands, can cope with mutliple
command arguments (spaces have to be replaced by 0x1) and offers an interactive mode.
Under the hood, there is now a cleaner separation between
DirectorConsole and FileDaemon connections.
The classes BSock and BSockJson are deprecated.
You should use the new classes
* bareos.bsock.DirectorConsole
* bareos.bsock.DirectorConsoleJson
* bareos.bsock.FileDaemon
-rw-r--r-- | README.rst | 12 | ||||
-rw-r--r-- | bareos/bsock/__init__.py | 14 | ||||
-rw-r--r-- | bareos/bsock/bsock.py | 69 | ||||
-rwxr-xr-x | bareos/bsock/bsockjson.py | 85 | ||||
-rw-r--r-- | bareos/bsock/constants.py | 2 | ||||
-rw-r--r-- | bareos/bsock/directorconsole.py | 29 | ||||
-rwxr-xr-x | bareos/bsock/directorconsolejson.py | 57 | ||||
-rw-r--r-- | bareos/bsock/filedaemon.py | 30 | ||||
-rw-r--r-- | bareos/bsock/lowlevel.py | 129 | ||||
-rwxr-xr-x | bin/bareos-fd-connect.py | 6 | ||||
-rwxr-xr-x | bin/bconsole-json.py | 2 | ||||
-rwxr-xr-x | bin/bconsole.py | 2 |
12 files changed, 256 insertions, 181 deletions
@@ -14,8 +14,8 @@ calling bareos-director user agent commands import bareos.bsock password=bareos.bsock.Password("secret") - bsock=bareos.bsock.BSock(address="localhost", port=9101, password=password) - print bsock.call("help") + directorconsole=bareos.bsock.DirectorConsole(address="localhost", port=9101, password=password) + print directorconsole.call("help") ... @@ -26,8 +26,8 @@ simple version of the bconsole in Python import bareos.bsock password=bareos.bsock.Password("secret") - bconsole=bareos.BSock(address="localhost", port=9101, password=password) - bconsole.interactive() + directorconsole=bareos.bsock.DirectorConsole(address="localhost", port=9101, password=password) + directorconsole.interactive() ... use JSON objects of API mode 2 @@ -39,7 +39,7 @@ Requires: bareos >= 15.2 import bareos.bsock password=bareos.bsock.Password("secret") - bconsole=bareos.bsock.BSockJson(address="localhost", port=9101, password=password) - bconsole.call("list pools") + directorconsole=bareos.bsock.DirectorConsoleJson(address="localhost", port=9101, password=password) + directorconsole.call("list pools") ... diff --git a/bareos/bsock/__init__.py b/bareos/bsock/__init__.py index f1a845d..8984d21 100644 --- a/bareos/bsock/__init__.py +++ b/bareos/bsock/__init__.py @@ -1,6 +1,10 @@ #__all__ = [ "bconsole" ] -from bareos.exceptions import * -from bareos.util.password import Password -from bareos.bsock.connectiontype import ConnectionType -from bareos.bsock.bsock import BSock -from bareos.bsock.bsockjson import BSockJson +from bareos.exceptions import * +from bareos.util.password import Password +from bareos.bsock.connectiontype import ConnectionType +from bareos.bsock.directorconsole import DirectorConsole +from bareos.bsock.directorconsolejson import DirectorConsoleJson +from bareos.bsock.filedaemon import FileDaemon +# compat +from bareos.bsock.bsock import BSock +from bareos.bsock.bsockjson import BSockJson diff --git a/bareos/bsock/bsock.py b/bareos/bsock/bsock.py index e2357b8..51dd59d 100644 --- a/bareos/bsock/bsock.py +++ b/bareos/bsock/bsock.py @@ -1,69 +1,12 @@ """ Communicates with the bareos-director -""" - -from bareos.bsock.connectiontype import ConnectionType -from bareos.bsock.lowlevel import LowLevel -from bareos.exceptions import * -import sys - -class BSock(LowLevel): - '''use to send and receive the response from director''' - - def __init__(self, - address="localhost", - port=9101, - dirname=None, - name="*UserAgent*", - password=None, - type=ConnectionType.DIRECTOR): - super(BSock, self).__init__() - self.connect(address, port, dirname, type) - self.auth(name=name, password=password) - if type == ConnectionType.DIRECTOR: - self._set_state_director_prompt() +Legacy, use DirectorConsole instead. +""" - def call(self, command): - ''' - call a bareos-director user agent command - ''' - return self.__call(command, 0) - - def __call(self, command, count): - ''' - Call a bareos-director user agent command. - If connection is lost, try to reconnect. - ''' - result = b'' - try: - self.send(bytearray(command, 'utf-8')) - result = self.recv_msg() - except (SocketEmptyHeader, ConnectionLostError) as e: - self.logger.error("connection problem (%s): %s" % (type(e).__name__, str(e))) - if count == 0: - if self.reconnect(): - return self.__call(command, count+1) - return result - - def send_command(self, commamd): - return self.call(command) +from bareos.bsock.directorconsole import DirectorConsole +class BSock(DirectorConsole): - def interactive(self): - """ - Enter the interactive mode. - Exit via typing "exit" or "quit". - """ - self._set_state_director_prompt() - command = "" - while command != "exit" and command != "quit": - # Python2: raw_input, Python3: input - try: - myinput = raw_input - except NameError: - myinput = input - command = myinput(">>") - resultmsg = self.call(command) - sys.stdout.write(resultmsg.decode('utf-8')) - return True + def __init__(self, *args, **kwargs): + super(BSock, self).__init__(*args, **kwargs) diff --git a/bareos/bsock/bsockjson.py b/bareos/bsock/bsockjson.py index d0b5898..17b2934 100755 --- a/bareos/bsock/bsockjson.py +++ b/bareos/bsock/bsockjson.py @@ -1,83 +1,12 @@ """ -Reimplementation of the bconsole program in python. -""" - -from bareos.bsock.bsock import BSock -from pprint import pformat, pprint -import json - -class BSockJson(BSock): - """ - use to send and receive the response from director - """ - - def __init__(self, - address="localhost", - port=9101, - dirname=None, - name="*UserAgent*", - password=None): - super(BSockJson, self).__init__( - address, port, dirname, name, - password) - - - def call(self, command): - json = self.call_fullresult(command) - if json == None: - return - if 'result' in json: - result = json['result'] - else: - # TODO: or raise an exception? - result = json - return result - - - def call_fullresult(self, command): - resultstring = super(BSockJson, self).call(command) - data = None - if resultstring: - #print(resultstring.decode('utf-8')) - try: - data = json.loads(resultstring.decode('utf-8')) - except ValueError as e: - # in case result is not valid json, - # create a JSON-RPC wrapper - data = { - 'error': { - 'code': 2, - 'message': str(e), - 'data': resultstring - }, - } - return data +Communicates with the bareos-director using JSON results +Legacy, use DirectorConsoleJson instead. +""" - def interactive(self): - """ - Enter the interactive mode. - """ - self._set_state_director_prompt() - command = "" - while command != "exit" and command != "quit": - try: - myinput = raw_input - except NameError: - myinput = input - command = myinput(">>") - if command: - pprint(self.call(command)) - return True +from bareos.bsock.directorconsolejson import DirectorConsoleJson +class BSockJson(DirectorConsoleJson): - def _set_state_director_prompt(self): - result = False - if super(BSockJson, self)._set_state_director_prompt(): - # older version did not support compact mode, - # therfore first set api mode to json (which should always work in bareos >= 15.2.0) - # and then set api mode json compact (which should work with bareos >= 15.2.2) - self.call(".api json") - self.call(".api json compact=yes") - result = True - return result + def __init__(self, *args, **kwargs): + super(BSockJson, self).__init__(*args, **kwargs) diff --git a/bareos/bsock/constants.py b/bareos/bsock/constants.py index f51cd54..b4c0ba4 100644 --- a/bareos/bsock/constants.py +++ b/bareos/bsock/constants.py @@ -39,7 +39,7 @@ class Constants: BNET_EOD: "End of data stream, new data may follow", BNET_EOD_POLL: "End of data and poll all in one", BNET_STATUS: "Send full status", - BNET_TERMINATE: "Conversation terminated, doing close()", + BNET_TERMINATE: "Conversation terminated", BNET_POLL: "Poll request, I'm hanging on a read", BNET_HEARTBEAT: "Heartbeat Response requested", BNET_HB_RESPONSE: "Only response permited to HB", diff --git a/bareos/bsock/directorconsole.py b/bareos/bsock/directorconsole.py new file mode 100644 index 0000000..81e4f6c --- /dev/null +++ b/bareos/bsock/directorconsole.py @@ -0,0 +1,29 @@ +""" +Communicates with the bareos-dir console +""" + +from bareos.bsock.connectiontype import ConnectionType +from bareos.bsock.lowlevel import LowLevel + +class DirectorConsole(LowLevel): + '''use to send and receive the response to Bareos File Daemon''' + + def __init__(self, + address="localhost", + port=9101, + dirname=None, + name="*UserAgent*", + password=None): + super(DirectorConsole, self).__init__() + self.connect(address, port, dirname, ConnectionType.DIRECTOR) + self.auth(name=name, password=password, auth_success_regex=b'^1000 OK.*$') + self._init_connection() + + + def _init_connection(self): + self.call("autodisplay off") + + + def get_to_prompt(self): + self.send(b".") + return super(DirectorConsole, self).get_to_prompt() diff --git a/bareos/bsock/directorconsolejson.py b/bareos/bsock/directorconsolejson.py new file mode 100755 index 0000000..2dfa952 --- /dev/null +++ b/bareos/bsock/directorconsolejson.py @@ -0,0 +1,57 @@ +""" +Reimplementation of the bconsole program in python. +""" + +from bareos.bsock.directorconsole import DirectorConsole +from pprint import pformat, pprint +import json + +class DirectorConsoleJson(DirectorConsole): + """ + use to send and receive the response from director + """ + + def __init__(self, *args, **kwargs): + super(DirectorConsoleJson, self).__init__(*args, **kwargs) + + def _init_connection(self): + # older version did not support compact mode, + # therfore first set api mode to json (which should always work in bareos >= 15.2.0) + # and then set api mode json compact (which should work with bareos >= 15.2.2) + self.logger.debug(self.call(".api json")) + self.logger.debug(self.call(".api json compact=yes")) + + + def call(self, command): + json = self.call_fullresult(command) + if json == None: + return + if 'result' in json: + result = json['result'] + else: + # TODO: or raise an exception? + result = json + return result + + + def call_fullresult(self, command): + resultstring = super(DirectorConsoleJson, self).call(command) + data = None + if resultstring: + try: + data = json.loads(resultstring.decode('utf-8')) + except ValueError as e: + # in case result is not valid json, + # create a JSON-RPC wrapper + data = { + 'error': { + 'code': 2, + 'message': str(e), + 'data': resultstring + }, + } + return data + + + def _show_result(self, msg): + pprint(msg) diff --git a/bareos/bsock/filedaemon.py b/bareos/bsock/filedaemon.py index a5845d7..93a06e1 100644 --- a/bareos/bsock/filedaemon.py +++ b/bareos/bsock/filedaemon.py @@ -2,10 +2,12 @@ Communicates with the bareos-fd """ -from bareos.bsock.connectiontype import ConnectionType -from bareos.bsock import BSock +from bareos.bsock.connectiontype import ConnectionType +from bareos.bsock.lowlevel import LowLevel +import shlex -class FileDaemon(BSock): + +class FileDaemon(LowLevel): '''use to send and receive the response to Bareos File Daemon''' def __init__(self, @@ -13,6 +15,22 @@ class FileDaemon(BSock): port=9102, dirname=None, name=None, - password=None, - type=ConnectionType.FILEDAEMON): - super(FileDaemon, self).__init__(address, port, dirname, name, password, type) + password=None): + super(FileDaemon, self).__init__() + self.connect(address, port, dirname, ConnectionType.FILEDAEMON) + self.auth(name=name, password=password, auth_success_regex=b'^2000 OK Hello.*$') + self._init_connection() + + def call(self, command): + ''' + Replace spaces by char(1) in quoted arguments + and then call the original function. + ''' + if isinstance(command, list): + cmdlist=command + else: + cmdlist=shlex.split(command) + command0 = [] + for arg in cmdlist: + command0.append(arg.replace(" ", "\x01")) + return super(FileDaemon, self).call(command0) diff --git a/bareos/bsock/lowlevel.py b/bareos/bsock/lowlevel.py index 644c3de..5e5fd38 100644 --- a/bareos/bsock/lowlevel.py +++ b/bareos/bsock/lowlevel.py @@ -14,8 +14,11 @@ from bareos.bsock.protocolmessages import ProtocolMessages import hmac import logging import random +import re +from select import select import socket import struct +import sys import time class LowLevel(object): @@ -34,12 +37,9 @@ class LowLevel(object): self.socket = None self.auth_credentials_valid = False self.connection_type = None + self.receive_buffer = b'' - - def connect(self, address="localhost", port=9101, dirname=None, type=ConnectionType.DIRECTOR): - ''' - connect to bareos-director - ''' + def connect(self, address, port, dirname, type): self.address = address self.port = port if dirname: @@ -62,17 +62,18 @@ class LowLevel(object): return True - def auth(self, password, name="*UserAgent*"): + def auth(self, name, password, auth_success_regex): ''' login to the bareos-director if the authenticate success return True else False dir: the director location - name: own name. Default is *UserAgent* + name: own name. ''' if not isinstance(password, Password): raise AuthenticationError("password must by of type bareos.Password() not %s" % (type(password))) self.password = password self.name = name + self.auth_success_regex = auth_success_regex return self.__auth() @@ -86,10 +87,15 @@ class LowLevel(object): raise AuthenticationError("failed (in response)") if not self._cram_md5_challenge(clientname=self.name, password=self.password.md5(), tls_local_need=0, compatible=True): raise AuthenticationError("failed (in challenge)") + self.recv_msg(self.auth_success_regex) self.auth_credentials_valid = True return True + def _init_connection(self): + pass + + def disconnect(self): ''' disconnect ''' # TODO @@ -100,13 +106,43 @@ class LowLevel(object): result = False if self.auth_credentials_valid: try: - if self.__connect() and self.__auth() and self._set_state_director_prompt(): + if self.__connect() and self.__auth() and self._init_connection(): result = True except socket.error: self.logger.warning("failed to reconnect") return result + def call(self, command): + ''' + call a bareos-director user agent command + ''' + if isinstance(command, list): + command = " ".join(command) + return self.__call(command, 0) + + + def __call(self, command, count): + ''' + Send a command and receive the result. + If connection is lost, try to reconnect. + ''' + result = b'' + try: + self.send(bytearray(command, 'utf-8')) + result = self.recv_msg() + except (SocketEmptyHeader, ConnectionLostError) as e: + self.logger.error("connection problem (%s): %s" % (type(e).__name__, str(e))) + if count == 0: + if self.reconnect(): + return self.__call(command, count+1) + return result + + + def send_command(self, commamd): + return self.call(command) + + def send(self, msg=None): '''use socket to send request to director''' self.__check_socket_connection() @@ -133,10 +169,9 @@ class LowLevel(object): return msg - def recv_msg(self): + def recv_msg(self, regex = b'^\d\d\d\d OK.*$', timeout = None): '''will receive data from director ''' self.__check_socket_connection() - msg = b"" try: timeouts = 0 while True: @@ -154,11 +189,22 @@ class LowLevel(object): # header is a signal self.__set_status(header) if self.is_end_of_message(header): - return msg + result = self.receive_buffer + self.receive_buffer = b'' + return result else: # header is the length of the next message length = header - msg += self.recv_submsg(length) + self.receive_buffer += self.recv_submsg(length) + # Bareos indicates end of command result by line starting with 4 digits + match = re.search(regex, self.receive_buffer, re.MULTILINE) + if match: + self.logger.debug("msg \"{0}\" matches regex \"{1}\"".format(self.receive_buffer.strip(), regex)) + result = self.receive_buffer[0:match.end()] + self.receive_buffer = self.receive_buffer[match.end()+1:] + return result + #elif re.search("^\d\d\d\d .*$", msg, re.MULTILINE): + #return msg except socket.error as e: self._handleSocketError(e) return msg @@ -166,7 +212,7 @@ class LowLevel(object): def recv_submsg(self, length): # get the message - msg = b"" + msg = b'' while length > 0: self.logger.debug(" submsg len: " + str(length)) # TODO @@ -179,9 +225,41 @@ class LowLevel(object): msg = bytearray(msg.decode('utf-8'), 'utf-8') if (type(msg) is bytes): msg = bytearray(msg) + #self.logger.debug(str(msg)) return msg + def interactive(self): + """ + Enter the interactive mode. + Exit via typing "exit" or "quit". + """ + command = "" + while command != "exit" and command != "quit" and self.is_connected(): + command = self._get_input() + resultmsg = self.call(command) + self._show_result(resultmsg) + return True + + + def _get_input(self): + # Python2: raw_input, Python3: input + try: + myinput = raw_input + except NameError: + myinput = input + data = myinput(">>") + return data + + + def _show_result(self, msg): + #print(msg.decode('utf-8')) + sys.stdout.write(msg.decode('utf-8')) + # add a linefeed, if there isn't one already + if msg[-2] != ord(b'\n'): + sys.stdout.write(b'\n') + + def __get_header(self): self.__check_socket_connection() header = self.socket.recv(4) @@ -201,11 +279,17 @@ class LowLevel(object): def is_end_of_message(self, data): - return (data == Constants.BNET_EOD or + return ((not self.is_connected()) or + data == Constants.BNET_EOD or + data == Constants.BNET_TERMINATE or data == Constants.BNET_MAIN_PROMPT or data == Constants.BNET_SUB_PROMPT) + def is_connected(self): + return (self.status != Constants.BNET_TERMINATE) + + def _cram_md5_challenge(self, clientname, password, tls_local_need=0, compatible=True): ''' client launch the challenge, @@ -246,6 +330,7 @@ class LowLevel(object): # check the response is equal to base64 return is_correct + def _cram_md5_respond(self, password, tls_remote_need=0, compatible=True): ''' client connect to dir, @@ -291,10 +376,18 @@ class LowLevel(object): self.logger.debug(str(status_text) + " (" + str(status) + ")") - def _set_state_director_prompt(self): - self.send(b".") - msg = self.recv_msg() - self.logger.debug("received message: " + str(msg)) + def has_data(self): + self.__check_socket_connection() + timeout = 0.1 + readable, writable, exceptional = select([self.socket], [], [], timeout) + return readable + + + def get_to_prompt(self): + time.sleep(0.1) + if self.has_data(): + msg = self.recv_msg() + self.logger.debug("received message: " + str(msg)) # TODO: check prompt return True diff --git a/bin/bareos-fd-connect.py b/bin/bareos-fd-connect.py index 37ac402..7998638 100755 --- a/bin/bareos-fd-connect.py +++ b/bin/bareos-fd-connect.py @@ -13,7 +13,7 @@ def getArguments(): parser.add_argument('-p', '--password', help="password to authenticate to a Bareos File Daemon", required=True) parser.add_argument('--port', default=9102, help="Bareos File Daemon network port") parser.add_argument('address', nargs='?', default="localhost", help="Bareos File Daemon network address") - parser.add_argument('command', nargs='?', help="Command to send to the Bareos File Daemon") + parser.add_argument('command', nargs='*', help="Command to send to the Bareos File Daemon") args = parser.parse_args() return args @@ -43,4 +43,6 @@ if __name__ == '__main__': sys.exit(1) logger.debug( "authentication successful" ) if args.command: - print(bsock.call(args.command).decode('utf-8')) + bsock.call(args.command) + else: + bsock.interactive() diff --git a/bin/bconsole-json.py b/bin/bconsole-json.py index 3cca0f2..d04c7ab 100755 --- a/bin/bconsole-json.py +++ b/bin/bconsole-json.py @@ -37,7 +37,7 @@ if __name__ == '__main__': logger.debug('options: %s' % (parameter)) password = bareos.bsock.Password(args.password) parameter['password']=password - director = bareos.bsock.BSockJson(**parameter) + director = bareos.bsock.DirectorConsoleJson(**parameter) except RuntimeError as e: print(str(e)) sys.exit(1) diff --git a/bin/bconsole.py b/bin/bconsole.py index 1c8f0c8..64afb48 100755 --- a/bin/bconsole.py +++ b/bin/bconsole.py @@ -38,7 +38,7 @@ if __name__ == '__main__': logger.debug('options: %s' % (parameter)) password = bareos.bsock.Password(args.password) parameter['password']=password - director = bareos.bsock.BSock(**parameter) + director = bareos.bsock.DirectorConsole(**parameter) except RuntimeError as e: print(str(e)) sys.exit(1) |