forked from shaba/openuds
Advancing on new UDS Actor
This commit is contained in:
parent
282816b4eb
commit
e76df6349d
@ -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
|
@ -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,6 +137,8 @@ class CommonService:
|
||||
while self._isAlive:
|
||||
if not self._interfaces:
|
||||
self._interfaces = list(platform.operations.getNetworkInfo())
|
||||
if not self._interfaces: # Wait a bit for interfaces to get initialized...
|
||||
self.doWait(5000)
|
||||
continue
|
||||
|
||||
try:
|
||||
@ -117,196 +146,67 @@ class CommonService:
|
||||
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)
|
||||
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:
|
||||
logger.error('Couln\'t remove comms url from broker: {}'.format(e))
|
||||
|
||||
# self.notifyStop()
|
||||
# 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))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user