Advancing on new UDS Actor

This commit is contained in:
Adolfo Gómez García 2019-11-27 12:09:50 +01:00
parent 282816b4eb
commit e76df6349d
9 changed files with 103 additions and 257 deletions

View File

@ -190,11 +190,20 @@ class REST:
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']
os = r['os']
return types.InitializationResultType(
own_token=r['own_token'],
unique_id=r['unique_id'],
max_idle=r['max_idle'],
os=r['os']
os=types.ActorOsConfigurationType(
action=os['action'],
name=os['name'],
username=os.get('username'),
password=os.get('password'),
new_password=os.get('new_password'),
ad=os.get('ad'),
ou=os.get('ou')
)
)
except requests.ConnectionError as e:
raise RESTConnectionError(str(e))
@ -206,3 +215,7 @@ class REST:
def ready(self, own_token: str, interfaces: typing.Iterable[types.InterfaceInfoType]) -> None:
# TODO: implement ready
return
def notifyIpChange(self, own_token: str, ip: str) -> None:
# TODO: implement notifyIpChange
return

View File

@ -32,19 +32,16 @@
import socket
import time
import random
import os
import subprocess
import shlex
import stat
import json
import typing
from . import platform
from . import rest
from . import types
from .script_thread import ScriptExecutorThread
from .utils import exceptionToMessage
# from .script_thread import ScriptExecutorThread
from .log import logger
@ -101,6 +98,36 @@ class CommonService:
def setReady(self) -> None:
if self._cfg.own_token and self._interfaces:
self._api.ready(self._cfg.own_token, self._interfaces)
# Cleans sensible data
if self._cfg.config:
self._cfg._replace(config=self._cfg.config._replace(os=None), data=None)
platform.store.writeConfig(self._cfg)
def configureMachine(self) -> bool:
# Retry configurations, config in case of error 10 times
counter = 1
while counter < 10 and self._isAlive:
counter += 1
try:
if self._cfg.config and self._cfg.config.os:
osData = self._cfg.config.os
if osData.action == 'rename':
self.rename(osData.name, osData.username, osData.password, osData.new_password)
elif osData.action == 'rename_ad':
self.joinDomain(osData.name, osData.ad or '', osData.ou or '', osData.username or '', osData.password or '')
if self._rebootRequested:
try:
platform.operations.reboot()
except Exception as e:
logger.error('Exception on reboot: {}'.format(e))
return False # Stops service
break
except Exception as e:
logger.error('Got exception operationg machine: {}'.format(e))
self.doWait(5000)
return True
def initialize(self) -> bool:
if not self._cfg.host: # Not configured
@ -110,203 +137,76 @@ class CommonService:
while self._isAlive:
if not self._interfaces:
self._interfaces = list(platform.operations.getNetworkInfo())
continue
if not self._interfaces: # Wait a bit for interfaces to get initialized...
self.doWait(5000)
continue
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))
logger.debug('This host is not managed by UDS Broker (ids: {})'.format(self._interfaces))
return False
self._cfg = self._cfg._replace(
master_token=None,
own_token=initResult.own_token,
config=types.ActorDataConfigurationType(
unique_id=initResult.unique_id,
max_idle=initResult.max_idle,
os=initResult.os
)
)
# On first successfull initialization request, master token will dissapear so it will be no more available (not needed anyway)
platform.store.writeConfig(self._cfg)
break # Initial configuration done..
except rest.RESTConnectionError:
logger.info('Trying to inititialize connection with broker (last error: {})'.format(exceptionToMessage(e)))
except rest.RESTConnectionError as e:
logger.info('Trying to inititialize connection with broker (last error: {})'.format(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
if self._cfg.runonce_command:
logger.info('Executing runOnce app: {}'.format(runOnce))
if self.execute(self._cfg.runonce_command, 'RunOnce'):
self._cfg.runonce_command = None
platform.store.writeConfig(self._cfg)
# operations.reboot()
return False
# Broker connection is initialized, now get information about what to
# do
counter = 0
while self.isAlive:
try:
logger.debug('Requesting information of what to do now')
info = self.api.information()
data = info.split('\r')
if len(data) != 2:
logger.error('The format of the information message is not correct (got {})'.format(info))
raise Exception
params = data[1].split('\t')
if data[0] == 'rename':
try:
if len(params) == 1: # Simple rename
logger.debug('Renaming computer to {}'.format(params[0]))
self.rename(params[0])
# Rename with change password for an user
elif len(params) == 4:
logger.debug('Renaming computer to {}'.format(params))
self.rename(params[0], params[1], params[2], params[3])
else:
logger.error('Got invalid parameter for rename operation: {}'.format(params))
return False
break
except Exception as e:
logger.error('Error at computer renaming stage: {}'.format(e.message))
return None # Will retry complete broker connection if this point is reached
elif data[0] == 'domain':
if len(params) != 5:
logger.error('Got invalid parameters for domain message: {}'.format(params))
return False # Stop running service
self.joinDomain(params[0], params[1], params[2], params[3], params[4])
break
else:
logger.error('Unrecognized action sent from broker: {}'.format(data[0]))
return False # Stop running service
except REST.UserServiceNotFoundError:
logger.error('The host has lost the sync state with broker! (host uuid changed?)')
return False
except Exception as err:
if counter % 60 == 0:
logger.warn('Too many retries in progress, though still trying (last error: {})'.format(exceptionToMessage(err)))
counter += 1
# Any other error is expectable and recoverable, so let's wait
# a bit and retry again
# Wait a bit before next check
self.doWait(5000)
if self.rebootRequested:
try:
operations.reboot()
except Exception as e:
logger.error('Exception on reboot: {}'.format(e.message))
return False # Stops service
self.configureMachine()
return True
def checkIpsChanged(self):
if self.api is None or self.api.uuid is None:
return # Not connected
netInfo = tuple(operations.getNetworkInfo())
for i in netInfo:
# If at least one ip has changed
if i.mac in self.knownIps and self.knownIps[i.mac] != i.ip:
logger.info('Notifying ip change to broker (mac {}, from {} to {})'.format(i.mac, self.knownIps[i.mac], i.ip))
try:
# Notifies all interfaces IPs
self.api.notifyIpChanges(((v.mac, v.ip) for v in netInfo))
# Regenerates Known ips
self.knownIps = dict(((v.mac, v.ip) for v in netInfo))
# And notify new listening address to broker
address = (self.knownIps[self.api.mac], self.httpServer.getPort())
# And new listening address
self.httpServer.restart(address)
# sends notification
self.api.notifyComm(self.httpServer.getServerUrl())
except Exception as e:
logger.warn('Got an error notifiying IPs to broker: {} (will retry in a bit)'.format(e.message.decode('windows-1250', 'ignore')))
def clientMessageProcessor(self, msg, data):
logger.debug('Got message {}'.format(msg))
if self.api is None:
logger.info('Rest api not ready')
if not self._cfg.own_token or not self._cfg.config or not self._cfg.config.unique_id:
# Not enouth data do check
return
if msg == ipc.REQ_LOGIN:
self.loggedIn = True
res = self.api.login(data).split('\t')
# third parameter, if exists, sets maxSession duration to this.
# First & second parameters are ip & hostname of connection source
if len(res) >= 3:
self.api.maxSession = int(res[2]) # Third parameter is max session duration
msg = ipc.REQ_INFORMATION # Senf information, requested or not, to client on login notification
if msg == ipc.REQ_LOGOUT and self.loggedIn is True:
self.loggedIn = False
self.api.logout(data)
self.onLogout(data)
if msg == ipc.REQ_INFORMATION:
info = {}
if self.api.idle is not None:
info['idle'] = self.api.idle
if self.api.maxSession is not None:
info['maxSession'] = self.api.maxSession
self.ipc.sendInformationMessage(info)
if msg == ipc.REQ_TICKET:
d = json.loads(data)
def locateMac(interfaces: typing.Iterable[types.InterfaceInfoType]) -> typing.Optional[types.InterfaceInfoType]:
try:
result = self.api.getTicket(d['ticketId'], d['secure'])
self.ipc.sendTicketMessage(result)
except Exception:
logger.exception('Getting ticket')
self.ipc.sendTicketMessage({'error': 'invalid ticket'})
return next(x for x in interfaces if x.mac.lower() == self._cfg.config.unique_id.lower())
except StopIteration:
return None
def initIPC(self):
# ******************************************
# * Initialize listener IPC & REST threads *
# ******************************************
logger.debug('Starting IPC listener at {}'.format(IPC_PORT))
self.ipc = ipc.ServerIPC(IPC_PORT, clientMessageProcessor=self.clientMessageProcessor)
self.ipc.start()
if self.api.mac in self.knownIps:
address = (self.knownIps[self.api.mac], random.randrange(43900, 44000))
logger.info('Starting REST listener at {}'.format(address))
self.httpServer = httpserver.HTTPServerThread(address, self)
self.httpServer.start()
# And notify it to broker
self.api.notifyComm(self.httpServer.getServerUrl())
def endIPC(self):
# Remove IPC threads
if self.ipc is not None:
try:
self.ipc.stop()
except Exception:
logger.error('Couln\'t stop ipc server')
if self.httpServer is not None:
try:
self.httpServer.stop()
except Exception:
logger.error('Couln\'t stop REST server')
def endAPI(self):
if self.api is not None:
try:
if self.loggedIn:
self.loggedIn = False
self.api.logout('service_stopped')
self.api.notifyComm(None)
except Exception as e:
logger.error('Couln\'t remove comms url from broker: {}'.format(e))
# self.notifyStop()
try:
oldIp = locateMac(self._interfaces)
newIp = locateMac(platform.operations.getNetworkInfo())
if not newIp:
raise Exception('No ip currently available for {}'.format(self._cfg.config.unique_id))
if oldIp != newIp:
self._api.notifyIpChange(self._cfg.own_token, newIp)
logger.info('Ip changed from {} to {}. Notified to UDS'.format(oldIp, newIp))
except Exception as e:
# No ip changed, log exception for info
logger.warn('Checking ips faield: {}'.format(e))
# ***************************************************
# Methods that ARE overriden by linux & windows Actor
# ***************************************************
def rename(self, name, user=None, oldPassword=None, newPassword=None):
def rename(self, name: str, user: typing.Optional[str] = None, oldPassword: typing.Optional[str] = None, newPassword: typing.Optional[str] = None):
'''
Invoked when broker requests a rename action
MUST BE OVERRIDEN
'''
raise NotImplementedError('Method renamed has not been implemented!')
def joinDomain(self, name, domain, ou, account, password):
def joinDomain(self, name: str, domain: str, ou: str, account: str, password: str):
'''
Invoked when broker requests a "domain" action
MUST BE OVERRIDEN
@ -317,7 +217,7 @@ class CommonService:
# Methods that CAN BE overriden by actors
# ****************************************
def notifyLocal(self):
self.setReady(operations.getComputerName())
self.setReady()
def doWait(self, miliseconds):
'''
@ -332,12 +232,12 @@ class CommonService:
'''
logger.info('Service is being stopped')
def preConnect(self, user, protocol):
def preConnect(self, user: str, protocol: str):
'''
Invoked when received a PRE Connection request via REST
'''
logger.debug('Pre-connect does nothing')
return 'ok'
def onLogout(self, user):
def onLogout(self, user: str):
logger.debug('On logout invoked for {}'.format(user))

View File

@ -2,8 +2,8 @@ import typing
class InterfaceInfoType(typing.NamedTuple):
name: str
mac: typing.Optional[str]
ip: typing.Optional[str]
mac: str
ip: str
class AuthenticatorType(typing.NamedTuple):
authId: str
@ -43,5 +43,8 @@ class ActorConfigurationType(typing.NamedTuple):
data: typing.Optional[typing.Dict[str, typing.Any]] = None
class InitializationResultType(ActorDataConfigurationType):
class InitializationResultType(typing.NamedTuple):
own_token: typing.Optional[str] = None
unique_id: typing.Optional[str] = None
max_idle: typing.Optional[int] = None
os: typing.Optional[ActorOsConfigurationType] = None

View File

@ -1,72 +0,0 @@
# -*- 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
'''
from __future__ import unicode_literals
import sys
import six
if sys.platform == 'win32':
_fromEncoding = 'windows-1250'
else:
_fromEncoding = 'utf-8'
def toUnicode(msg):
try:
if not isinstance(msg, six.text_type):
if isinstance(msg, six.binary_type):
return msg.decode(_fromEncoding, 'ignore')
return six.text_type(msg)
else:
return msg
except Exception:
try:
return six.text_type(msg)
except Exception:
return ''
def exceptionToMessage(e):
msg = ''
for arg in e.args:
if isinstance(arg, Exception):
msg = msg + exceptionToMessage(arg)
else:
msg = msg + toUnicode(arg) + '. '
return msg
class Bunch(dict):
def __init__(self, **kw):
dict.__init__(self, kw)
self.__dict__ = self

View File

@ -35,7 +35,7 @@
import logging
import typing
import dns
import dns.resolver
from django.utils.translation import ugettext_noop as _
from uds.core import auths
@ -126,6 +126,8 @@ class InternalDBAuth(auths.Authenticator):
groupsManager.validate([g.name for g in user.groups.all()])
def createUser(self, usrData):
pass
@staticmethod
def test(env, data):

View File

@ -111,7 +111,7 @@ class OSManager(Module):
def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]:
"""
This method provides information to actor, so actor can complete os configuration.
Currently exists 3 types of os managers
Currently exists 3 types of os managers actions
* rename vm and do NOT ADD to AD
{
'action': 'rename',
@ -119,7 +119,7 @@ class OSManager(Module):
}
* rename vm and ADD to AD
{
'action': 'renameAD',
'action': 'rename_ad',
'name': 'xxxxxxx',
'ad': 'domain.xxx'
'ou': 'ou' # or '' if default ou
@ -128,7 +128,7 @@ class OSManager(Module):
}
* rename vm, do NOT ADD to AD, and change password for an user
{
'action': 'rename_and_pw'
'action': 'rename'
'name': 'xxxxx'
'username': 'username to change pass'
'password': 'current password for username to change password'

View File

@ -92,7 +92,7 @@ class LinuxRandomPassManager(LinuxOsManager):
def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]:
return {
'action': 'rename_and_pw',
'action': 'rename',
'name': userService.getName(),
'username': self._userAccount,
'password': '', # On linux, user password is not needed so we provide an empty one

View File

@ -321,7 +321,7 @@ class WinDomainOsManager(WindowsOsManager):
def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]:
return {
'action': 'rename_and_pw',
'action': 'rename_ad',
'name': userService.getName(),
'ad': self._domain,
'ou': self._ou,

View File

@ -98,7 +98,7 @@ class WinRandomPassManager(WindowsOsManager):
def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]:
return {
'action': 'rename_and_pw',
'action': 'rename',
'name': userService.getName(),
'username': self._userAccount,
'password': self._password,