Advancing on new UDS actor

This commit is contained in:
Adolfo Gómez 2019-11-26 15:13:07 +01:00
parent e967d994b1
commit 282816b4eb
13 changed files with 203 additions and 170 deletions

View File

@ -54,7 +54,7 @@ class UDSConfigDialog(QDialog):
def __init__(self):
QDialog.__init__(self, None)
# Get local config config
config: udsactor.types.ActorConfigurationType = udsactor.store.readConfig()
config: udsactor.types.ActorConfigurationType = udsactor.platform.store.readConfig()
self.ui = Ui_UdsActorSetupDialog()
self.ui.setupUi(self)
self.ui.host.setText(config.host)
@ -109,13 +109,13 @@ class UDSConfigDialog(QDialog):
def registerWithUDS(self):
# Get network card. Will fail if no network card is available, but don't mind (not contempled)
data: udsactor.types.InterfaceInfoType = next(udsactor.operations.getNetworkInfo())
data: udsactor.types.InterfaceInfoType = next(udsactor.platform.operations.getNetworkInfo())
try:
token = self.api.register(
self.ui.authenticators.currentData().auth,
self.ui.username.text(),
self.ui.password.text(),
udsactor.operations.getComputerName(),
udsactor.platform.operations.getComputerName(),
data.ip or '', # IP
data.mac or '', # MAC
self.ui.preCommand.text(),
@ -124,7 +124,7 @@ class UDSConfigDialog(QDialog):
self.ui.logLevelComboBox.currentIndex() # Loglevel
)
# Store parameters on register for later use, notify user of registration
udsactor.store.writeConfig(
udsactor.platform.store.writeConfig(
udsactor.types.ActorConfigurationType(
host=self.ui.host.text(),
validateCertificate=self.ui.validateCertificate.currentIndex() == 1,
@ -148,7 +148,7 @@ if __name__ == "__main__":
app = QApplication(sys.argv)
if udsactor.store.checkPermissions() is False:
if udsactor.platform.operations.checkPermissions() is False:
QMessageBox.critical(None, 'UDS Actor', 'This Program must be executed as administrator', QMessageBox.Ok)
sys.exit(1)

View File

@ -28,23 +28,11 @@
'''
@author: Adolfo Gómez, dkmaster at dkmon dot com
'''
import sys
from . import types
from . import rest
if sys.platform == 'win32':
from .windows import operations, store
else:
from .linux import operations, store
from .info import VERSION
from . import platform
__title__ = 'udsactor'
__version__ = VERSION
__build__ = 0x010756
__author__ = 'Adolfo Gómez <dkmaster@dkmon.com>'
__license__ = "BSD 3-clause"
__copyright__ = "Copyright 2014-2019 VirtualCable S.L.U."

View File

@ -25,12 +25,10 @@
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
@author: Adolfo Gómez, dkmaster at dkmon dot com
'''
from __future__ import unicode_literals
# pylint: disable=invalid-name
import logging
import os
import tempfile
@ -40,14 +38,16 @@ import six
OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in six.moves.xrange(6)) # @UndefinedVariable
class LocalLogger(object):
class LocalLogger: # pylint: disable=too-few-public-methods
linux = False
windows = True
def __init__(self):
# tempdir is different for "user application" and "service"
# service wil get c:\windows\temp, while user will get c:\users\XXX\temp
# Try to open logger at /var/log path
# If it fails (access denied normally), will try to open one at user's home folder, and if
# agaim it fails, open it at the tmpPath
for logDir in ('/var/log', os.path.expanduser('~'), tempfile.gettempdir()):
try:
fname = os.path.join(logDir, 'udsactor.log')
@ -66,15 +66,9 @@ class LocalLogger(object):
# Logger can't be set
self.logger = None
def log(self, level, message):
def log(self, level: int, message: str) -> None:
# Debug messages are logged to a file
# our loglevels are 10000 (other), 20000 (debug), ....
# logging levels are 10 (debug), 20 (info)
# OTHER = logging.NOTSET
self.logger.log(int(level / 1000) - 10, message)
def isWindows(self):
return False
def isLinux(self):
return True

View File

@ -106,6 +106,10 @@ def _getIpAndMac(ifname: str) -> typing.Tuple[typing.Optional[str], typing.Optio
return (ip, mac)
def checkPermissions() -> bool:
return os.getuid() == 0
def getComputerName() -> str:
'''
Returns computer name, with no domain

View File

@ -25,10 +25,10 @@
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
@author: Adolfo Gómez, dkmaster at dkmon dot com
'''
# pylint: disable=invalid-name
import os
import configparser
import base64
@ -38,15 +38,15 @@ from .. import types
CONFIGFILE = '/etc/udsactor/udsactor.cfg'
def checkPermissions() -> bool:
return os.getuid() == 0
def readConfig() -> types.ActorConfigurationType:
try:
cfg = configparser.ConfigParser()
cfg.read(CONFIGFILE)
uds: configparser.SectionProxy = cfg['uds']
# Extract data:
base64Config = uds.get('config', None)
config = pickle.loads(base64.b64decode(base64Config.encode())) if base64Config else None
base64Data = uds.get('data', None)
data = pickle.loads(base64.b64decode(base64Data.encode())) if base64Data else None
@ -59,6 +59,7 @@ def readConfig() -> types.ActorConfigurationType:
runonce_command=uds.get('runonce_command', None),
post_command=uds.get('post_command', None),
log_level=int(uds.get('log_level', '1')),
config=config,
data=data
)
except Exception:
@ -80,6 +81,9 @@ def writeConfig(config: types.ActorConfigurationType) -> None:
writeIfValue(config.post_command, 'post_command')
writeIfValue(config.runonce_command, 'runonce_command')
uds['log_level'] = str(config.log_level)
if config.config: # Special case, encoded & dumped
uds['config'] = base64.b64encode(pickle.dumps(config.config)).decode()
if config.data: # Special case, encoded & dumped
uds['data'] = base64.b64encode(pickle.dumps(config.data)).decode()

View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
@author: Adolfo Gómez, dkmaster at dkmon dot com
'''
import sys
if sys.platform == 'win32':
from .windows import operations, store # pylint: disable=unused-import
else:
from .linux import operations, store # pylint: disable=unused-import

View File

@ -37,6 +37,7 @@ import typing
import requests
from . import types
from .info import VERSION
class RESTError(Exception):
ERRCODE = 0
@ -177,3 +178,31 @@ class REST:
pass
raise RESTError(result.content)
def initialize(self, token: str, interfaces: typing.Iterable[types.InterfaceInfoType]) -> types.InitializationResultType:
# Generate id list from netork cards
payload = {
'token': token,
'version': VERSION,
'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces]
}
try:
result = requests.post(self.url + 'actor/v2/initialize', data=json.dumps(payload), headers=self._headers, verify=self.validateCert)
if result.ok:
r = result.json()['result']
return types.InitializationResultType(
own_token=r['own_token'],
unique_id=r['unique_id'],
max_idle=r['max_idle'],
os=r['os']
)
except requests.ConnectionError as e:
raise RESTConnectionError(str(e))
except Exception:
pass
raise RESTError(result.content)
def ready(self, own_token: str, interfaces: typing.Iterable[types.InterfaceInfoType]) -> None:
# TODO: implement ready
return

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 201 Virtual Cable S.L.
# Copyright (c) 2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -25,27 +25,23 @@
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
@author: Adolfo Gómez, dkmaster at dkmon dot com
'''
# pylint: disable-msg=E1101,W0703
# pylint: disable=invalid-name
import threading
from udsactor.log import logger
import threading
import six
class ScriptExecutorThread(threading.Thread):
def __init__(self, script):
def __init__(self, script: str) -> None:
super(ScriptExecutorThread, self).__init__()
self.script = script
def run(self):
def run(self) -> None:
try:
logger.debug('Executing script: {}'.format(self.script))
six.exec_(self.script, globals(), None)
exec(self.script, globals(), None) # pylint: disable=exec-used
except Exception as e:
logger.error('Error executing script: {}'.format(e))

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014 Virtual Cable S.L.
# Copyright (c) 2014-2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -25,21 +25,10 @@
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
@author: Adolfo Gómez, dkmaster at dkmon dot com
'''
from __future__ import unicode_literals
from udsactor.log import logger
from . import operations
from . import store
from . import REST
from . import ipc
from . import httpserver
from .scriptThread import ScriptExecutorThread
from .utils import exceptionToMessage
# pylint: disable=invalid-name
import socket
import time
@ -49,51 +38,39 @@ import subprocess
import shlex
import stat
import json
import typing
IPC_PORT = 39188
cfg = None
from . import platform
from . import rest
from . import types
from .script_thread import ScriptExecutorThread
from .utils import exceptionToMessage
from .log import logger
def initCfg():
global cfg # pylint: disable=global-statement
cfg = store.readConfig()
# def setup() -> None:
# cfg = platform.store.readConfig()
if logger.logger.isWindows():
# Logs will also go to windows event log for services
logger.logger.serviceLogger = True
# if logger.logger.windows:
# # Logs will also go to windows event log for services
# logger.logger.serviceLogger = True
if cfg is not None:
logger.setLevel(cfg.get('logLevel', 20000))
else:
logger.setLevel(20000)
cfg = {}
# If ANY var is missing, reset cfg
for v in ('host', 'ssl', 'masterKey'):
if v not in cfg:
cfg = None
break
return cfg
# if cfg.x:
# logger.setLevel(cfg.get('logLevel', 20000))
# else:
# logger.setLevel(20000)
class CommonService(object):
class CommonService:
_isAlive: bool = True
_rebootRequested: bool = False
_loggedIn = False
_cfg: types.ActorConfigurationType
_api: rest.REST
_interfaces: typing.List[types.InterfaceInfoType]
def __init__(self):
self.isAlive = True
self.api = None
self.ipc = None
self.httpServer = None
self.rebootRequested = False
self.knownIps = []
self.loggedIn = False
socket.setdefaulttimeout(20)
def reboot(self):
self.rebootRequested = True
def execute(self, cmdLine, section): # pylint: disable=no-self-use
@staticmethod
def execute(cmdLine: str, section: str):
cmd = shlex.split(cmdLine, posix=False)
if os.path.isfile(cmd[0]):
@ -105,70 +82,59 @@ class CommonService(object):
return False
logger.info('Result of executing cmd was {}'.format(res))
return True
else:
logger.error('{} file exists but it it is not executable (needs execution permission by admin/root)'.format(section))
logger.error('{} file exists but it it is not executable (needs execution permission by admin/root)'.format(section))
else:
logger.error('{} file not found & not executed'.format(section))
return False
def setReady(self):
self.api.setReady([(v.mac, v.ip) for v in operations.getNetworkInfo()])
def __init__(self):
self._cfg = platform.store.readConfig()
self._interfaces = []
self._api = rest.REST(self._cfg.host, self._cfg.validateCert)
def interactWithBroker(self):
'''
Returns True to continue to main loop, false to stop & exit service
'''
# If no configuration is found, stop service
if cfg is None:
logger.fatal('No configuration found, stopping service')
socket.setdefaulttimeout(20)
def reboot(self) -> None:
self._rebootRequested = True
def setReady(self) -> None:
if self._cfg.own_token and self._interfaces:
self._api.ready(self._cfg.own_token, self._interfaces)
def initialize(self) -> bool:
if not self._cfg.host: # Not configured
return False
self.api = REST.Api(cfg['host'], cfg['masterKey'], cfg['ssl'])
# Wait for Broker to be ready
counter = 0
while self.isAlive:
try:
# getNetworkInfo is a generator function
netInfo = tuple(operations.getNetworkInfo())
self.knownIps = dict(((i.mac, i.ip) for i in netInfo))
ids = ','.join([i.mac for i in netInfo])
if ids == '':
# Wait for any network interface to be ready
logger.debug('No valid network interfaces found, retrying in a while...')
raise Exception()
logger.debug('Ids: {}'.format(ids))
self.api.init(ids)
# Set remote logger to notify log info to broker
logger.setRemoteLogger(self.api)
while self._isAlive:
if not self._interfaces:
self._interfaces = list(platform.operations.getNetworkInfo())
continue
break
except REST.InvalidKeyError:
logger.fatal('Can\'t sync with broker: Invalid broker Master Key')
return False
except REST.UnmanagedHostError:
# Maybe interface that is registered with broker is not enabled already?
# Right now, we thing that the interface connected to broker is
# the interface that broker will know, let's see how this works
logger.fatal('This host is not managed by UDS Broker (ids: {})'.format(ids))
return False # On unmanaged hosts, there is no reason right now to continue running
except Exception as e:
logger.debug('Exception on network info: retrying')
# Any other error is expectable and recoverable, so let's wait a bit and retry again
# but, if too many errors, will log it (one every minute, for
# example)
counter += 1
if counter % 60 == 0: # Every 5 minutes, raise a log
logger.info('Trying to inititialize connection with broker (last error: {})'.format(exceptionToMessage(e)))
# Wait a bit before next check
self.doWait(5000)
try:
# If master token is present, initialize and get configuration data
if self._cfg.master_token:
initResult: types.InitializationResultType = self._api.initialize(self._cfg.master_token, self._interfaces)
if not initResult.own_token: # Not managed
logger.fatal('This host is not managed by UDS Broker (ids: {})'.format(ids))
return False
break # Initial configuration done..
except rest.RESTConnectionError:
logger.info('Trying to inititialize connection with broker (last error: {})'.format(exceptionToMessage(e)))
self.doWait(5000) # Wait a bit and retry
except rest.RESTError as e: # Invalid key?
logger.error('Error validating with broker. (Invalid token?): {}'.format(e))
self._cfg.own_token = initResult.own_token
self._cfg.master_token = None
# Now try to run the "runonce" element
runOnce = store.runApplication()
if runOnce is not None:
if self._cfg.runonce_command:
logger.info('Executing runOnce app: {}'.format(runOnce))
if self.execute(runOnce, 'RunOnce') is True:
if self.execute(self._cfg.runonce_command, 'RunOnce'):
self._cfg.runonce_command = None
platform.store.writeConfig(self._cfg)
# operations.reboot()
return False

View File

@ -13,6 +13,20 @@ class AuthenticatorType(typing.NamedTuple):
priority: int
isCustom: bool
class ActorOsConfigurationType(typing.NamedTuple):
action: str
name: str
username: typing.Optional[str] = None
password: typing.Optional[str] = None
new_password: typing.Optional[str] = None
ad: typing.Optional[str] = None
ou: typing.Optional[str] = None
class ActorDataConfigurationType(typing.NamedTuple):
unique_id: typing.Optional[str] = None
max_idle: typing.Optional[int] = None
os: typing.Optional[ActorOsConfigurationType] = None
class ActorConfigurationType(typing.NamedTuple):
host: str
validateCertificate: bool
@ -25,4 +39,9 @@ class ActorConfigurationType(typing.NamedTuple):
log_level: int = 0
data: typing.Optional[typing.Dict[str, str]] = None
config: typing.Optional[ActorDataConfigurationType] = None
data: typing.Optional[typing.Dict[str, typing.Any]] = None
class InitializationResultType(ActorDataConfigurationType):
own_token: typing.Optional[str] = None

View File

@ -25,35 +25,40 @@
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
@author: Adolfo Gómez, dkmaster at dkmon dot com
'''
from __future__ import unicode_literals
import servicemanager # @UnresolvedImport, pylint: disable=import-error
# pylint: disable=invalid-name
import logging
import os
import tempfile
import servicemanager # @UnresolvedImport, pylint: disable=import-error
# Valid logging levels, from UDS Broker (uds.core.utils.log)
OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in range(6))
class LocalLogger: # pylint: disable=too-few-public-methods
linux = False
windows = True
class LocalLogger(object):
def __init__(self):
# tempdir is different for "user application" and "service"
# service wil get c:\windows\temp, while user will get c:\users\XXX\temp
logging.basicConfig(
filename=os.path.join(tempfile.gettempdir(), 'udsactor.log'),
filemode='a',
format='%(levelname)s %(asctime)s %(message)s',
level=logging.INFO
)
try:
logging.basicConfig(
filename=os.path.join(tempfile.gettempdir(), 'udsactor.log'),
filemode='a',
format='%(levelname)s %(asctime)s %(message)s',
level=logging.INFO
)
except Exception:
logging.basicConfig() # basic init
self.logger = logging.getLogger('udsactor')
self.serviceLogger = False
def log(self, level, message):
def log(self, level: int, message: str) -> None:
# Debug messages are logged to a file
# our loglevels are 10000 (other), 20000 (debug), ....
# logging levels are 10 (debug), 20 (info)
@ -69,9 +74,3 @@ class LocalLogger(object):
servicemanager.LogWarningMsg(message)
else: # Error & Fatal
servicemanager.LogErrorMsg(message)
def isWindows(self):
return True
def isLinux(self):
return False

View File

@ -35,6 +35,7 @@ from ctypes.wintypes import DWORD, LPCWSTR
import typing
import win32com.client
from win32com.shell import shell # pylint: disable=no-name-in-module,import-error
import win32net
import win32security
import win32api
@ -43,6 +44,9 @@ import win32con
from .. import types
from ..log import logger
def checkPermissions() -> bool:
return shell.IsUserAnAdmin()
def getErrorMessage(resultCode: int = 0) -> str:
# sys_fs_enc = sys.getfilesystemencoding() or 'mbcs'
msg = win32api.FormatMessage(resultCode)

View File

@ -34,16 +34,11 @@ import pickle
import winreg as wreg
import win32security
from win32com.shell import shell # pylint: disable=no-name-in-module,import-error
from .. import types
PATH = 'Software\\UDSActor'
BASEKEY = wreg.HKEY_LOCAL_MACHINE
def checkPermissions() -> bool:
return shell.IsUserAnAdmin()
def fixRegistryPermissions(handle) -> None:
# Fix permissions so users can't read this key
v = win32security.GetSecurityInfo(handle, win32security.SE_REGISTRY_KEY, win32security.DACL_SECURITY_INFORMATION)
@ -65,7 +60,6 @@ def fixRegistryPermissions(handle) -> None:
None
)
def readConfig() -> types.ActorConfigurationType:
try:
key = wreg.OpenKey(BASEKEY, PATH, 0, wreg.KEY_QUERY_VALUE) # @UndefinedVariable