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) result = requests.post(self.url + 'actor/v2/initialize', data=json.dumps(payload), headers=self._headers, verify=self.validateCert)
if result.ok: if result.ok:
r = result.json()['result'] r = result.json()['result']
os = r['os']
return types.InitializationResultType( return types.InitializationResultType(
own_token=r['own_token'], own_token=r['own_token'],
unique_id=r['unique_id'], unique_id=r['unique_id'],
max_idle=r['max_idle'], 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: except requests.ConnectionError as e:
raise RESTConnectionError(str(e)) raise RESTConnectionError(str(e))
@ -206,3 +215,7 @@ class REST:
def ready(self, own_token: str, interfaces: typing.Iterable[types.InterfaceInfoType]) -> None: def ready(self, own_token: str, interfaces: typing.Iterable[types.InterfaceInfoType]) -> None:
# TODO: implement ready # TODO: implement ready
return return
def notifyIpChange(self, own_token: str, ip: str) -> None:
# TODO: implement notifyIpChange
return

View File

@ -32,19 +32,16 @@
import socket import socket
import time import time
import random
import os import os
import subprocess import subprocess
import shlex import shlex
import stat import stat
import json
import typing import typing
from . import platform from . import platform
from . import rest from . import rest
from . import types from . import types
from .script_thread import ScriptExecutorThread # from .script_thread import ScriptExecutorThread
from .utils import exceptionToMessage
from .log import logger from .log import logger
@ -101,6 +98,36 @@ class CommonService:
def setReady(self) -> None: def setReady(self) -> None:
if self._cfg.own_token and self._interfaces: if self._cfg.own_token and self._interfaces:
self._api.ready(self._cfg.own_token, 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: def initialize(self) -> bool:
if not self._cfg.host: # Not configured if not self._cfg.host: # Not configured
@ -110,203 +137,76 @@ class CommonService:
while self._isAlive: while self._isAlive:
if not self._interfaces: if not self._interfaces:
self._interfaces = list(platform.operations.getNetworkInfo()) self._interfaces = list(platform.operations.getNetworkInfo())
continue if not self._interfaces: # Wait a bit for interfaces to get initialized...
self.doWait(5000)
continue
try: try:
# If master token is present, initialize and get configuration data # If master token is present, initialize and get configuration data
if self._cfg.master_token: if self._cfg.master_token:
initResult: types.InitializationResultType = self._api.initialize(self._cfg.master_token, self._interfaces) initResult: types.InitializationResultType = self._api.initialize(self._cfg.master_token, self._interfaces)
if not initResult.own_token: # Not managed 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 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.. break # Initial configuration done..
except rest.RESTConnectionError: except rest.RESTConnectionError as e:
logger.info('Trying to inititialize connection with broker (last error: {})'.format(exceptionToMessage(e))) logger.info('Trying to inititialize connection with broker (last error: {})'.format(e))
self.doWait(5000) # Wait a bit and retry self.doWait(5000) # Wait a bit and retry
except rest.RESTError as e: # Invalid key? except rest.RESTError as e: # Invalid key?
logger.error('Error validating with broker. (Invalid token?): {}'.format(e)) logger.error('Error validating with broker. (Invalid token?): {}'.format(e))
self._cfg.own_token = initResult.own_token self.configureMachine()
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
return True return True
def checkIpsChanged(self): def checkIpsChanged(self):
if self.api is None or self.api.uuid is None: if not self._cfg.own_token or not self._cfg.config or not self._cfg.config.unique_id:
return # Not connected # Not enouth data do check
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')
return return
if msg == ipc.REQ_LOGIN: def locateMac(interfaces: typing.Iterable[types.InterfaceInfoType]) -> typing.Optional[types.InterfaceInfoType]:
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)
try: try:
result = self.api.getTicket(d['ticketId'], d['secure']) return next(x for x in interfaces if x.mac.lower() == self._cfg.config.unique_id.lower())
self.ipc.sendTicketMessage(result) except StopIteration:
except Exception: return None
logger.exception('Getting ticket')
self.ipc.sendTicketMessage({'error': 'invalid ticket'})
def initIPC(self): try:
# ****************************************** oldIp = locateMac(self._interfaces)
# * Initialize listener IPC & REST threads * newIp = locateMac(platform.operations.getNetworkInfo())
# ****************************************** if not newIp:
logger.debug('Starting IPC listener at {}'.format(IPC_PORT)) raise Exception('No ip currently available for {}'.format(self._cfg.config.unique_id))
self.ipc = ipc.ServerIPC(IPC_PORT, clientMessageProcessor=self.clientMessageProcessor) if oldIp != newIp:
self.ipc.start() self._api.notifyIpChange(self._cfg.own_token, newIp)
logger.info('Ip changed from {} to {}. Notified to UDS'.format(oldIp, newIp))
if self.api.mac in self.knownIps: except Exception as e:
address = (self.knownIps[self.api.mac], random.randrange(43900, 44000)) # No ip changed, log exception for info
logger.info('Starting REST listener at {}'.format(address)) logger.warn('Checking ips faield: {}'.format(e))
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()
# *************************************************** # ***************************************************
# Methods that ARE overriden by linux & windows Actor # 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 Invoked when broker requests a rename action
MUST BE OVERRIDEN MUST BE OVERRIDEN
''' '''
raise NotImplementedError('Method renamed has not been implemented!') 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 Invoked when broker requests a "domain" action
MUST BE OVERRIDEN MUST BE OVERRIDEN
@ -317,7 +217,7 @@ class CommonService:
# Methods that CAN BE overriden by actors # Methods that CAN BE overriden by actors
# **************************************** # ****************************************
def notifyLocal(self): def notifyLocal(self):
self.setReady(operations.getComputerName()) self.setReady()
def doWait(self, miliseconds): def doWait(self, miliseconds):
''' '''
@ -332,12 +232,12 @@ class CommonService:
''' '''
logger.info('Service is being stopped') 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 Invoked when received a PRE Connection request via REST
''' '''
logger.debug('Pre-connect does nothing') logger.debug('Pre-connect does nothing')
return 'ok' return 'ok'
def onLogout(self, user): def onLogout(self, user: str):
logger.debug('On logout invoked for {}'.format(user)) logger.debug('On logout invoked for {}'.format(user))

View File

@ -2,8 +2,8 @@ import typing
class InterfaceInfoType(typing.NamedTuple): class InterfaceInfoType(typing.NamedTuple):
name: str name: str
mac: typing.Optional[str] mac: str
ip: typing.Optional[str] ip: str
class AuthenticatorType(typing.NamedTuple): class AuthenticatorType(typing.NamedTuple):
authId: str authId: str
@ -43,5 +43,8 @@ class ActorConfigurationType(typing.NamedTuple):
data: typing.Optional[typing.Dict[str, typing.Any]] = None data: typing.Optional[typing.Dict[str, typing.Any]] = None
class InitializationResultType(ActorDataConfigurationType): class InitializationResultType(typing.NamedTuple):
own_token: typing.Optional[str] = None 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 logging
import typing import typing
import dns import dns.resolver
from django.utils.translation import ugettext_noop as _ from django.utils.translation import ugettext_noop as _
from uds.core import auths from uds.core import auths
@ -126,6 +126,8 @@ class InternalDBAuth(auths.Authenticator):
groupsManager.validate([g.name for g in user.groups.all()]) groupsManager.validate([g.name for g in user.groups.all()])
def createUser(self, usrData):
pass
@staticmethod @staticmethod
def test(env, data): def test(env, data):

View File

@ -111,7 +111,7 @@ class OSManager(Module):
def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]: def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]:
""" """
This method provides information to actor, so actor can complete os configuration. 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 * rename vm and do NOT ADD to AD
{ {
'action': 'rename', 'action': 'rename',
@ -119,7 +119,7 @@ class OSManager(Module):
} }
* rename vm and ADD to AD * rename vm and ADD to AD
{ {
'action': 'renameAD', 'action': 'rename_ad',
'name': 'xxxxxxx', 'name': 'xxxxxxx',
'ad': 'domain.xxx' 'ad': 'domain.xxx'
'ou': 'ou' # or '' if default ou '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 * rename vm, do NOT ADD to AD, and change password for an user
{ {
'action': 'rename_and_pw' 'action': 'rename'
'name': 'xxxxx' 'name': 'xxxxx'
'username': 'username to change pass' 'username': 'username to change pass'
'password': 'current password for username to change password' '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]: def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]:
return { return {
'action': 'rename_and_pw', 'action': 'rename',
'name': userService.getName(), 'name': userService.getName(),
'username': self._userAccount, 'username': self._userAccount,
'password': '', # On linux, user password is not needed so we provide an empty one '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]: def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]:
return { return {
'action': 'rename_and_pw', 'action': 'rename_ad',
'name': userService.getName(), 'name': userService.getName(),
'ad': self._domain, 'ad': self._domain,
'ou': self._ou, 'ou': self._ou,

View File

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