Setting up new actor based ofc on old one

This commit is contained in:
Adolfo Gómez 2019-11-20 09:23:35 +01:00
parent 37229e92e7
commit 04d204883b
28 changed files with 3882 additions and 0 deletions

3
actor/src/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
dist
build
*.spec

View File

@ -0,0 +1,39 @@
# -*- 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
'''
VERSION = '3.0.0'
__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."

101
actor/src/udsactor/certs.py Normal file
View File

@ -0,0 +1,101 @@
# -*- 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 tempfile import gettempdir
from os.path import exists, join
CERTFILE = 'UDSActor.pem'
def createSelfSignedCert(force=False):
certFile = join(gettempdir(), CERTFILE)
if exists(certFile) and not force:
return certFile
certData = '''-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCb50K3mIznNklz
yVAD7xSQOSJQ6+NPXj7U9/4zLZ+TvmbQ7RqUUsxbfxHbeRnoYTWV2nKk4+tHqmvz
ujLSS/loFhTSMqtrLn7rowSYJoQhKOUkAiQlWkqCfItWgL5pJopDpNHFul9Rn3ds
PMWQTiGeUNR4Y3RnBhr1Q1BsqAzf4m6zFUmgLPPmVLdF4uJ3Tuz8TSy2gWLs5aSr
5do4WamwUfYjRSVMJECmwjUM4rQ8SQgg0sHBeBuDUGNBvBQFac1G7qUcMReeu8Zr
DUtMsXma/l4rA8NB5CRmTrQbTBF4l+jb2BDFebDqDUK1Oqs9X35yOQfDOAFYHiix
PX0IsXOZAgMBAAECggEBAJi3000RrIUZUp6Ph0gzPMuCjDEEwWiQA7CPNX1gpb8O
dp0WhkDhUroWIaICYPSXtOwUTtVjRqivMoxPy1Thg3EIoGC/rdeSdlXRHMEGicwJ
yVyalFnatr5Xzg5wkxVh4XMd0zeDt7e3JD7s0QLo5lm1CEzd77qz6lhzFic5/1KX
bzdULtTlq60dazg2hEbcS4OmM1UMCtRVDAsOIUIZPL0M9j1C1d1iEdYnh2xshKeG
/GOfo95xsgdMlGjtv3hUT5ryKVoEsu+36rGb4VfhPfUvvoVbRx5QZpW+QvxaYh5E
Fi0JEROozFwG31Y++8El7J3yQko8cFBa1lYYUwwpNAECgYEAykT+GiM2YxJ4uVF1
OoKiE9BD53i0IG5j87lGPnWqzEwYBwnqjEKDTou+uzMGz3MDV56UEFNho7wUWh28
LpEkjJB9QgbsugjxIBr4JoL/rYk036e/6+U8I95lvYWrzb+rBMIkRDYI7kbQD/mQ
piYUpuCkTymNAu2RisK6bBzJslkCgYEAxVE23OQvkCeOV8hJNPZGpJ1mDS+TiOow
oOScMZmZpail181eYbAfMsCr7ri812lSj98NvA2GNVLpddil6LtS1cQ5p36lFBtV
xQUMZiFz4qVbEak+izL+vPaev/mXXsOcibAIQ+qI/0txFpNhJjpaaSy6vRCBYFmc
8pgSoBnBI0ECgYAUKCn2atnpp5aWSTLYgNosBU4vDA1PShD14dnJMaqyr0aZtPhF
v/8b3btFJoGgPMLxgWEZ+2U4ju6sSFhPf7FXvLJu2QfQRkHZRDbEh7t5DLpTK4Fp
va9vl6Ml7uM/HsGpOLuqfIQJUs87OFCc7iCSvMJDDU37I7ekT2GKkpfbCQKBgBrE
0NeY0WcSJrp7/oqD2sOcYurpCG/rrZs2SIZmGzUhMxaa0vIXzbO59dlWELB8pmnE
Tf20K//x9qA5OxDe0PcVPukdQlH+/1zSOYNliG44FqnHtyd1TJ/gKVtMBiAiE4uO
aSClod5Yosf4SJbCFd/s5Iyfv52NqsAyp1w3Aj/BAoGAVCnEiGUfyHlIR+UH4zZW
GXJMeqdZLfcEIszMxLePkml4gUQhoq9oIs/Kw+L1DDxUwzkXN4BNTlFbOSu9gzK1
dhuIUGfS6RPL88U+ivC3A0y2jT43oUMqe3hiRt360UQ1GXzp2dMnR9odSRB1wHoO
IOjEBZ8341/c9ZHc5PCGAG8=
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIID7zCCAtegAwIBAgIJAIrEIthCfxUCMA0GCSqGSIb3DQEBCwUAMIGNMQswCQYD
VQQGEwJFUzEPMA0GA1UECAwGTWFkcmlkMREwDwYDVQQHDAhBbGNvcmNvbjEMMAoG
A1UECgwDVURTMQ4wDAYDVQQLDAVBY3RvcjESMBAGA1UEAwwJVURTIEFjdG9yMSgw
JgYJKoZIhvcNAQkBFhlzdXBwb3J0QHVkc2VudGVycHJpc2UuY29tMB4XDTE0MTAy
NjIzNDEyNFoXDTI0MTAyMzIzNDEyNFowgY0xCzAJBgNVBAYTAkVTMQ8wDQYDVQQI
DAZNYWRyaWQxETAPBgNVBAcMCEFsY29yY29uMQwwCgYDVQQKDANVRFMxDjAMBgNV
BAsMBUFjdG9yMRIwEAYDVQQDDAlVRFMgQWN0b3IxKDAmBgkqhkiG9w0BCQEWGXN1
cHBvcnRAdWRzZW50ZXJwcmlzZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
ggEKAoIBAQCb50K3mIznNklzyVAD7xSQOSJQ6+NPXj7U9/4zLZ+TvmbQ7RqUUsxb
fxHbeRnoYTWV2nKk4+tHqmvzujLSS/loFhTSMqtrLn7rowSYJoQhKOUkAiQlWkqC
fItWgL5pJopDpNHFul9Rn3dsPMWQTiGeUNR4Y3RnBhr1Q1BsqAzf4m6zFUmgLPPm
VLdF4uJ3Tuz8TSy2gWLs5aSr5do4WamwUfYjRSVMJECmwjUM4rQ8SQgg0sHBeBuD
UGNBvBQFac1G7qUcMReeu8ZrDUtMsXma/l4rA8NB5CRmTrQbTBF4l+jb2BDFebDq
DUK1Oqs9X35yOQfDOAFYHiixPX0IsXOZAgMBAAGjUDBOMB0GA1UdDgQWBBRShS90
5lJTNvYPIEqP3GxWwG5iiDAfBgNVHSMEGDAWgBRShS905lJTNvYPIEqP3GxWwG5i
iDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAU0Sp4gXhQmRVzq+7+
vRFUkQuPj4Ga/d9r5Wrbg3hck3+5pwe9/7APoq0P/M0DBhQpiJKjrD6ydUevC+Y/
43ZOJPhMlNw0o6TdQxOkX6FDwQanLLs7sfvJvqtVzYn3nuRFKT3dvl7Zg44QMw2M
ay42q59fAcpB4LaDx/i7gOYSS5eca3lYW7j7YSr/+ozXK2KlgUkuCUHN95lOq+dF
trmV9mjzM4CNPZqKSE7kpHRywgrXGPCO000NvEGSYf82AtgRSFKiU8NWLQSEPdcB
k//2dsQZw2cRZ8DrC2B6Tb3M+3+CA6wVyqfqZh1SZva3LfGvq/C+u+ItguzPqNpI
xtvM
-----END CERTIFICATE-----'''
with open(certFile, "wt") as f:
f.write(certData)
return certFile

View File

@ -0,0 +1,233 @@
# -*- 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
from udsactor.log import logger
from udsactor import utils
from udsactor.certs import createSelfSignedCert
from udsactor.scriptThread import ScriptExecutorThread
import threading
import string
import random
import json
import six
from six.moves import socketserver # @UnresolvedImport, pylint: disable=import-error
from six.moves import BaseHTTPServer # @UnresolvedImport, pylint: disable=import-error
import time
import ssl
startTime = time.time()
class HTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
protocol_version = 'HTTP/1.0'
server_version = 'UDS Actor Server'
sys_version = ''
uuid = None
service = None
lock = threading.Lock()
def sendJsonError(self, code, message):
self.send_response(code)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({'error': message}))
return
def sendJsonResponse(self, data):
self.send_response(200)
data = json.dumps(data)
self.send_header('Content-type', 'application/json')
self.send_header('Content-Length', len(data))
self.end_headers()
# Send the html message
self.wfile.write(data)
def do_GET(self):
# Very simple path & params splitter
path = self.path.split('?')[0][1:].split('/')
try:
params = dict((v.split('=') for v in self.path.split('?')[1].split('&')))
except Exception:
params = {}
if path[0] != HTTPServerHandler.uuid:
self.sendJsonError(403, 'Forbidden')
return
if len(path) != 2:
self.sendJsonResponse("UDS Actor has been running for {} seconds".format(time.time() - startTime))
return
try:
operation = getattr(self, 'get_' + path[1])
result = operation(params) # Protect not POST methods
except AttributeError:
self.sendJsonError(404, 'Method not found')
return
except Exception as e:
logger.error('Got exception executing GET {}: {}'.format(path[1], utils.toUnicode(e.message)))
self.sendJsonError(500, str(e))
return
self.sendJsonResponse(result)
def do_POST(self):
path = self.path.split('?')[0][1:].split('/')
if path[0] != HTTPServerHandler.uuid:
self.sendJsonError(403, 'Forbidden')
return
if len(path) != 2:
self.sendJsonError(400, 'Invalid request')
return
try:
HTTPServerHandler.lock.acquire()
length = int(self.headers.get('content-length'))
content = self.rfile.read(length).decode('utf8')
logger.debug('length: {}, content >>{}<<'.format(length, content))
params = json.loads(content)
operation = getattr(self, 'post_' + path[1])
result = operation(params) # Protect not POST methods
except AttributeError:
self.sendJsonError(404, 'Method not found')
return
except Exception as e:
logger.error('Got exception executing POST {}: {}'.format(path[1], utils.toUnicode(e.message)))
self.sendJsonError(500, str(e))
return
finally:
HTTPServerHandler.lock.release()
self.sendJsonResponse(result)
def post_logoff(self, params):
logger.debug('Sending LOGOFF to clients')
HTTPServerHandler.service.ipc.sendLoggofMessage()
return 'ok'
# Alias
post_logout = post_logoff
def post_message(self, params):
logger.debug('Sending MESSAGE to clients')
if 'message' not in params:
raise Exception('Invalid message parameters')
HTTPServerHandler.service.ipc.sendMessageMessage(params['message'])
return 'ok'
def post_script(self, params):
logger.debug('Received script: {}'.format(params))
if 'script' not in params:
raise Exception('Invalid script parameters')
if 'user' in params:
logger.debug('Sending SCRIPT to clients')
HTTPServerHandler.service.ipc.sendScriptMessage(params['script'])
else:
# Execute script at server space, that is, here
# as a parallel thread
th = ScriptExecutorThread(params['script'])
th.start()
return 'ok'
def post_preConnect(self, params):
logger.debug('Received Pre connection')
if 'user' not in params or 'protocol' not in params:
raise Exception('Invalid preConnect parameters')
return HTTPServerHandler.service.preConnect(params.get('user'), params.get('protocol'))
def get_information(self, params):
# TODO: Return something useful? :)
return 'Up and running'
def get_uuid(self, params):
return self.service.api.uuid
def log_error(self, fmt, *args):
logger.error('HTTP ' + fmt % args)
def log_message(self, fmt, *args):
logger.info('HTTP ' + fmt % args)
class HTTPServerThread(threading.Thread):
def __init__(self, address, service):
super(self.__class__, self).__init__()
if HTTPServerHandler.uuid is None:
HTTPServerHandler.uuid = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(48))
self.certFile = createSelfSignedCert()
HTTPServerHandler.service = service
self.initiateServer(address)
def getPort(self):
return self.address[1]
def getIp(self):
return self.address[0]
def initiateServer(self, address):
self.address = (address[0], address[1]) # Copy address & keep it for future reference...
addr = ('0.0.0.0', address[1]) # Adapt to listen on 0.0.0.0
self.server = socketserver.TCPServer(addr, HTTPServerHandler)
self.server.socket = ssl.wrap_socket(self.server.socket, certfile=self.certFile, server_side=True)
def getServerUrl(self):
return 'https://{}:{}/{}'.format(self.getIp(), self.getPort(), HTTPServerHandler.uuid)
def stop(self):
logger.debug('Stopping REST Service')
self.server.shutdown()
def restart(self, address=None):
if address is None:
# address = self.server.server_address
address = self.address
self.address = (address[0], self.address[1]) # Copy address & keep it for future reference, port is never changed once assigned on init
# Listening on 0.0.0.0, does not need to restart listener..
# self.stop()
# self.initiateServer(address)
def run(self):
self.server.serve_forever()

438
actor/src/udsactor/ipc.py Normal file
View File

@ -0,0 +1,438 @@
# -*- 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 socket
import threading
import sys
import traceback
import pickle
import errno
import time
import six
from udsactor.utils import toUnicode
from udsactor.log import logger
# The IPC Server will wait for connections from clients
# Clients will open socket, and wait for data from server
# The messages sent (from server) will be the following (subject to future changes):
# Message_id Data Action
# ------------ -------- --------------------------
# MSG_LOGOFF None Logout user from session
# MSG_MESSAGE message,level Display a message with level (INFO, WARN, ERROR, FATAL) # TODO: Include level, right now only has message
# MSG_SCRIPT python script Execute an specific python script INSIDE CLIENT environment (this messages is not sent right now)
# The messages received (sent from client) will be the following:
# Message_id Data Action
# ------------ -------- --------------------------
# REQ_LOGOUT Logout user from session
# REQ_INFORMATION None Request information from ipc server (maybe configuration parameters in a near future)
# REQ_LOGIN python script Execute an specific python script INSIDE CLIENT environment (this messages is not sent right now)
#
# All messages are in the form:
# BYTE
# 0 1-2 3 4 ...
# MSG_ID DATA_LENGTH (little endian) Data (can be 0 length)
# With a previos "MAGIC" header in fron of each message
MSG_LOGOFF = 0xA1
MSG_MESSAGE = 0xB2
MSG_SCRIPT = 0xC3
MSG_INFORMATION = 0xD4
MSG_TICKET = 0x90
# Request messages
REQ_INFORMATION = MSG_INFORMATION
REQ_LOGIN = 0xE5
REQ_LOGOUT = MSG_LOGOFF
REQ_TICKET = MSG_TICKET
VALID_REQUESTS = (REQ_INFORMATION, REQ_LOGIN, REQ_LOGOUT, REQ_TICKET)
VALID_MESSAGES = (MSG_LOGOFF, MSG_MESSAGE, MSG_SCRIPT, MSG_INFORMATION)
REQ_INFORMATION = 0xAA
# Reverse msgs dict for debugging
REV_DICT = {
MSG_LOGOFF: 'MSG_LOGOFF',
MSG_MESSAGE: 'MSG_MESSAGE',
MSG_SCRIPT: 'MSG_SCRIPT',
MSG_INFORMATION: 'MSG_INFORMATION',
REQ_TICKET: 'REQ_TICKET',
REQ_LOGIN: 'REQ_LOGIN',
REQ_LOGOUT: 'REQ_LOGOUT'
}
MAGIC = b'\x55\x44\x53\x00' # UDS in hexa with a padded 0 to the right
# Allows notifying login/logout from client for linux platform
ALLOW_LOG_METHODS = sys.platform != 'win32'
# States for client processor
ST_SECOND_BYTE = 0x01
ST_RECEIVING = 0x02
ST_PROCESS_MESSAGE = 0x02
class ClientProcessor(threading.Thread):
def __init__(self, parent, clientSocket):
super(ClientProcessor, self).__init__()
self.parent = parent
self.clientSocket = clientSocket
self.running = False
self.messages = six.moves.queue.Queue(32) # @UndefinedVariable
def stop(self):
logger.debug('Stoping client processor')
self.running = False
def processRequest(self, msg, data):
logger.debug('Got Client message {}={}'.format(msg, REV_DICT.get(msg)))
if self.parent.clientMessageProcessor is not None:
self.parent.clientMessageProcessor(msg, data)
def run(self):
self.running = True
self.clientSocket.setblocking(0)
state = None
recv_msg = None
recv_data = None
while self.running:
try:
counter = 1024
while counter > 0: # So we process at least the incoming queue every XX bytes readed
counter -= 1
b = self.clientSocket.recv(1)
if b == b'':
# Client disconnected
self.running = False
self.processRequest(REQ_LOGOUT, 'CLIENT_CONNECTION_LOST')
break
buf = six.byte2int(b) # Empty buffer, this is set as non-blocking
if state is None:
if buf in VALID_REQUESTS:
logger.debug('State set to {}'.format(buf))
state = buf
recv_msg = buf
continue # Get next byte
else:
logger.debug('Got unexpected data {}'.format(buf))
elif state in VALID_REQUESTS:
logger.debug('First length byte is {}'.format(buf))
msg_len = buf
state = ST_SECOND_BYTE
continue
elif state == ST_SECOND_BYTE:
msg_len += buf << 8
logger.debug('Second length byte is {}, len is {}'.format(buf, msg_len))
if msg_len == 0:
self.processRequest(recv_msg, None)
state = None
break
state = ST_RECEIVING
recv_data = b''
continue
elif state == ST_RECEIVING:
recv_data += six.int2byte(buf)
msg_len -= 1
if msg_len == 0:
self.processRequest(recv_msg, recv_data)
recv_data = None
state = None
break
else:
logger.debug('Got invalid message from request: {}, state: {}'.format(buf, state))
except socket.error as e:
# If no data is present, no problem at all, pass to check messages
pass
except Exception as e:
tb = traceback.format_exc()
logger.error('Error: {}, trace: {}'.format(e, tb))
if self.running is False:
break
try:
msg = self.messages.get(block=True, timeout=1)
except six.moves.queue.Empty: # No message got in time @UndefinedVariable
continue
logger.debug('Got message {}={}'.format(msg, REV_DICT.get(msg)))
try:
m = msg[1] if msg[1] is not None else b''
ln = len(m)
data = MAGIC + six.int2byte(msg[0]) + six.int2byte(ln & 0xFF) + six.int2byte(ln >> 8) + m
try:
self.clientSocket.sendall(data)
except socket.error as e:
# Send data error
logger.debug('Socket connection is no more available: {}'.format(e.args))
self.running = False
except Exception as e:
logger.error('Invalid message in queue: {}'.format(e))
logger.debug('Client processor stopped')
try:
self.clientSocket.close()
except Exception:
pass # If can't close, nothing happens, just end thread
class ServerIPC(threading.Thread):
def __init__(self, listenPort, clientMessageProcessor=None):
super(ServerIPC, self).__init__()
self.port = listenPort
self.running = False
self.serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.threads = []
self.clientMessageProcessor = clientMessageProcessor
def stop(self):
logger.debug('Stopping Server IPC')
self.running = False
for t in self.threads:
t.stop()
socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect(('localhost', self.port))
self.serverSocket.close()
for t in self.threads:
t.join()
def sendMessage(self, msgId, msgData):
'''
Notify message to all listening threads
'''
logger.debug('Sending message {}({}),{} to all clients'.format(msgId, REV_DICT.get(msgId), msgData))
# Convert to bytes so length is correctly calculated
if isinstance(msgData, six.text_type):
msgData = msgData.encode('utf8')
for t in self.threads:
if t.isAlive():
logger.debug('Sending to {}'.format(t))
t.messages.put((msgId, msgData))
def sendLoggofMessage(self):
self.sendMessage(MSG_LOGOFF, '')
def sendMessageMessage(self, message):
self.sendMessage(MSG_MESSAGE, message)
def sendScriptMessage(self, script):
self.sendMessage(MSG_SCRIPT, script)
def sendInformationMessage(self, info):
self.sendMessage(MSG_INFORMATION, pickle.dumps(info))
# This one is the only one dumped in json, be care with this!!
def sendTicketMessage(self, ticketData):
self.sendMessage(MSG_TICKET, json.dumps(ticketData))
def cleanupFinishedThreads(self):
'''
Cleans up current threads list
'''
aliveThreads = []
for t in self.threads:
if t.isAlive():
logger.debug('Thread {} is alive'.format(t))
aliveThreads.append(t)
self.threads[:] = aliveThreads
def run(self):
self.running = True
self.serverSocket.bind(('localhost', self.port))
self.serverSocket.setblocking(1)
self.serverSocket.listen(4)
while True:
try:
(clientSocket, address) = self.serverSocket.accept()
# Stop processing if thread is mean to stop
if self.running is False:
break
logger.debug('Got connection from {}'.format(address))
self.cleanupFinishedThreads() # House keeping
logger.debug('Starting new thread, current: {}'.format(self.threads))
t = ClientProcessor(self, clientSocket)
self.threads.append(t)
t.start()
except Exception as e:
logger.error('Got an exception on Server ipc thread: {}'.format(e))
class ClientIPC(threading.Thread):
def __init__(self, listenPort):
super(ClientIPC, self).__init__()
self.port = listenPort
self.running = False
self.clientSocket = None
self.messages = six.moves.queue.Queue(32) # @UndefinedVariable
self.connect()
def stop(self):
self.running = False
def getMessage(self):
while self.running:
try:
return self.messages.get(timeout=1)
except six.moves.queue.Empty: # @UndefinedVariable
continue
return None
def sendRequestMessage(self, msg, data=None):
logger.debug('Sending request for msg: {}({}), {}'.format(msg, REV_DICT.get(msg), data))
if data is None:
data = b''
if isinstance(data, six.text_type): # Convert to bytes if necessary
data = data.encode('utf-8')
ln = len(data)
msg = six.int2byte(msg) + six.int2byte(ln & 0xFF) + six.int2byte(ln >> 8) + data
self.clientSocket.sendall(msg)
def requestInformation(self):
self.sendRequestMessage(REQ_INFORMATION)
def sendLogin(self, username):
self.sendRequestMessage(REQ_LOGIN, username)
def sendLogout(self, username):
self.sendRequestMessage(REQ_LOGOUT, username)
def requestTicket(self, ticketId, secure=True):
self.sendRequestMessage(REQ_TICKET, json.dumps({'ticketId': ticketId, 'secure': secure}))
def messageReceived(self):
'''
Override this method to automatically get notified on new message
received. Message is at self.messages queue
'''
pass # Messa
def receiveBytes(self, number):
msg = b''
while self.running and len(msg) < number:
try:
buf = self.clientSocket.recv(number - len(msg))
if buf == b'':
logger.debug('Buf {}, msg {}({})'.format(buf, msg, REV_DICT.get(msg)))
self.running = False
break
msg += buf
except socket.timeout:
pass
if self.running is False:
logger.debug('Not running, returning None')
return None
return msg
def connect(self):
self.clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.clientSocket.connect(('localhost', self.port))
self.clientSocket.settimeout(2) # 2 seconds timeout
def run(self):
self.running = True
while self.running:
try:
msg = b''
# We look for magic message header
while self.running: # Wait for MAGIC
try:
buf = self.clientSocket.recv(len(MAGIC) - len(msg))
if buf == b'':
self.running = False
break
msg += buf
if len(msg) != len(MAGIC):
continue # Do not have message
if msg != MAGIC: # Skip first byte an continue searchong
msg = msg[1:]
continue
break
except socket.timeout: # Timeout is here so we can get stop thread
continue
if self.running is False:
break
# Now we get message basic data (msg + datalen)
msg = bytearray(self.receiveBytes(3))
# We have the magic header, here comes the message itself
if msg is None:
continue
msgId = msg[0]
dataLen = msg[1] + (msg[2] << 8)
if msgId not in VALID_MESSAGES:
raise Exception('Invalid message id: {}'.format(msgId))
data = self.receiveBytes(dataLen)
if data is None:
continue
self.messages.put((msgId, data))
self.messageReceived()
except socket.error as e:
if e.errno == errno.EINTR:
time.sleep(1) #
continue # Ignore interrupted system call
logger.error('Communication with server got an error: {}'.format(toUnicode(e.strerror)))
# self.running = False
return
except Exception as e:
tb = traceback.format_exc()
logger.error('Error: {}, trace: {}'.format(e, tb))
try:
self.clientSocket.close()
except Exception:
pass # If can't close, nothing happens, just end thread

View File

@ -0,0 +1,234 @@
# -*- 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 os
import stat
import subprocess
from udsactor import operations
from udsactor.service import CommonService
from udsactor.service import initCfg
from udsactor.service import IPC_PORT
from udsactor import ipc
from udsactor import store
from udsactor.log import logger
from udsactor.linux.daemon import Daemon
from udsactor.linux import renamer
POST_CMD = '/etc/udsactor/post'
try:
from prctl import set_proctitle # @UnresolvedImport
except Exception: # Platform may not include prctl, so in case it's not available, we let the "name" as is
def set_proctitle(_):
pass
class UDSActorSvc(Daemon, CommonService):
rebootMachineAfterOp = False
def __init__(self, args=None):
Daemon.__init__(self, '/var/run/udsa.pid')
CommonService.__init__(self)
def rename(self, name, user=None, oldPassword=None, newPassword=None):
'''
Renames the computer, and optionally sets a password for an user
before this
'''
hostName = operations.getComputerName()
if hostName.lower() == name.lower():
logger.info('Computer name is already {}'.format(hostName))
self.setReady()
return
# Check for password change request for an user
if user is not None:
logger.info('Setting password for user {}'.format(user))
try:
operations.changeUserPassword(user, oldPassword, newPassword)
except Exception as e:
# We stop here without even renaming computer, because the
# process has failed
raise Exception(
'Could not change password for user {} (maybe invalid current password is configured at broker): {} '.format(user, unicode(e)))
renamer.rename(name)
if self.rebootMachineAfterOp is False:
self.setReady()
else:
logger.info('Rebooting computer to activate new name {}'.format(name))
self.reboot()
def joinDomain(self, name, domain, ou, account, password):
logger.fatal('Join domain is not supported on linux platforms right now')
def preConnect(self, user, protocol):
'''
Invoked when received a PRE Connection request via REST
'''
# Execute script in /etc/udsactor/post after interacting with broker, if no reboot is requested ofc
# This will be executed only when machine gets "ready"
try:
pre_cmd = store.preApplication()
if os.path.isfile(pre_cmd):
if (os.stat(pre_cmd).st_mode & stat.S_IXUSR) != 0:
subprocess.call([pre_cmd, user, protocol])
else:
logger.info('PRECONNECT file exists but it it is not executable (needs execution permission by root)')
else:
logger.info('PRECONNECT file not found & not executed')
except Exception:
# Ignore output of execution command
logger.error('Executing preconnect command give')
return 'ok'
def run(self):
cfg = initCfg() # Gets a local copy of config to get "reboot"
logger.debug('CFG: {}'.format(cfg))
if cfg is not None:
self.rebootMachineAfterOp = cfg.get('reboot', True)
else:
self.rebootMachineAfterOp = False
logger.info('Reboot after is {}'.format(self.rebootMachineAfterOp))
logger.debug('Running Daemon')
set_proctitle('UDSActorDaemon')
# Linux daemon will continue running unless something is requested to
while True:
brokerConnected = self.interactWithBroker()
if brokerConnected is False:
logger.debug('Interact with broker returned false, stopping service after a while')
return
elif brokerConnected is True:
break
# If brokerConnected returns None, repeat the cycle
self.doWait(16000) # Wait for a looong while
if self.isAlive is False:
logger.debug('The service is not alive after broker interaction, stopping it')
return
if self.rebootRequested is True:
logger.debug('Reboot has been requested, stopping service')
return
# Execute script in /etc/udsactor/post after interacting with broker, if no reboot is requested ofc
# This will be executed only when machine gets "ready"
try:
if os.path.isfile(POST_CMD):
if (os.stat(POST_CMD).st_mode & stat.S_IXUSR) != 0:
subprocess.call([POST_CMD, ])
else:
logger.info('POST file exists but it it is not executable (needs execution permission by root)')
else:
logger.info('POST file not found & not executed')
except Exception as e:
# Ignore output of execution command
logger.error('Executing post command give')
self.initIPC()
# *********************
# * Main Service loop *
# *********************
# Counter used to check ip changes only once every 10 seconds, for
# example
counter = 0
while self.isAlive:
counter += 1
if counter % 10 == 0:
self.checkIpsChanged()
# In milliseconds, will break
self.doWait(1000)
self.endIPC()
self.endAPI()
self.notifyStop()
def usage():
sys.stderr.write("usage: {} start|stop|restart|login 'username'|logout 'username'\n".format(sys.argv[0]))
sys.exit(2)
if __name__ == '__main__':
logger.setLevel(20000)
if len(sys.argv) == 3 and sys.argv[1] in ('login', 'logout'):
logger.debug('Running client udsactor')
client = None
try:
client = ipc.ClientIPC(IPC_PORT)
if 'login' == sys.argv[1]:
client.sendLogin(sys.argv[2])
sys.exit(0)
elif 'logout' == sys.argv[1]:
client.sendLogout(sys.argv[2])
sys.exit(0)
else:
usage()
except Exception as e:
logger.error(e)
elif len(sys.argv) != 2:
usage()
logger.debug('Executing actor')
daemon = UDSActorSvc()
if len(sys.argv) == 2:
if 'start' == sys.argv[1]:
daemon.start()
elif 'stop' == sys.argv[1]:
daemon.stop()
elif 'restart' == sys.argv[1]:
daemon.restart()
else:
usage()
sys.exit(0)
else:
usage()

View File

@ -0,0 +1,32 @@
# -*- 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

View File

@ -0,0 +1,183 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014-2018 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: : http://www.jejik.com/authors/sander_marechal/
@see: : http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/
'''
from __future__ import unicode_literals
import sys
import os
import time
import atexit
from udsactor.log import logger
from signal import SIGTERM
class Daemon:
"""
A generic daemon class.
Usage: subclass the Daemon class and override the run() method
"""
def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
self.stdin = stdin
self.stdout = stdout
self.stderr = stderr
self.pidfile = pidfile
def daemonize(self):
"""
do the UNIX double-fork magic, see Stevens' "Advanced
Programming in the UNIX Environment" for details (ISBN 0201563177)
http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
"""
try:
pid = os.fork()
if pid > 0:
# exit first parent
sys.exit(0)
except OSError as e:
logger.error("fork #1 error: {}".format(e))
sys.stderr.write("fork #1 failed: {}\n".format(e))
sys.exit(1)
# decouple from parent environment
os.chdir("/")
os.setsid()
os.umask(0)
# do second fork
try:
pid = os.fork()
if pid > 0:
# exit from second parent
sys.exit(0)
except OSError as e:
logger.error("fork #2 error: {}".format(e))
sys.stderr.write("fork #2 failed: {}\n".format(e))
sys.exit(1)
# redirect standard file descriptors
sys.stdout.flush()
sys.stderr.flush()
si = open(self.stdin, 'r')
so = open(self.stdout, 'ab+')
se = open(self.stderr, 'ab+', 0)
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
# write pidfile
atexit.register(self.delpid)
pid = str(os.getpid())
with open(self.pidfile, 'w+') as f:
f.write("{}\n".format(pid))
def delpid(self):
try:
os.remove(self.pidfile)
except Exception:
# Not found/not permissions or whatever...
pass
def start(self):
"""
Start the daemon
"""
logger.debug('Starting daemon')
# Check for a pidfile to see if the daemon already runs
try:
pf = open(self.pidfile, 'r')
pid = int(pf.read().strip())
pf.close()
except IOError:
pid = None
if pid:
message = "pidfile {} already exist. Daemon already running?\n".format(pid)
logger.error(message)
sys.stderr.write(message)
sys.exit(1)
# Start the daemon
self.daemonize()
try:
self.run()
except Exception as e:
logger.error('Exception running process: {}'.format(e))
if os.path.exists(self.pidfile):
os.remove(self.pidfile)
def stop(self):
"""
Stop the daemon
"""
# Get the pid from the pidfile
try:
pf = open(self.pidfile, 'r')
pid = int(pf.read().strip())
pf.close()
except IOError:
pid = None
if pid is None:
message = "pidfile {} does not exist. Daemon not running?\n".format(self.pidfile)
logger.info(message)
# sys.stderr.write(message)
return # not an error in a restart
# Try killing the daemon process
try:
while True:
os.kill(pid, SIGTERM)
time.sleep(1)
except OSError as err:
if err.errno == 3: # No such process
if os.path.exists(self.pidfile):
os.remove(self.pidfile)
else:
sys.stderr.write(err)
sys.exit(1)
def restart(self):
"""
Restart the daemon
"""
self.stop()
self.start()
# Overridables
def run(self):
"""
You should override this method when you subclass Daemon. It will be called after the process has been
daemonized by start() or restart().
"""

View File

@ -0,0 +1,80 @@
# -*- 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 logging
import os
import tempfile
import six
# Valid logging levels, from UDS Broker (uds.core.utils.log)
OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in six.moves.xrange(6)) # @UndefinedVariable
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
# 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')
logging.basicConfig(
filename=fname,
filemode='a',
format='%(levelname)s %(asctime)s %(message)s',
level=logging.DEBUG
)
self.logger = logging.getLogger('udsactor')
os.chmod(fname, 0o0600)
return
except Exception:
pass
# Logger can't be set
self.logger = None
def log(self, level, message):
# 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

@ -0,0 +1,248 @@
# -*- 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 socket
import platform
import fcntl
import os
import ctypes # @UnusedImport
import ctypes.util
import subprocess
import struct
import array
import six
from udsactor import utils
from .renamer import rename
def _getMacAddr(ifname):
'''
Returns the mac address of an interface
Mac is returned as unicode utf-8 encoded
'''
if isinstance(ifname, list):
return dict([(name, _getMacAddr(name)) for name in ifname])
if isinstance(ifname, six.text_type):
ifname = ifname.encode('utf-8') # If unicode, convert to bytes (or str in python 2.7)
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
info = bytearray(fcntl.ioctl(s.fileno(), 0x8927, struct.pack(str('256s'), ifname[:15])))
return six.text_type(''.join(['%02x:' % char for char in info[18:24]])[:-1]).upper()
except Exception:
return None
def _getIpAddr(ifname):
'''
Returns the ip address of an interface
Ip is returned as unicode utf-8 encoded
'''
if isinstance(ifname, list):
return dict([(name, _getIpAddr(name)) for name in ifname])
if isinstance(ifname, six.text_type):
ifname = ifname.encode('utf-8') # If unicode, convert to bytes (or str in python 2.7)
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
return six.text_type(socket.inet_ntoa(fcntl.ioctl(
s.fileno(),
0x8915, # SIOCGIFADDR
struct.pack(str('256s'), ifname[:15])
)[20:24]))
except Exception:
return None
def _getInterfaces():
'''
Returns a list of interfaces names coded in utf-8
'''
max_possible = 128 # arbitrary. raise if needed.
space = max_possible * 16
if platform.architecture()[0] == '32bit':
offset, length = 32, 32
elif platform.architecture()[0] == '64bit':
offset, length = 16, 40
else:
raise OSError('Unknown arquitecture {0}'.format(platform.architecture()[0]))
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
names = array.array(str('B'), b'\0' * space)
outbytes = struct.unpack(str('iL'), fcntl.ioctl(
s.fileno(),
0x8912, # SIOCGIFCONF
struct.pack(str('iL'), space, names.buffer_info()[0])
))[0]
namestr = names.tostring()
# return namestr, outbytes
return [namestr[i:i + offset].split(b'\0', 1)[0].decode('utf-8') for i in range(0, outbytes, length)]
def _getIpAndMac(ifname):
ip, mac = _getIpAddr(ifname), _getMacAddr(ifname)
return (ip, mac)
def getComputerName():
'''
Returns computer name, with no domain
'''
return socket.gethostname().split('.')[0]
def getNetworkInfo():
for ifname in _getInterfaces():
ip, mac = _getIpAndMac(ifname)
if mac != '00:00:00:00:00:00' and ip.startswith('169.254') is False: # Skips local interfaces & interfaces with no dhcp IPs
yield utils.Bunch(name=ifname, mac=mac, ip=ip)
def getDomainName():
return ''
def getLinuxVersion():
lv = platform.linux_distribution()
return lv[0] + ', ' + lv[1]
def reboot(flags=0):
'''
Simple reboot using os command
'''
# Workaround for dummy thread
if six.PY3 is False:
import threading
threading._DummyThread._Thread__stop = lambda x: 42
subprocess.call(['/sbin/shutdown', 'now', '-r'])
def loggoff():
'''
Right now restarts the machine...
'''
# Workaround for dummy thread
if six.PY3 is False:
import threading
threading._DummyThread._Thread__stop = lambda x: 42
subprocess.call(['/usr/bin/pkill', '-u', os.environ['USER']])
# subprocess.call(['/sbin/shutdown', 'now', '-r'])
# subprocess.call(['/usr/bin/systemctl', 'reboot', '-i'])
def renameComputer(newName):
rename(newName)
def joinDomain(domain, ou, account, password, executeInOneStep=False):
pass
def changeUserPassword(user, oldPassword, newPassword):
'''
Simple password change for user using command line
'''
os.system('echo "{1}\n{1}" | /usr/bin/passwd {0} 2> /dev/null'.format(user, newPassword))
class XScreenSaverInfo(ctypes.Structure):
_fields_ = [('window', ctypes.c_long),
('state', ctypes.c_int),
('kind', ctypes.c_int),
('til_or_since', ctypes.c_ulong),
('idle', ctypes.c_ulong),
('eventMask', ctypes.c_ulong)]
# Initialize xlib & xss
try:
xlibPath = ctypes.util.find_library('X11')
xssPath = ctypes.util.find_library('Xss')
xlib = xss = None
if not xlibPath or not xssPath:
raise Exception()
xlib = ctypes.cdll.LoadLibrary(xlibPath)
xss = ctypes.cdll.LoadLibrary(xssPath)
# Fix result type to XScreenSaverInfo Structure
xss.XScreenSaverQueryExtension.restype = ctypes.c_int
xss.XScreenSaverAllocInfo.restype = ctypes.POINTER(XScreenSaverInfo) # Result in a XScreenSaverInfo structure
display = xlib.XOpenDisplay(None)
info = xss.XScreenSaverAllocInfo()
except Exception: # Libraries not accesible, not found or whatever..
xlib = xss = display = info = None
def initIdleDuration(atLeastSeconds):
'''
On linux we set the screensaver to at least required seconds, or we never will get "idle"
'''
# Workaround for dummy thread
if six.PY3 is False:
import threading
threading._DummyThread._Thread__stop = lambda x: 42
subprocess.call(['/usr/bin/xset', 's', '{}'.format(atLeastSeconds + 30)])
# And now reset it
subprocess.call(['/usr/bin/xset', 's', 'reset'])
def getIdleDuration():
'''
Returns idle duration, in seconds
'''
if xlib is None or xss is None:
return 0 # Libraries not available
event_base = ctypes.c_int()
error_base = ctypes.c_int()
available = xss.XScreenSaverQueryExtension(display, ctypes.byref(event_base), ctypes.byref(error_base))
if available != 1:
return 0 # No screen saver is available, no way of getting idle
xss.XScreenSaverQueryInfo(display, xlib.XDefaultRootWindow(display), info)
# Centos seems to set state to 1?? (weird, but it's happening don't know why... will try this way)
if info.contents.state != 0 and 'centos' not in platform.linux_distribution()[0].lower().strip():
return 3600 * 100 * 1000 # If screen saver is active, return a high enough value
return info.contents.idle / 1000.0
def getCurrentUser():
'''
Returns current logged in user
'''
return os.environ['USER']

View File

@ -0,0 +1,61 @@
# -*- 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 platform
import os
import sys
import pkgutil
from udsactor.log import logger
renamers = {}
# Renamers now are for IPv4 only addresses
def rename(newName):
distribution = platform.linux_distribution()[0].lower().strip()
if distribution in renamers:
return renamers[distribution](newName)
# Try Debian renamer, simplest one
logger.info('Renamer for platform "{0}" not found, tryin debian renamer'.format(distribution))
return renamers['debian'](newName)
# Do load of packages
def _init():
pkgpath = os.path.dirname(sys.modules[__name__].__file__)
for _, name, _ in pkgutil.iter_modules([pkgpath]):
__import__(__name__ + '.' + name, globals(), locals())
_init()

View File

@ -0,0 +1,68 @@
# -*- 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
from udsactor.linux.renamer import renamers
from udsactor.log import logger
import os
def rename(newName):
'''
Debian renamer
Expects new host name on newName
Host does not needs to be rebooted after renaming
'''
logger.debug('using Debian renamer')
with open('/etc/hostname', 'w') as hostname:
hostname.write(newName)
# Force system new name
os.system('/bin/hostname %s' % newName)
# add name to "hosts"
with open('/etc/hosts', 'r') as hosts:
lines = hosts.readlines()
with open('/etc/hosts', 'w') as hosts:
hosts.write("127.0.1.1\t%s\n" % newName)
for l in lines:
if l[:9] == '127.0.1.1': # Skips existing 127.0.1.1. if it already exists
continue
hosts.write(l)
return True
# All names in lower case
renamers['debian'] = rename
renamers['ubuntu'] = rename

View File

@ -0,0 +1,66 @@
# -*- 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
from udsactor.linux.renamer import renamers
from udsactor.log import logger
import os
def rename(newName):
'''
RH, Centos, Fedora Renamer
Expects new host name on newName
Host does not needs to be rebooted after renaming
'''
logger.debug('using SUSE renamer')
with open('/etc/hostname', 'w') as hostname:
hostname.write(newName)
# Force system new name
os.system('/bin/hostname %s' % newName)
# add name to "hosts"
with open('/etc/hosts', 'r') as hosts:
lines = hosts.readlines()
with open('/etc/hosts', 'w') as hosts:
hosts.write("127.0.1.1\t{}\n".format(newName))
for l in lines:
if l[:9] != '127.0.1.1': # Skips existing 127.0.1.1. if it already exists
hosts.write(l)
return True
# All names in lower case
renamers['opensuse'] = rename
renamers['suse'] = rename

View File

@ -0,0 +1,75 @@
# -*- 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
from udsactor.linux.renamer import renamers
from udsactor.log import logger
import os
def rename(newName):
'''
RH, Centos, Fedora Renamer
Expects new host name on newName
Host does not needs to be rebooted after renaming
'''
logger.debug('using RH renamer')
with open('/etc/hostname', 'w') as hostname:
hostname.write(newName)
# Force system new name
os.system('/bin/hostname %s' % newName)
# add name to "hosts"
with open('/etc/hosts', 'r') as hosts:
lines = hosts.readlines()
with open('/etc/hosts', 'w') as hosts:
hosts.write("127.0.1.1\t{}\n".format(newName))
for l in lines:
if l[:9] != '127.0.1.1': # Skips existing 127.0.1.1. if it already exists
hosts.write(l)
with open('/etc/sysconfig/network', 'r') as net:
lines = net.readlines()
with open('/etc/sysconfig/network', 'w') as net:
net.write('HOSTNAME={}\n'.format(newName))
for l in lines:
if l[:8] != 'HOSTNAME':
net.write(l)
return True
# All names in lower case
renamers['centos linux'] = rename
renamers['centos'] = rename
renamers['fedora'] = rename

View File

@ -0,0 +1,93 @@
# -*- 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 six
import os
DEBUG = False
CONFIGFILE = '/etc/udsactor/udsactor.cfg' if DEBUG is False else '/tmp/udsactor.cfg'
PRECONNECT_CMD = '/etc/udsactor/pre'
def checkPermissions():
return True if DEBUG else os.getuid() == 0
def readConfig():
res = {}
try:
cfg = six.moves.configparser.SafeConfigParser() # @UndefinedVariable
cfg.optionxform = six.text_type
cfg.read(CONFIGFILE)
# Just reads 'uds' section
for key in cfg.options('uds'):
res[key] = cfg.get('uds', key)
if res[key].lower() in ('true', 'yes', 'si'):
res[key] = True
elif res[key].lower() in ('false', 'no'):
res[key] = False
except Exception:
pass
return res
def writeConfig(data):
cfg = six.moves.configparser.SafeConfigParser() # @UndefinedVariable
cfg.optionxform = six.text_type
cfg.add_section('uds')
for key, val in data.items():
cfg.set('uds', key, str(val))
# Ensures exists destination folder
dirname = os.path.dirname(CONFIGFILE)
if not os.path.exists(dirname):
os.mkdir(dirname, mode=0o700) # Will create only if route to path already exists, for example, /etc (that must... :-))
with open(CONFIGFILE, 'w') as f:
cfg.write(f)
os.chmod(CONFIGFILE, 0o0600)
def useOldJoinSystem():
return False
# Right now, we do not really need an application to be run on "startup" as could ocur with windows
def runApplication():
return None
def preApplication():
return PRECONNECT_CMD

104
actor/src/udsactor/log.py Normal file
View File

@ -0,0 +1,104 @@
# -*- 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 traceback
import sys
import six
if sys.platform == 'win32':
from udsactor.windows.log import LocalLogger # @UnusedImport
else:
from udsactor.linux.log import LocalLogger # @Reimport
# Valid logging levels, from UDS Broker (uds.core.utils.log)
OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in six.moves.xrange(6)) # @UndefinedVariable
class Logger(object):
def __init__(self):
self.logLevel = INFO
self.logger = LocalLogger()
self.remoteLogger = None
def setLevel(self, level):
'''
Sets log level filter (minimum level required for a log message to be processed)
:param level: Any message with a level below this will be filtered out
'''
self.logLevel = int(level) # Ensures level is an integer or fails
def setRemoteLogger(self, remoteLogger):
self.remoteLogger = remoteLogger
def log(self, level, message):
if level < self.logLevel: # Skip not wanted messages
return
# If remote logger is available, notify message to it
try:
if self.remoteLogger is not None and self.remoteLogger.isConnected and level >= INFO:
self.remoteLogger.log(level, message)
except Exception as e:
self.logger.log(FATAL, 'Error notifying log to broker: {}'.format(e.message))
self.logger.log(level, message)
def debug(self, message):
self.log(DEBUG, message)
def warn(self, message):
self.log(WARN, message)
def info(self, message):
self.log(INFO, message)
def error(self, message):
self.log(ERROR, message)
def fatal(self, message):
self.log(FATAL, message)
def exception(self):
try:
tb = traceback.format_exc()
except Exception:
tb = '(could not get traceback!)'
self.log(DEBUG, tb)
def flush(self):
pass
logger = Logger()

View File

@ -0,0 +1,40 @@
# -*- 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
'''
# pylint: disable=unused-wildcard-import,wildcard-import
from __future__ import unicode_literals
import sys
if sys.platform == 'win32':
from .windows.operations import * # @UnusedWildImport
else:
from .linux.operations import * # @UnusedWildImport

231
actor/src/udsactor/rest.py Normal file
View File

@ -0,0 +1,231 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2019 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
'''
# pylint: disable=invalid-name
import uuid
import warnings
import json
import logging
import typing
import requests
from udsactor import VERSION
from .utils import exceptionToMessage
from .log import logger
class RESTError(Exception):
ERRCODE = 0
class RESTConnectionError(RESTError):
ERRCODE = -1
# Errors ""raised"" from broker
class RESTInvalidKeyError(RESTError):
ERRCODE = 1
class RESTUnmanagedHostError(RESTError):
ERRCODE = 2
class RESTUserServiceNotFoundError(RESTError):
ERRCODE = 3
class RESTOsManagerError(RESTError):
ERRCODE = 4
# Disable warnings log messages
try:
import urllib3 # @UnusedImport @UnresolvedImport
except Exception:
from requests.packages import urllib3 # @Reimport @UnresolvedImport
try:
urllib3.disable_warnings() # @UndefinedVariable
warnings.simplefilter("ignore")
except Exception:
pass # In fact, isn't too important, but will log warns to logging file
def ensureResultIsOk(result: typing.Any) -> None:
if 'error' not in result:
return
for i in (RESTInvalidKeyError, RESTUnmanagedHostError, RESTUserServiceNotFoundError, RESTOsManagerError):
if result['error'] == i.ERRCODE:
raise i(result['result'])
err = RESTError(result['result'])
err.ERRCODE = result['error']
raise err
class REST:
def __init__(self, host: str, validateCert: bool) -> None:
self.host = host
self.validateCert = validateCert
self.url = "{}://{}/rest/actor/".format(('https', 'https'), self.host)
# Disable logging requests messages except for errors, ...
logging.getLogger("requests").setLevel(logging.CRITICAL)
# Tries to disable all warnings
try:
warnings.simplefilter("ignore") # Disables all warnings
except Exception:
pass
def _getUrl(self, method, key=None, ids=None):
url = self.url + method
params = []
if key is not None:
params.append('key=' + key)
if ids is not None:
params.append('id=' + ids)
params.append('version=' + VERSION)
if len(params) > 0:
url += '?' + '&'.join(params)
return url
def _request(self, url, data=None):
try:
if data is None:
# Old requests version does not support verify, but they do not checks ssl certificate by default
if self.newerRequestLib:
r = requests.get(url, verify=VERIFY_CERT)
else:
logger.debug('Requesting with old')
r = requests.get(url) # Always ignore certs??
else:
if data == '':
data = '{"dummy": true}' # Ensures no proxy rewrites POST as GET because body is empty...
if self.newerRequestLib:
r = requests.post(url, data=data, headers={'content-type': 'application/json'}, verify=VERIFY_CERT)
else:
logger.debug('Requesting with old')
r = requests.post(url, data=data, headers={'content-type': 'application/json'})
# From versions of requests, content maybe bytes or str. We need str for json.loads
content = r.content
if not isinstance(content, six.text_type):
content = content.decode('utf8')
r = json.loads(content) # Using instead of r.json() to make compatible with oooold rquests lib versions
except requests.exceptions.RequestException as e:
raise ConnectionError(e)
except Exception as e:
raise ConnectionError(exceptionToMessage(e))
ensureResultIsOk(r)
return r
@property
def isConnected(self):
return self.uuid is not None
def test(self):
url = self._getUrl('test', self.masterKey)
return self._request(url)['result']
def init(self, ids):
'''
Ids is a comma separated values indicating MAC=ip
Server returns:
uuid, mac
Optionally can return an third parameter, that is max "idle" request time
'''
logger.debug('Invoking init')
url = self._getUrl('init', key=self.masterKey, ids=ids)
res = self._request(url)['result']
logger.debug('Got response parameters: {}'.format(res))
self.uuid, self.mac = res[0:2]
# Optional idle parameter
try:
self.idle = int(res[2])
if self.idle < 30:
self.idle = None # No values under 30 seconds are allowed :)
except Exception:
self.idle = None
return self.uuid
def postMessage(self, msg, data, processData=True):
logger.debug('Invoking post message {} with data {}'.format(msg, data))
if self.uuid is None:
raise ConnectionError('REST api has not been initialized')
if processData:
if data and not isinstance(data, six.text_type):
data = data.decode('utf8')
data = json.dumps({'data': data})
url = self._getUrl('/'.join([self.uuid, msg]))
return self._request(url, data)['result']
def notifyComm(self, url):
logger.debug('Notifying comms {}'.format(url))
return self.postMessage('notifyComms', url)
def login(self, username):
logger.debug('Notifying login {}'.format(username))
return self.postMessage('login', username)
def logout(self, username):
logger.debug('Notifying logout {}'.format(username))
return self.postMessage('logout', username)
def information(self):
logger.debug('Requesting information'.format())
return self.postMessage('information', '')
def setReady(self, ipsInfo, hostName=None):
logger.debug('Notifying readyness: {}'.format(ipsInfo))
# data = ','.join(['{}={}'.format(v[0], v[1]) for v in ipsInfo])
data = {
'ips': ipsInfo,
'hostname': hostName
}
return self.postMessage('ready', data)
def notifyIpChanges(self, ipsInfo):
logger.debug('Notifying ip changes: {}'.format(ipsInfo))
data = ','.join(['{}={}'.format(v[0], v[1]) for v in ipsInfo])
return self.postMessage('ip', data)
def getTicket(self, ticketId, secure=False):
url = self._getUrl('ticket/' + ticketId, self.masterKey) + "&secure={}".format('1' if secure else '0')
return self._request(url)['result']
def log(self, logLevel, message):
data = json.dumps({'message': message, 'level': logLevel})
return self.postMessage('log', data, processData=False)

View File

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 201 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
'''
# pylint: disable-msg=E1101,W0703
from udsactor.log import logger
import threading
import six
class ScriptExecutorThread(threading.Thread):
def __init__(self, script):
super(ScriptExecutorThread, self).__init__()
self.script = script
def run(self):
try:
logger.debug('Executing script: {}'.format(self.script))
six.exec_(self.script, globals(), None)
except Exception as e:
logger.error('Error executing script: {}'.format(e))

View File

@ -0,0 +1,377 @@
# -*- 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
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
import socket
import time
import random
import os
import subprocess
import shlex
import stat
import json
IPC_PORT = 39188
cfg = None
def initCfg():
global cfg # pylint: disable=global-statement
cfg = store.readConfig()
if logger.logger.isWindows():
# 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
class CommonService(object):
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
cmd = shlex.split(cmdLine, posix=False)
if os.path.isfile(cmd[0]):
if (os.stat(cmd[0]).st_mode & stat.S_IXUSR) != 0:
try:
res = subprocess.check_call(cmd)
except Exception as e:
logger.error('Got exception executing: {} - {}'.format(cmdLine, e))
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))
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 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')
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)
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)
# Now try to run the "runonce" element
runOnce = store.runApplication()
if runOnce is not None:
logger.info('Executing runOnce app: {}'.format(runOnce))
if self.execute(runOnce, 'RunOnce') is True:
# 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
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')
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)
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'})
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()
# ***************************************************
# Methods that ARE overriden by linux & windows Actor
# ***************************************************
def rename(self, name, user=None, oldPassword=None, newPassword=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):
'''
Invoked when broker requests a "domain" action
MUST BE OVERRIDEN
'''
raise NotImplementedError('Method renamed has not been implemented!')
# ****************************************
# Methods that CAN BE overriden by actors
# ****************************************
def notifyLocal(self):
self.setReady(operations.getComputerName())
def doWait(self, miliseconds):
'''
Invoked to wait a bit
CAN be OVERRIDEN
'''
time.sleep(float(miliseconds) / 1000)
def notifyStop(self):
'''
Overriden to log stop
'''
logger.info('Service is being stopped')
def preConnect(self, user, protocol):
'''
Invoked when received a PRE Connection request via REST
'''
logger.debug('Pre-connect does nothing')
return 'ok'
def onLogout(self, user):
logger.debug('On logout invoked for {}'.format(user))

View File

@ -0,0 +1,39 @@
# -*- 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
'''
# pylint: disable=unused-wildcard-import, wildcard-import
from __future__ import unicode_literals
import sys
if sys.platform == 'win32':
from udsactor.windows.store import * # @UnusedWildImport
else:
from udsactor.linux.store import * # @UnusedWildImport

View File

@ -0,0 +1,72 @@
# -*- 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

@ -0,0 +1,148 @@
# -*- 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
'''
# _*_ coding: iso-8859-1 _*_
from __future__ import unicode_literals
import win32com.client # @UnresolvedImport, pylint: disable=import-error
import win32com.server.policy # @UnresolvedImport, pylint: disable=import-error
import os
from udsactor.log import logger
# based on python SENS example from
# http://timgolden.me.uk/python/win32_how_do_i/track-session-events.html
# from Sens.h
SENSGUID_PUBLISHER = "{5fee1bd6-5b9b-11d1-8dd2-00aa004abd5e}"
SENSGUID_EVENTCLASS_LOGON = "{d5978630-5b9f-11d1-8dd2-00aa004abd5e}"
# from EventSys.h
PROGID_EventSystem = "EventSystem.EventSystem"
PROGID_EventSubscription = "EventSystem.EventSubscription"
IID_ISensLogon = "{d597bab3-5b9f-11d1-8dd2-00aa004abd5e}"
class SensLogon(win32com.server.policy.DesignatedWrapPolicy):
_com_interfaces_ = [IID_ISensLogon]
_public_methods_ = [
'Logon',
'Logoff',
'StartShell',
'DisplayLock',
'DisplayUnlock',
'StartScreenSaver',
'StopScreenSaver'
]
def __init__(self, service):
self._wrap_(self)
self.service = service
def Logon(self, *args):
logger.debug('Logon event: {}'.format(args))
if self.service.api is not None and self.service.api.isConnected:
try:
data = self.service.api.login(args[0])
logger.debug('Data received for login: {}'.format(data))
data = data.split('\t')
if len(data) >= 2:
logger.debug('Data is valid: {}'.format(data))
windir = os.environ['windir']
with open(os.path.join(windir, 'remoteip.txt'), 'w') as f:
f.write(data[0])
with open(os.path.join(windir, 'remoteh.txt'), 'w') as f:
f.write(data[1])
except Exception as e:
logger.fatal('Error notifying logon to server: {}'.format(e))
def Logoff(self, *args):
logger.debug('Logoff event: arguments: {}'.format(args))
if self.service is not None and self.service.api is not None and self.service.api.isConnected:
try:
self.service.api.logout(args[0])
except Exception as e:
logger.fatal('Error notifying logoff to server: {}'.format(e))
logger.debug('Invoking onLogout: {}'.format(self.service))
self.service.onLogout(args[0])
logger.debug('Invoked!!')
def StartShell(self, *args):
# logevent('StartShell : %s' % [args])
pass
def DisplayLock(self, *args):
# logevent('DisplayLock : %s' % [args])
pass
def DisplayUnlock(self, *args):
# logevent('DisplayUnlock : %s' % [args])
pass
def StartScreenSaver(self, *args):
# When finished basic actor, we will use this to provide a new parameter: logout on screensaver
# This will allow to easily close sessions of idle users
# logevent('StartScreenSaver : %s' % [args])
pass
def StopScreenSaver(self, *args):
# logevent('StopScreenSaver : %s' % [args])
pass
def logevent(msg):
logger.info(msg)
# def register():
# call the CoInitialize to allow the registration to run in an other
# thread
# pythoncom.CoInitialize()
# logevent('Registring ISensLogon')
# sl=SensLogon()
# subscription_interface=pythoncom.WrapObject(sl)
# event_system=win32com.client.Dispatch(PROGID_EventSystem)
# event_subscription=win32com.client.Dispatch(PROGID_EventSubscription)
# event_subscription.EventClassID=SENSGUID_EVENTCLASS_LOGON
# event_subscription.PublisherID=SENSGUID_PUBLISHER
# event_subscription.SubscriptionName='Python subscription'
# event_subscription.SubscriberInterface=subscription_interface
# event_system.Store(PROGID_EventSubscription, event_subscription)
# pythoncom.PumpMessages()
# #logevent('ISensLogon stopped')

View File

@ -0,0 +1,371 @@
# -*- 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
'''
# pylint: disable=invalid-name
import struct
import subprocess
import os
import stat
import win32serviceutil
import win32service
import win32security
import win32net
import win32event
import win32com.client
import pythoncom
import servicemanager
from udsactor import operations
from udsactor import store
from udsactor.service import CommonService
from udsactor.service import initCfg
from udsactor.log import logger
from .SENS import SensLogon
from .SENS import logevent
from .SENS import SENSGUID_EVENTCLASS_LOGON
from .SENS import SENSGUID_PUBLISHER
from .SENS import PROGID_EventSubscription
from .SENS import PROGID_EventSystem
POST_CMD = 'c:\\windows\\post-uds.bat'
class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService):
'''
This class represents a Windows Service for managing actor interactions
with UDS Broker and Machine
'''
_svc_name_ = "UDSActorNG"
_svc_display_name_ = "UDS Actor Service"
_svc_description_ = "UDS Actor Management Service"
# 'System Event Notification' is the SENS service
_svc_deps_ = ['EventLog', 'SENS']
def __init__(self, args):
win32serviceutil.ServiceFramework.__init__(self, args)
CommonService.__init__(self)
self.hWaitStop = win32event.CreateEvent(None, 1, 0, None)
self._user = None
def SvcStop(self):
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
self.isAlive = False
win32event.SetEvent(self.hWaitStop)
SvcShutdown = SvcStop
def notifyStop(self):
servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
servicemanager.PYS_SERVICE_STOPPED,
(self._svc_name_, ''))
def doWait(self, miliseconds):
win32event.WaitForSingleObject(self.hWaitStop, miliseconds)
def rename(self, name, user=None, oldPassword=None, newPassword=None):
'''
Renames the computer, and optionally sets a password for an user
before this
'''
hostName = operations.getComputerName()
if hostName.lower() == name.lower():
logger.info('Computer name is now {}'.format(hostName))
self.setReady()
return
# Check for password change request for an user
if user is not None:
logger.info('Setting password for user {}'.format(user))
try:
operations.changeUserPassword(user, oldPassword, newPassword)
except Exception as e:
# We stop here without even renaming computer, because the
# process has failed
raise Exception(
'Could not change password for user {} (maybe invalid current password is configured at broker): {} '.format(user, str(e)))
operations.renameComputer(name)
# Reboot just after renaming
logger.info('Rebooting computer to activate new name {}'.format(name))
self.reboot()
def oneStepJoin(self, name, domain, ou, account, password):
'''
Ejecutes the join domain in exactly one step
'''
currName = operations.getComputerName()
# If name is desired, simply execute multiStepJoin, because computer
# name will not change
if currName.lower() == name.lower():
self.multiStepJoin(name, domain, ou, account, password)
else:
operations.renameComputer(name)
logger.debug('Computer renamed to {} without reboot'.format(name))
operations.joinDomain(
domain, ou, account, password, executeInOneStep=True)
logger.debug(
'Requested join domain {} without errors'.format(domain))
self.reboot()
def multiStepJoin(self, name, domain, ou, account, password):
currName = operations.getComputerName()
if currName.lower() == name.lower():
currDomain = operations.getDomainName()
if currDomain is not None:
# logger.debug('Name: "{}" vs "{}", Domain: "{}" vs "{}"'.format(currName.lower(), name.lower(), currDomain.lower(), domain.lower()))
logger.info('Machine {} is part of domain {}'.format(name, domain))
self.setReady()
else:
operations.joinDomain(
domain, ou, account, password, executeInOneStep=False)
self.reboot()
else:
operations.renameComputer(name)
logger.info(
'Rebooting computer got activate new name {}'.format(name))
self.reboot()
def joinDomain(self, name, domain, ou, account, password):
ver = operations.getWindowsVersion()
ver = ver[0] * 10 + ver[1]
logger.debug('Starting joining domain {} with name {} (detected operating version: {})'.format(
domain, name, ver))
# If file c:\compat.bin exists, joind domain in two steps instead one
# Accepts one step joinDomain, also remember XP is no more supported by
# microsoft, but this also must works with it because will do a "multi
# step" join
if ver >= 60 and store.useOldJoinSystem() is False:
self.oneStepJoin(name, domain, ou, account, password)
else:
logger.info('Using multiple step join because configuration requests to do so')
self.multiStepJoin(name, domain, ou, account, password)
def preConnect(self, user, protocol):
logger.debug('Pre connect invoked')
if protocol != 'rdp': # If connection is not using rdp, skip adding user
return 'ok'
# Well known SSID for Remote Desktop Users
REMOTE_USERS_SID = 'S-1-5-32-555'
p = win32security.GetBinarySid(REMOTE_USERS_SID)
groupName = win32security.LookupAccountSid(None, p)[0]
useraAlreadyInGroup = False
resumeHandle = 0
while True:
users, _, resumeHandle = win32net.NetLocalGroupGetMembers(None, groupName, 1, resumeHandle, 32768)
if user.lower() in [u['name'].lower() for u in users]:
useraAlreadyInGroup = True
break
if resumeHandle == 0:
break
if useraAlreadyInGroup is False:
logger.debug('User not in group, adding it')
self._user = user
try:
userSSID = win32security.LookupAccountName(None, user)[0]
win32net.NetLocalGroupAddMembers(None, groupName, 0, [{'sid': userSSID}])
except Exception as e:
logger.error('Exception adding user to Remote Desktop Users: {}'.format(e))
else:
self._user = None
logger.debug('User {} already in group'.format(user))
# Now try to run pre connect command
try:
pre_cmd = store.preApplication()
if os.path.isfile(pre_cmd):
if (os.stat(pre_cmd).st_mode & stat.S_IXUSR) != 0:
subprocess.call([pre_cmd, user, protocol])
else:
logger.info('PRECONNECT file exists but it it is not executable (needs execution permission by root)')
else:
logger.info('PRECONNECT file not found & not executed')
except Exception as e:
# Ignore output of execution command
logger.error('Executing preconnect command give')
return 'ok'
def ovLogon(self, username, password):
# Compose packet for ov
ub = username.encode('utf8')
up = username.encode('utf8')
packet = struct.pack('!I', len(ub)) + ub + struct.pack('!I', len(up)) + up
# Send packet with username/password to ov pipe
operations.writeToPipe("\\\\.\\pipe\\VDSMDPipe", packet, True)
return 'done'
def onLogout(self, user):
logger.debug('Windows onLogout invoked: {}, {}'.format(user, self._user))
try:
REMOTE_USERS_SID = 'S-1-5-32-555'
p = win32security.GetBinarySid(REMOTE_USERS_SID)
groupName = win32security.LookupAccountSid(None, p)[0]
except Exception:
logger.error('Exception getting Windows Group')
return
if self._user is not None:
try:
win32net.NetLocalGroupDelMembers(None, groupName, [self._user])
except Exception as e:
logger.error('Exception removing user from Remote Desktop Users: {}'.format(e))
def SvcDoRun(self): # pylint: disable=too-many-statements, too-many-branches
'''
Main service loop
'''
try:
initCfg()
logger.debug('running SvcDoRun')
servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
servicemanager.PYS_SERVICE_STARTED,
(self._svc_name_, ''))
# call the CoInitialize to allow the registration to run in an other
# thread
logger.debug('Initializing com...')
pythoncom.CoInitialize()
# ********************************************************
# * Ask brokers what to do before proceding to main loop *
# ********************************************************
while True:
brokerConnected = self.interactWithBroker()
if brokerConnected is False:
logger.debug('Interact with broker returned false, stopping service after a while')
self.notifyStop()
win32event.WaitForSingleObject(self.hWaitStop, 5000)
return
elif brokerConnected is True:
break
# If brokerConnected returns None, repeat the cycle
self.doWait(16000) # Wait for a looong while
if self.interactWithBroker() is False:
logger.debug('Interact with broker returned false, stopping service after a while')
self.notifyStop()
win32event.WaitForSingleObject(self.hWaitStop, 5000)
return
if self.isAlive is False:
logger.debug('The service is not alive after broker interaction, stopping it')
self.notifyStop()
return
if self.rebootRequested is True:
logger.debug('Reboot has been requested, stopping service')
self.notifyStop()
return
self.initIPC()
except Exception: # Any init exception wil be caught, service must be then restarted
logger.exception()
logger.debug('Exiting service with failure status')
os._exit(-1) # pylint: disable=protected-access
# ********************************
# * Registers SENS subscriptions *
# ********************************
logevent('Registering ISensLogon')
subscription_guid = '{41099152-498E-11E4-8FD3-10FEED05884B}'
sl = SensLogon(self)
subscription_interface = pythoncom.WrapObject(sl)
event_system = win32com.client.Dispatch(PROGID_EventSystem)
event_subscription = win32com.client.Dispatch(PROGID_EventSubscription)
event_subscription.EventClassID = SENSGUID_EVENTCLASS_LOGON
event_subscription.PublisherID = SENSGUID_PUBLISHER
event_subscription.SubscriptionName = 'UDS Actor subscription'
event_subscription.SubscriptionID = subscription_guid
event_subscription.SubscriberInterface = subscription_interface
event_system.Store(PROGID_EventSubscription, event_subscription)
logger.debug('Registered SENS, running main loop')
# Execute script in c:\\windows\\post-uds.bat after interacting with broker, if no reboot is requested ofc
# This will be executed only when machine gets "ready"
try:
if os.path.isfile(POST_CMD):
subprocess.call([POST_CMD, ])
else:
logger.info('POST file not found & not executed')
except Exception as e:
# Ignore output of execution command
logger.error('Executing post command give')
# *********************
# * Main Service loop *
# *********************
# Counter used to check ip changes only once every 10 seconds, for
# example
counter = 0
while self.isAlive:
counter += 1
# Process SENS messages, This will be a bit asyncronous (1 second
# delay)
pythoncom.PumpWaitingMessages()
if counter >= 15: # Once every 15 seconds
counter = 0
try:
self.checkIpsChanged()
except Exception as e:
logger.error('Error checking ip change: {}'.format(e))
# In milliseconds, will break
win32event.WaitForSingleObject(self.hWaitStop, 1000)
logger.debug('Exited main loop, deregistering SENS')
# *******************************************
# * Remove SENS subscription before exiting *
# *******************************************
event_system.Remove(
PROGID_EventSubscription, "SubscriptionID == " + subscription_guid)
self.endIPC() # Ends IPC servers
self.endAPI() # And deinitializes REST api if needed
self.notifyStop()
if __name__ == '__main__':
initCfg()
win32serviceutil.HandleCommandLine(UDSActorSvc)

View File

@ -0,0 +1,32 @@
# -*- 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

View File

@ -0,0 +1,77 @@
# -*- 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 servicemanager # @UnresolvedImport, pylint: disable=import-error
import logging
import os
import tempfile
# 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(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
)
self.logger = logging.getLogger('udsactor')
self.serviceLogger = False
def log(self, level, message):
# 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(level // 1000 - 10, message)
if level < INFO or self.serviceLogger is False: # Only information and above will be on event log
return
if level < WARN: # Info
servicemanager.LogInfoMsg(message)
elif level < ERROR: # WARN
servicemanager.LogWarningMsg(message)
else: # Error & Fatal
servicemanager.LogErrorMsg(message)
def isWindows(self):
return True
def isLinux(self):
return False

View File

@ -0,0 +1,243 @@
# -*- 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 win32com.client # @UnresolvedImport, pylint: disable=import-error
import win32net # @UnresolvedImport, pylint: disable=import-error
import win32security # @UnresolvedImport, pylint: disable=import-error
import win32api # @UnresolvedImport, pylint: disable=import-error
import win32con # @UnresolvedImport, pylint: disable=import-error
import ctypes
from ctypes.wintypes import DWORD, LPCWSTR
import os
from udsactor import utils
from udsactor.log import logger
def getErrorMessage(res=0):
# sys_fs_enc = sys.getfilesystemencoding() or 'mbcs'
msg = win32api.FormatMessage(res)
return msg.decode('windows-1250', 'ignore')
def getComputerName():
return win32api.GetComputerNameEx(win32con.ComputerNamePhysicalDnsHostname)
def getNetworkInfo():
obj = win32com.client.Dispatch("WbemScripting.SWbemLocator")
wmobj = obj.ConnectServer("localhost", "root\\cimv2")
adapters = wmobj.ExecQuery("Select * from Win32_NetworkAdapterConfiguration where IpEnabled=True")
try:
for obj in adapters:
for ip in obj.IPAddress:
if ':' in ip: # Is IPV6, skip this
continue
if ip is None or ip == '' or ip.startswith('169.254') or ip.startswith('0.'): # If single link ip, or no ip
continue
# logger.debug('Net config found: {}=({}, {})'.format(obj.Caption, obj.MACAddress, ip))
yield utils.Bunch(name=obj.Caption, mac=obj.MACAddress, ip=ip)
except Exception:
return
def getDomainName():
'''
Will return the domain name if we belong a domain, else None
(if part of a network group, will also return None)
'''
# Status:
# 0 = Unknown
# 1 = Unjoined
# 2 = Workgroup
# 3 = Domain
domain, status = win32net.NetGetJoinInformation()
if status != 3:
domain = None
return domain
def getWindowsVersion():
return win32api.GetVersionEx()
EWX_LOGOFF = 0x00000000
EWX_SHUTDOWN = 0x00000001
EWX_REBOOT = 0x00000002
EWX_FORCE = 0x00000004
EWX_POWEROFF = 0x00000008
EWX_FORCEIFHUNG = 0x00000010
def reboot(flags=EWX_FORCEIFHUNG | EWX_REBOOT):
hproc = win32api.GetCurrentProcess()
htok = win32security.OpenProcessToken(hproc, win32security.TOKEN_ADJUST_PRIVILEGES | win32security.TOKEN_QUERY)
privs = ((win32security.LookupPrivilegeValue(None, win32security.SE_SHUTDOWN_NAME), win32security.SE_PRIVILEGE_ENABLED),)
win32security.AdjustTokenPrivileges(htok, 0, privs)
win32api.ExitWindowsEx(flags, 0)
def loggoff():
win32api.ExitWindowsEx(EWX_LOGOFF)
def renameComputer(newName):
# Needs admin privileges to work
if ctypes.windll.kernel32.SetComputerNameExW(DWORD(win32con.ComputerNamePhysicalDnsHostname), LPCWSTR(newName)) == 0: # @UndefinedVariable
# win32api.FormatMessage -> returns error string
# win32api.GetLastError -> returns error code
# (just put this comment here to remember to log this when logger is available)
error = getErrorMessage()
computerName = win32api.GetComputerNameEx(win32con.ComputerNamePhysicalDnsHostname)
raise Exception('Error renaming computer from {} to {}: {}'.format(computerName, newName, error))
NETSETUP_JOIN_DOMAIN = 0x00000001
NETSETUP_ACCT_CREATE = 0x00000002
NETSETUP_ACCT_DELETE = 0x00000004
NETSETUP_WIN9X_UPGRADE = 0x00000010
NETSETUP_DOMAIN_JOIN_IF_JOINED = 0x00000020
NETSETUP_JOIN_UNSECURE = 0x00000040
NETSETUP_MACHINE_PWD_PASSED = 0x00000080
NETSETUP_JOIN_WITH_NEW_NAME = 0x00000400
NETSETUP_DEFER_SPN_SET = 0x1000000
def joinDomain(domain, ou, account, password, executeInOneStep=False):
'''
Joins machine to a windows domain
:param domain: Domain to join to
:param ou: Ou that will hold machine
:param account: Account used to join domain
:param password: Password of account used to join domain
:param executeInOneStep: If true, means that this machine has been renamed and wants to add NETSETUP_JOIN_WITH_NEW_NAME to request so we can do rename/join in one step.
'''
# If account do not have domain, include it
if '@' not in account and '\\' not in account:
if '.' in domain:
account = account + '@' + domain
else:
account = domain + '\\' + account
# Do log
flags = NETSETUP_ACCT_CREATE | NETSETUP_DOMAIN_JOIN_IF_JOINED | NETSETUP_JOIN_DOMAIN
if executeInOneStep:
flags |= NETSETUP_JOIN_WITH_NEW_NAME
flags = DWORD(flags)
domain = LPCWSTR(domain)
# Must be in format "ou=.., ..., dc=...,"
ou = LPCWSTR(ou) if ou is not None and ou != '' else None
account = LPCWSTR(account)
password = LPCWSTR(password)
res = ctypes.windll.netapi32.NetJoinDomain(None, domain, ou, account, password, flags)
# Machine found in another ou, use it and warn this on log
if res == 2224:
flags = DWORD(NETSETUP_DOMAIN_JOIN_IF_JOINED | NETSETUP_JOIN_DOMAIN)
res = ctypes.windll.netapi32.NetJoinDomain(None, domain, None, account, password, flags)
if res != 0:
# Log the error
error = getErrorMessage(res)
if res == 1355:
error = "DC Is not reachable"
logger.error('Error joining domain: {}, {}'.format(error, res))
raise Exception('Error joining domain {}, with credentials {}/*****{}: {}, {}'.format(domain, account, ', under OU {}'.format(ou) if ou is not None else '', res, error))
def changeUserPassword(user, oldPassword, newPassword):
computerName = LPCWSTR(getComputerName())
user = LPCWSTR(user)
oldPassword = LPCWSTR(oldPassword)
newPassword = LPCWSTR(newPassword)
res = ctypes.windll.netapi32.NetUserChangePassword(computerName, user, oldPassword, newPassword)
if res != 0:
# Log the error, and raise exception to parent
error = getErrorMessage(res)
raise Exception('Error changing password for user {}: {} {}'.format(user.value, res, error))
class LASTINPUTINFO(ctypes.Structure):
_fields_ = [
('cbSize', ctypes.c_uint),
('dwTime', ctypes.c_uint),
]
def initIdleDuration(atLeastSeconds):
'''
In windows, there is no need to set screensaver
'''
pass
def getIdleDuration():
try:
lastInputInfo = LASTINPUTINFO()
lastInputInfo.cbSize = ctypes.sizeof(lastInputInfo)
if ctypes.windll.user32.GetLastInputInfo(ctypes.byref(lastInputInfo)) == 0:
return 0
# if lastInputInfo.dwTime > 1000000000: # Value toooo high, nonsense...
# return 0
millis = ctypes.windll.kernel32.GetTickCount() - lastInputInfo.dwTime # @UndefinedVariable
if millis < 0 or millis > 1000000000:
return 0
return millis / 1000.0
except Exception as e:
logger.error('Getting idle duration: {}'.format(e))
return 0
def getCurrentUser():
'''
Returns current logged in username
'''
return os.environ['USERNAME']
def writeToPipe(pipeName, bytesPayload, waitForResponse):
# (str, bytes, bool) -> Optional[bytes]
try:
with open(pipeName, 'r+b', 0) as f:
f.write(bytesPayload)
# f.seek(0) # As recommended on intenet, but seems to work fin without thos
if waitForResponse:
return f.read()
return b'ok'
except Exception as e:
None

View File

@ -0,0 +1,143 @@
# -*- 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 pickle
from win32com.shell import shell # @UnresolvedImport, pylint: disable=import-error
try:
import winreg as wreg
except ImportError: # Python 2.7 fallback
import _winreg as wreg # @UnresolvedImport, pylint: disable=import-error
import win32security # @UnresolvedImport, pylint: disable=import-error
DEBUG = False
# Can be changed to whatever we want, but registry key is protected by permissions
def encoder(data):
return data.encode('bz2')
def decoder(data):
return data.decode('bz2')
path = 'Software\\UDSActor'
baseKey = wreg.HKEY_CURRENT_USER if DEBUG is True else wreg.HKEY_LOCAL_MACHINE # @UndefinedVariable
def checkPermissions():
return True if DEBUG else shell.IsUserAnAdmin()
def fixRegistryPermissions(handle):
if DEBUG:
return
# Fix permissions so users can't read this key
v = win32security.GetSecurityInfo(handle, win32security.SE_REGISTRY_KEY, win32security.DACL_SECURITY_INFORMATION)
dacl = v.GetSecurityDescriptorDacl()
n = 0
# Remove all normal users access permissions to the registry key
while n < dacl.GetAceCount():
if unicode(dacl.GetAce(n)[2]) == u'PySID:S-1-5-32-545': # Whell known Users SID
dacl.DeleteAce(n)
else:
n += 1
win32security.SetSecurityInfo(handle, win32security.SE_REGISTRY_KEY,
win32security.DACL_SECURITY_INFORMATION | win32security.PROTECTED_DACL_SECURITY_INFORMATION,
None, None, dacl, None)
def readConfig():
try:
key = wreg.OpenKey(baseKey, path, 0, wreg.KEY_QUERY_VALUE) # @UndefinedVariable
data, _ = wreg.QueryValueEx(key, '') # @UndefinedVariable
wreg.CloseKey(key) # @UndefinedVariable
return pickle.loads(decoder(data))
except Exception:
return None
def writeConfig(data, fixPermissions=True):
try:
key = wreg.OpenKey(baseKey, path, 0, wreg.KEY_ALL_ACCESS) # @UndefinedVariable
except Exception:
key = wreg.CreateKeyEx(baseKey, path, 0, wreg.KEY_ALL_ACCESS) # @UndefinedVariable
if fixPermissions is True:
fixRegistryPermissions(key.handle)
wreg.SetValueEx(key, "", 0, wreg.REG_BINARY, encoder(pickle.dumps(data))) # @UndefinedVariable
wreg.CloseKey(key) # @UndefinedVariable
def useOldJoinSystem():
try:
key = wreg.OpenKey(baseKey, 'Software\\UDSEnterpriseActor', 0, wreg.KEY_QUERY_VALUE) # @UndefinedVariable
try:
data, _ = wreg.QueryValueEx(key, 'join') # @UndefinedVariable
except Exception:
data = ''
wreg.CloseKey(key) # @UndefinedVariable
except:
data = ''
return data == 'old'
# Gives the oportunity to run an application ONE TIME (because, the registry key "run" will be deleted after read)
def runApplication():
try:
key = wreg.OpenKey(baseKey, 'Software\\UDSEnterpriseActor', 0, wreg.KEY_ALL_ACCESS) # @UndefinedVariable
try:
data, _ = wreg.QueryValueEx(key, 'run') # @UndefinedVariable
wreg.DeleteValue(key, 'run') # @UndefinedVariable
except Exception:
data = None
wreg.CloseKey(key) # @UndefinedVariable
except Exception:
data = None
return data
def preApplication():
try:
key = wreg.OpenKey(baseKey, 'Software\\UDSEnterpriseActor', 0, wreg.KEY_ALL_ACCESS) # @UndefinedVariable
try:
data, _ = wreg.QueryValueEx(key, 'pre') # @UndefinedVariable
except Exception:
data = None
wreg.CloseKey(key) # @UndefinedVariable
except Exception:
data = None
return data