devel 8.5.9 #76
17
CHANGELOG.md
17
CHANGELOG.md
|
|
@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [8.5.9] - 2026-07-23
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Preserve original exit code in Windows PowerShell script wrapper when redirecting streams, ensuring ogcore receives failure status (e.g., CommandNotFoundException).
|
||||||
|
- Prevent raw PowerShell CLIXML output from leaking into stderr on Windows when a command fails non-interactively.
|
||||||
|
- Removed the `START_SCRIPT` webhook entirely from the script launching process for both admin (`client=false`) and user-session (`client=true`) paths. This eliminates the race condition that caused duplicate traces in `ogcore`, as `ogcore` now marks traces as `IN_PROGRESS` synchronously.
|
||||||
|
|
||||||
|
## [8.5.6] - 2026-07-21
|
||||||
|
### Added
|
||||||
|
- Notify `ogcore` via webhook when natively executed scripts finish, bringing `ogagent` in line with `ogLive` functionality (resolves stuck traces in "Enviada" state).
|
||||||
|
|
||||||
## [8.5.5] - 2026-05-13
|
## [8.5.5] - 2026-05-13
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
@ -13,6 +24,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Promote job completion log in `oglive_worker.mon()` from `DEBUG` to `INFO`, now including the job result.
|
- Promote job completion log in `oglive_worker.mon()` from `DEBUG` to `INFO`, now including the job result.
|
||||||
- When `interfaceAdmin()` fails, log the script stdout at `WARNING` in addition to the existing `ERROR` for stderr, providing full diagnostic context without requiring a debug-level rerun.
|
- When `interfaceAdmin()` fails, log the script stdout at `WARNING` in addition to the existing `ERROR` for stderr, providing full diagnostic context without requiring a debug-level rerun.
|
||||||
|
|
||||||
|
## [8.5.5] - 2026-05-20
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- On machines with multiple network interfaces, the agent now selects the interface whose IP the OS routing table would use to reach ogcore, instead of always picking the first interface returned by the OS. This ensures the agent registers with the correct IP on multi-homed machines. If the routing query fails (network not ready yet) the detection loop keeps retrying rather than accepting a wrong interface. Falls back to the first available interface only when the routing query succeeds but no interface matches (e.g. NAT or VPN).
|
||||||
|
|
||||||
## [8.5.4] - 2026-05-11
|
## [8.5.4] - 2026-05-11
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,20 @@ pipeline {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
stage('Generate Changelog (Devel)') {
|
||||||
|
when {
|
||||||
|
branch 'devel'
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
def devName = params.DEV_NAME ? params.DEV_NAME : env.DEFAULT_DEV_NAME
|
||||||
|
def devEmail = params.DEV_EMAIL ? params.DEV_EMAIL : env.DEFAULT_DEV_EMAIL
|
||||||
|
generateDebianChangelog(env.BUILD_DIR, devName, devEmail, "devel")
|
||||||
|
sh "mkdir -p ${env.BUILD_DIR}/debian/"
|
||||||
|
sh "cp ${env.BUILD_DIR}/debian/changelog ${env.BUILD_DIR}/linux/debian/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
stage('Build') {
|
stage('Build') {
|
||||||
steps {
|
steps {
|
||||||
dir ("${env.BUILD_DIR}") {
|
dir ("${env.BUILD_DIR}") {
|
||||||
|
|
@ -112,8 +126,32 @@ pipeline {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stage ('Publish to Debian Repository (Devel)') {
|
||||||
|
when {
|
||||||
|
branch 'devel'
|
||||||
|
}
|
||||||
|
agent { label 'debian-repo' }
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
def versionPattern = "-${env.BUILD_NUMBER}~devel"
|
||||||
|
publicarEnAptly('/var/tmp/opengnsys/debian-repo/ogagent', 'devel', versionPattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
post {
|
post {
|
||||||
|
success {
|
||||||
|
script {
|
||||||
|
if (env.BRANCH_NAME == 'devel') {
|
||||||
|
build job: 'Aptly publish develop repository',
|
||||||
|
wait: false,
|
||||||
|
parameters: [
|
||||||
|
string(name: 'TRIGGERED_BY', value: "${env.JOB_NAME}-${env.BUILD_NUMBER}")
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
always {
|
always {
|
||||||
notifyBuildStatus('narenas@qindel.com')
|
notifyBuildStatus('narenas@qindel.com')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ install-ogagent:
|
||||||
|
|
||||||
cp $(SOURCEDIR)/OGAgentUser.py $(LIBDIR)
|
cp $(SOURCEDIR)/OGAgentUser.py $(LIBDIR)
|
||||||
# QT Dialogs & resources
|
# QT Dialogs & resources
|
||||||
|
cd $(SOURCEDIR) && python3 update.py
|
||||||
cp $(SOURCEDIR)/*_ui.py $(LIBDIR)
|
cp $(SOURCEDIR)/*_ui.py $(LIBDIR)
|
||||||
# Version file
|
# Version file
|
||||||
cp $(SOURCEDIR)/VERSION $(LIBDIR)
|
cp $(SOURCEDIR)/VERSION $(LIBDIR)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
ogagent (8.5.9-1) stable; urgency=medium
|
||||||
|
|
||||||
|
* Removed START_SCRIPT webhook to fix race conditions causing duplicate traces
|
||||||
|
* Added script completion webhook to resolve stuck traces in Enviada state
|
||||||
|
|
||||||
|
-- OpenGnsys developers <info@opengnsys.es> Wed, 29 Jul 2026 13:58:00 +0200
|
||||||
|
|
||||||
|
ogagent (8.5.5-1) stable; urgency=medium
|
||||||
|
|
||||||
|
* Remove obsolete dependencies (six, prctl, libxss1) to support modern Ubuntu releases (like 26.04)
|
||||||
|
* Update policykit-1 to polkitd
|
||||||
|
|
||||||
|
-- OpenGnsys developers <info@opengnsys.es> Thu, 09 Jul 2026 12:00:00 +0200
|
||||||
|
|
||||||
ogagent (8.5.4-1) stable; urgency=medium
|
ogagent (8.5.4-1) stable; urgency=medium
|
||||||
|
|
||||||
* Fix network detection race condition on slow-booting machines: activation
|
* Fix network detection race condition on slow-booting machines: activation
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ Source: ogagent
|
||||||
Section: admin
|
Section: admin
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Maintainer: OpenGnsys developers <info@opengnsys.es>
|
Maintainer: OpenGnsys developers <info@opengnsys.es>
|
||||||
Build-Depends: debhelper (>= 7), po-debconf
|
Build-Depends: debhelper (>= 7), po-debconf, pyqt6-dev-tools
|
||||||
Standards-Version: 3.9.2
|
Standards-Version: 3.9.2
|
||||||
Homepage: https://opengnsys.es/
|
Homepage: https://opengnsys.es/
|
||||||
|
|
||||||
|
|
@ -11,7 +11,7 @@ Section: admin
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Architecture: all
|
Architecture: all
|
||||||
Depends:
|
Depends:
|
||||||
policykit-1 (>= 0.100), python3 (>=3.4) | python (>= 3.4), python3-pyqt6, python3-requests,
|
polkitd | policykit-1 (>= 0.100), python3 (>=3.4), python3-pyqt6, python3-requests,
|
||||||
python3-six, python3-prctl, python3-distro, libxss1, zenity, ${misc:Depends}
|
python3-distro, zenity, ${misc:Depends}
|
||||||
Description: OpenGnsys Agent for Operating Systems
|
Description: OpenGnsys Agent for Operating Systems
|
||||||
This package provides the required components to allow this machine to work on an environment managed by OpenGnsys.
|
This package provides the required components to allow this machine to work on an environment managed by OpenGnsys.
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
8.3.1
|
8.5.9
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="VersionLabel">
|
<widget class="QLabel" name="VersionLabel">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Version 0</string>
|
<string>Version 8.5.6</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|
|
||||||
|
|
@ -31,13 +31,10 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
# On centos, old six release does not includes byte2int, nor six.PY2
|
|
||||||
import six
|
|
||||||
|
|
||||||
from . import modules
|
from . import modules
|
||||||
from .RESTApi import REST, RESTError
|
from .RESTApi import REST, RESTError
|
||||||
|
|
||||||
VERSION='0'
|
VERSION='8.5.6'
|
||||||
|
|
||||||
__title__ = 'OpenGnsys Agent'
|
__title__ = 'OpenGnsys Agent'
|
||||||
__version__ = VERSION
|
__version__ = VERSION
|
||||||
|
|
@ -46,11 +43,4 @@ __author__ = 'Adolfo Gómez'
|
||||||
__license__ = "BSD 3-clause"
|
__license__ = "BSD 3-clause"
|
||||||
__copyright__ = "Copyright VirtualCable S.L.U."
|
__copyright__ = "Copyright VirtualCable S.L.U."
|
||||||
|
|
||||||
if not hasattr(six, 'byte2int'):
|
|
||||||
if six.PY3:
|
|
||||||
import operator
|
|
||||||
six.byte2int = operator.itemgetter(0)
|
|
||||||
else:
|
|
||||||
def _byte2int(bs):
|
|
||||||
return ord(bs[0])
|
|
||||||
six.byte2int = _byte2int
|
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,9 @@ import os
|
||||||
import json
|
import json
|
||||||
import ssl
|
import ssl
|
||||||
import threading
|
import threading
|
||||||
from six.moves.socketserver import ThreadingMixIn # @UnresolvedImport
|
from socketserver import ThreadingMixIn
|
||||||
from six.moves.BaseHTTPServer import BaseHTTPRequestHandler # @UnresolvedImport
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
from six.moves.BaseHTTPServer import HTTPServer # @UnresolvedImport
|
from urllib.parse import unquote
|
||||||
from six.moves.urllib.parse import unquote # @UnresolvedImport
|
|
||||||
|
|
||||||
from .utils import exceptionToMessage
|
from .utils import exceptionToMessage
|
||||||
from .certs import createSelfSignedCert
|
from .certs import createSelfSignedCert
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ class JobMgr():
|
||||||
ts = now.strftime ('%Y-%m-%d %H:%M:%S.%f%z') ## '%s' doesn't work on windows
|
ts = now.strftime ('%Y-%m-%d %H:%M:%S.%f%z') ## '%s' doesn't work on windows
|
||||||
job_id = hashlib.sha256 (now.isoformat().encode('UTF-8') + script.encode ('UTF-8')).hexdigest()[0:12]
|
job_id = hashlib.sha256 (now.isoformat().encode('UTF-8') + script.encode ('UTF-8')).hexdigest()[0:12]
|
||||||
p = subprocess.Popen (args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
p = subprocess.Popen (args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
self.jobs[job_id] = { 'p': p, 'pid': p.pid, 'starttime': ts, 'script': script, 'client': is_client, 'status': 'running', 'stdout': '', 'stderr': '' }
|
self.jobs[job_id] = { 'p': p, 'pid': p.pid, 'starttime': ts, 'script': script, 'client': is_client, 'status': 'running', 'stdout': '', 'stderr': '', 'notified': False }
|
||||||
self.jobs[job_id]['t1'] = threading.Thread (target=job_readstdout, args=(self.jobs[job_id],))
|
self.jobs[job_id]['t1'] = threading.Thread (target=job_readstdout, args=(self.jobs[job_id],))
|
||||||
self.jobs[job_id]['t2'] = threading.Thread (target=job_readstderr, args=(self.jobs[job_id],))
|
self.jobs[job_id]['t2'] = threading.Thread (target=job_readstderr, args=(self.jobs[job_id],))
|
||||||
self.jobs[job_id]['t1'].start()
|
self.jobs[job_id]['t1'].start()
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,6 @@ import ctypes.util
|
||||||
import subprocess
|
import subprocess
|
||||||
import struct
|
import struct
|
||||||
import array
|
import array
|
||||||
import six
|
|
||||||
import distro
|
import distro
|
||||||
from opengnsys import utils
|
from opengnsys import utils
|
||||||
|
|
||||||
|
|
@ -53,12 +52,12 @@ def _getMacAddr(ifname):
|
||||||
'''
|
'''
|
||||||
if isinstance(ifname, list):
|
if isinstance(ifname, list):
|
||||||
return dict([(name, _getMacAddr(name)) for name in ifname])
|
return dict([(name, _getMacAddr(name)) for name in ifname])
|
||||||
if isinstance(ifname, six.text_type):
|
if isinstance(ifname, str):
|
||||||
ifname = ifname.encode('utf-8') # If unicode, convert to bytes (or str in python 2.7)
|
ifname = ifname.encode('utf-8') # If unicode, convert to bytes (or str in python 2.7)
|
||||||
try:
|
try:
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
info = bytearray(fcntl.ioctl(s.fileno(), 0x8927, struct.pack(str('256s'), ifname[:15])))
|
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])
|
return str(''.join(['%02x:' % char for char in info[18:24]])[:-1])
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -70,11 +69,11 @@ def _getIpAddr(ifname):
|
||||||
'''
|
'''
|
||||||
if isinstance(ifname, list):
|
if isinstance(ifname, list):
|
||||||
return dict([(name, _getIpAddr(name)) for name in ifname])
|
return dict([(name, _getIpAddr(name)) for name in ifname])
|
||||||
if isinstance(ifname, six.text_type):
|
if isinstance(ifname, str):
|
||||||
ifname = ifname.encode('utf-8') # If unicode, convert to bytes (or str in python 2.7)
|
ifname = ifname.encode('utf-8') # If unicode, convert to bytes (or str in python 2.7)
|
||||||
try:
|
try:
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
return six.text_type(socket.inet_ntoa(fcntl.ioctl(
|
return str(socket.inet_ntoa(fcntl.ioctl(
|
||||||
s.fileno(),
|
s.fileno(),
|
||||||
0x8915, # SIOCGIFADDR
|
0x8915, # SIOCGIFADDR
|
||||||
struct.pack(str('256s'), ifname[:15])
|
struct.pack(str('256s'), ifname[:15])
|
||||||
|
|
@ -138,11 +137,6 @@ def reboot(flags=0):
|
||||||
'''
|
'''
|
||||||
Simple reboot using os command
|
Simple reboot using os command
|
||||||
'''
|
'''
|
||||||
# Workaround for dummy thread
|
|
||||||
if six.PY3 is False:
|
|
||||||
import threading
|
|
||||||
threading._DummyThread._Thread__stop = lambda x: 42
|
|
||||||
|
|
||||||
# Check for OpenGnsys Client or GNU/Linux distribution.
|
# Check for OpenGnsys Client or GNU/Linux distribution.
|
||||||
if os.path.exists('/scripts/oginit'):
|
if os.path.exists('/scripts/oginit'):
|
||||||
subprocess.call('source /opt/opengnsys/etc/preinit/loadenviron.sh; /opt/opengnsys/scripts/reboot', shell=True)
|
subprocess.call('source /opt/opengnsys/etc/preinit/loadenviron.sh; /opt/opengnsys/scripts/reboot', shell=True)
|
||||||
|
|
@ -154,11 +148,6 @@ def poweroff(flags=0):
|
||||||
'''
|
'''
|
||||||
Simple poweroff using os command
|
Simple poweroff using os command
|
||||||
'''
|
'''
|
||||||
# Workaround for dummy thread
|
|
||||||
if six.PY3 is False:
|
|
||||||
import threading
|
|
||||||
threading._DummyThread._Thread__stop = lambda x: 42
|
|
||||||
|
|
||||||
# Check for OpenGnsys Client or GNU/Linux distribution.
|
# Check for OpenGnsys Client or GNU/Linux distribution.
|
||||||
if os.path.exists('/scripts/oginit'):
|
if os.path.exists('/scripts/oginit'):
|
||||||
subprocess.call('source /opt/opengnsys/etc/preinit/loadenviron.sh; /opt/opengnsys/scripts/poweroff', shell=True)
|
subprocess.call('source /opt/opengnsys/etc/preinit/loadenviron.sh; /opt/opengnsys/scripts/poweroff', shell=True)
|
||||||
|
|
@ -171,11 +160,6 @@ def logoff():
|
||||||
Kills all curent user processes, which must send a logogof
|
Kills all curent user processes, which must send a logogof
|
||||||
caveat: If the user has other sessions, will also disconnect from them
|
caveat: If the user has other sessions, will also disconnect from them
|
||||||
'''
|
'''
|
||||||
# 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(['/usr/bin/pkill', '-u', os.environ['USER']])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,6 @@ import ctypes.util
|
||||||
import subprocess
|
import subprocess
|
||||||
import struct
|
import struct
|
||||||
import array
|
import array
|
||||||
import six
|
|
||||||
import json
|
import json
|
||||||
from opengnsys import utils
|
from opengnsys import utils
|
||||||
|
|
||||||
|
|
@ -134,11 +133,6 @@ def reboot(flags=0):
|
||||||
'''
|
'''
|
||||||
Simple reboot command
|
Simple reboot command
|
||||||
'''
|
'''
|
||||||
# Workaround for dummy thread
|
|
||||||
if six.PY3 is False:
|
|
||||||
import threading
|
|
||||||
threading._DummyThread._Thread__stop = lambda x: 42
|
|
||||||
|
|
||||||
# Exec reboot command
|
# Exec reboot command
|
||||||
subprocess.call('/sbin/shutdown -r now', shell=True)
|
subprocess.call('/sbin/shutdown -r now', shell=True)
|
||||||
|
|
||||||
|
|
@ -147,11 +141,6 @@ def poweroff(flags=0):
|
||||||
'''
|
'''
|
||||||
Simple poweroff command
|
Simple poweroff command
|
||||||
'''
|
'''
|
||||||
# Workaround for dummy thread
|
|
||||||
if six.PY3 is False:
|
|
||||||
import threading
|
|
||||||
threading._DummyThread._Thread__stop = lambda x: 42
|
|
||||||
|
|
||||||
# Exec shutdown command
|
# Exec shutdown command
|
||||||
subprocess.call('/sbin/shutdown -h now', shell=True)
|
subprocess.call('/sbin/shutdown -h now', shell=True)
|
||||||
|
|
||||||
|
|
@ -160,11 +149,6 @@ def logoff():
|
||||||
'''
|
'''
|
||||||
Simple logout using AppleScript
|
Simple logout using AppleScript
|
||||||
'''
|
'''
|
||||||
# Workaround for dummy thread
|
|
||||||
if six.PY3 is False:
|
|
||||||
import threading
|
|
||||||
threading._DummyThread._Thread__stop = lambda x: 42
|
|
||||||
|
|
||||||
# Exec logout using AppleScript
|
# Exec logout using AppleScript
|
||||||
subprocess.call('/usr/bin/osascript -e \'tell app "System Events" to «event aevtrlgo»\'', shell=True)
|
subprocess.call('/usr/bin/osascript -e \'tell app "System Events" to «event aevtrlgo»\'', shell=True)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,35 @@ class OpenGnSysWorker(ClientWorker):
|
||||||
job_id = self.jobmgr.launch_job (script, True)
|
job_id = self.jobmgr.launch_job (script, True)
|
||||||
self.sendServerMessage('script_launched', {'op': 'launched', 'job_id': job_id})
|
self.sendServerMessage('script_launched', {'op': 'launched', 'job_id': job_id})
|
||||||
|
|
||||||
|
# Monitor the job in the background and notify the server when done
|
||||||
|
def monitor_job():
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
|
||||||
|
job = self.jobmgr.jobs.get(job_id)
|
||||||
|
if not job:
|
||||||
|
return
|
||||||
|
|
||||||
|
p = job.get('p')
|
||||||
|
if not p:
|
||||||
|
return
|
||||||
|
|
||||||
|
p.wait()
|
||||||
|
# Wait for stdout/stderr threads to finish
|
||||||
|
if job.get('t1'): job['t1'].join()
|
||||||
|
if job.get('t2'): job['t2'].join()
|
||||||
|
|
||||||
|
logger.debug(f'Client job {job_id} finished, notifying server.')
|
||||||
|
self.sendServerMessage('script_finished', {
|
||||||
|
'job_id': job_id,
|
||||||
|
'rc': p.returncode,
|
||||||
|
'stderr': job.get('stderr', ''),
|
||||||
|
'stdout': job.get('stdout', '')
|
||||||
|
})
|
||||||
|
|
||||||
|
import threading
|
||||||
|
threading.Thread(target=monitor_job, daemon=True).start()
|
||||||
|
|
||||||
def process_terminatescript(self, json_params):
|
def process_terminatescript(self, json_params):
|
||||||
job_id = json_params['job_id']
|
job_id = json_params['job_id']
|
||||||
logger.debug('Processing terminatescript request, job_id "{}"'.format (job_id))
|
logger.debug('Processing terminatescript request, job_id "{}"'.format (job_id))
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import base64
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import shutil
|
import shutil
|
||||||
|
import socket
|
||||||
import string
|
import string
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
|
@ -102,6 +103,7 @@ class OpenGnSysWorker(ServerWorker):
|
||||||
exec_level = None # Execution level (permitted operations)
|
exec_level = None # Execution level (permitted operations)
|
||||||
jobmgr = JobMgr()
|
jobmgr = JobMgr()
|
||||||
_registered = False # Whether ogcore has acknowledged our ogagent/started
|
_registered = False # Whether ogcore has acknowledged our ogagent/started
|
||||||
|
client_jobs = set() # Track active client job IDs for busy status
|
||||||
|
|
||||||
def _send_started(self):
|
def _send_started(self):
|
||||||
"""Send ogagent/started to ogcore and mark as registered on success."""
|
"""Send ogagent/started to ogcore and mark as registered on success."""
|
||||||
|
|
@ -125,13 +127,40 @@ class OpenGnSysWorker(ServerWorker):
|
||||||
while True:
|
while True:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
n += 1
|
n += 1
|
||||||
|
|
||||||
|
# Check for finished jobs to send webhook
|
||||||
|
for k, elem in list(self.jobmgr.jobs.items()):
|
||||||
|
if elem.get('p') and elem['p'].poll() is not None:
|
||||||
|
if not elem.get('notified', False):
|
||||||
|
if not elem.get('t1').is_alive() and not elem.get('t2').is_alive():
|
||||||
|
elem['notified'] = True
|
||||||
|
elem['status'] = 'finished'
|
||||||
|
elem['rc'] = elem['p'].returncode
|
||||||
|
|
||||||
|
stdout = elem.get('stdout', '')
|
||||||
|
stderr = elem.get('stderr', '')
|
||||||
|
der = stdout + ('\n' if stdout and stderr else '') + stderr
|
||||||
|
webhook_body = {
|
||||||
|
"nfn": "RESPUESTA_EjecutarScript",
|
||||||
|
"job_id": k,
|
||||||
|
"res": 1 if elem['rc'] == 0 else 0,
|
||||||
|
"der": der
|
||||||
|
}
|
||||||
|
logger.info(f"Job {k} finished with return code {elem['rc']}. Sending webhook to ogcore.")
|
||||||
|
try:
|
||||||
|
self.REST.sendMessage('clients/status/webhook', webhook_body)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to send webhook for job {k}: {e}")
|
||||||
|
|
||||||
if not n % 10:
|
if not n % 10:
|
||||||
_st_map = {'linux': 'LNX', 'macos': 'OSX', 'windows': 'WIN'}
|
_st_map = {'linux': 'LNX', 'macos': 'OSX', 'windows': 'WIN'}
|
||||||
|
is_busy = any(j['p'].poll() is None for j in self.jobmgr.jobs.values()) or len(self.client_jobs) > 0
|
||||||
body = {
|
body = {
|
||||||
"iph": self.interface.ip,
|
"iph": self.interface.ip,
|
||||||
"timestamp": int(time.time()),
|
"timestamp": int(time.time()),
|
||||||
"busy": any(j['p'].poll() is None for j in self.jobmgr.jobs.values()),
|
"busy": is_busy,
|
||||||
"st": _st_map.get(operations.os_type.lower(), 'UNK'),
|
"st": _st_map.get(operations.os_type.lower(), 'UNK'),
|
||||||
|
"loggedin": len(self.user) > 0,
|
||||||
}
|
}
|
||||||
result = self.REST.sendMessage('clients/status/webhook', body)
|
result = self.REST.sendMessage('clients/status/webhook', body)
|
||||||
if result is None:
|
if result is None:
|
||||||
|
|
@ -142,6 +171,36 @@ class OpenGnSysWorker(ServerWorker):
|
||||||
logger.info('Connection to ogcore restored, re-registering')
|
logger.info('Connection to ogcore restored, re-registering')
|
||||||
self._send_started()
|
self._send_started()
|
||||||
|
|
||||||
|
def _best_interface(self, nets):
|
||||||
|
"""
|
||||||
|
Return the interface whose IP the OS routing table would use to reach
|
||||||
|
ogcore, by opening a no-op UDP socket (no packets sent) and reading
|
||||||
|
the local address assigned by the kernel.
|
||||||
|
|
||||||
|
Returns None if the routing query itself fails (network not ready yet),
|
||||||
|
so the caller keeps retrying. Falls back to nets[0] only when the query
|
||||||
|
succeeds but no interface matches the routing IP (e.g. NAT or VPN).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
host = urllib.parse.urlparse(self.REST.endpoint).hostname
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||||
|
s.connect((host, 80))
|
||||||
|
local_ip = s.getsockname()[0]
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug('Routing query to ogcore failed (network not ready?): {}'.format(e))
|
||||||
|
return None
|
||||||
|
|
||||||
|
for iface in nets:
|
||||||
|
if iface.ip == local_ip:
|
||||||
|
logger.debug('Selected interface {} ({}) for ogcore at {}'.format(
|
||||||
|
iface.name, local_ip, host))
|
||||||
|
return iface
|
||||||
|
|
||||||
|
# Routing succeeded but no interface matched (NAT/VPN): best effort
|
||||||
|
logger.debug('No interface matched routing IP {}, falling back to {}'.format(
|
||||||
|
local_ip, nets[0].ip))
|
||||||
|
return nets[0]
|
||||||
|
|
||||||
def _setup_network(self, ca_file, crt_file, key_file):
|
def _setup_network(self, ca_file, crt_file, key_file):
|
||||||
"""
|
"""
|
||||||
Background thread: waits indefinitely for a network interface, then
|
Background thread: waits indefinitely for a network interface, then
|
||||||
|
|
@ -159,8 +218,8 @@ class OpenGnSysWorker(ServerWorker):
|
||||||
try:
|
try:
|
||||||
nets = list(operations.getNetworkInfo())
|
nets = list(operations.getNetworkInfo())
|
||||||
if nets:
|
if nets:
|
||||||
self.interface = nets[0]
|
self.interface = self._best_interface(nets)
|
||||||
if t > 0:
|
if self.interface and t > 0:
|
||||||
logger.debug("Network interface found after {} seconds".format(t))
|
logger.debug("Network interface found after {} seconds".format(t))
|
||||||
else:
|
else:
|
||||||
logger.error('No network interfaces found')
|
logger.error('No network interfaces found')
|
||||||
|
|
@ -441,11 +500,38 @@ class OpenGnSysWorker(ServerWorker):
|
||||||
|
|
||||||
def process_client_script_launched(self, data):
|
def process_client_script_launched(self, data):
|
||||||
import tempfile
|
import tempfile
|
||||||
|
job_id = data['job_id']
|
||||||
job_id_path = os.path.join(tempfile.gettempdir(), 'EjecutarScript-jobid')
|
job_id_path = os.path.join(tempfile.gettempdir(), 'EjecutarScript-jobid')
|
||||||
with open (job_id_path, 'w') as fd:
|
with open (job_id_path, 'w') as fd:
|
||||||
fd.write (data['job_id'])
|
fd.write (job_id)
|
||||||
|
self.client_jobs.add(job_id)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def process_client_script_finished(self, data):
|
||||||
|
job_id = data['job_id']
|
||||||
|
rc = data['rc']
|
||||||
|
stdout = data.get('stdout', '')
|
||||||
|
stderr = data.get('stderr', '')
|
||||||
|
der = stdout + ('\n' if stdout and stderr else '') + stderr
|
||||||
|
|
||||||
|
if job_id in self.client_jobs:
|
||||||
|
self.client_jobs.remove(job_id)
|
||||||
|
|
||||||
|
logger.info(f"Client job {job_id} finished with return code {rc}. Sending webhook to ogcore.")
|
||||||
|
webhook_body = {
|
||||||
|
"nfn": "RESPUESTA_EjecutarScript",
|
||||||
|
"job_id": job_id,
|
||||||
|
"res": 1 if rc == 0 else 0,
|
||||||
|
"der": der
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
self.REST.sendMessage('clients/status/webhook', webhook_body)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to send webhook for client job {job_id}: {e}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@execution_level('full')
|
@execution_level('full')
|
||||||
@check_secret
|
@check_secret
|
||||||
def process_terminatescript(self, path, get_params, post_params, server):
|
def process_terminatescript(self, path, get_params, post_params, server):
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@
|
||||||
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import six
|
|
||||||
|
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
_fromEncoding = 'windows-1250'
|
_fromEncoding = 'windows-1250'
|
||||||
|
|
@ -42,15 +41,15 @@ else:
|
||||||
|
|
||||||
def toUnicode(msg):
|
def toUnicode(msg):
|
||||||
try:
|
try:
|
||||||
if not isinstance(msg, six.text_type):
|
if not isinstance(msg, str):
|
||||||
if isinstance(msg, six.binary_type):
|
if isinstance(msg, bytes):
|
||||||
return msg.decode(_fromEncoding, 'ignore')
|
return msg.decode(_fromEncoding, 'ignore')
|
||||||
return six.text_type(msg)
|
return str(msg)
|
||||||
else:
|
else:
|
||||||
return msg
|
return msg
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
return six.text_type(msg)
|
return str(msg)
|
||||||
except Exception:
|
except Exception:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,8 @@ def get_etc_path():
|
||||||
|
|
||||||
def build_popen_args(script):
|
def build_popen_args(script):
|
||||||
## turn the script into utf16le, then to b64 again, and feed the blob to powershell
|
## turn the script into utf16le, then to b64 again, and feed the blob to powershell
|
||||||
|
## wrap the script to redirect stderr and output as a simple string to avoid CLIXML wrappers in non-interactive errors
|
||||||
|
script = f"$ProgressPreference = 'SilentlyContinue'; try {{ $errBefore = $Error.Count; $output = & {{ {script} }} 2>&1; $errAfter = $Error.Count; $exitcode = $LASTEXITCODE; $output | Out-String; if ($exitcode) {{ exit $exitcode }} elseif ($errAfter -gt $errBefore) {{ exit 1 }} }} catch {{ $_ | Out-String; exit 1 }}"
|
||||||
u16 = script.encode ('utf-16le') ## utf16
|
u16 = script.encode ('utf-16le') ## utf16
|
||||||
b64 = base64.b64encode (u16).decode ('utf-8') ## b64 (which returns bytes, so we need an additional decode(utf8))
|
b64 = base64.b64encode (u16).decode ('utf-8') ## b64 (which returns bytes, so we need an additional decode(utf8))
|
||||||
return ['powershell', '-WindowStyle', 'Hidden', '-EncodedCommand', b64]
|
return ['powershell', '-WindowStyle', 'Hidden', '-EncodedCommand', b64]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
pywin32
|
pywin32
|
||||||
pyqt6
|
pyqt6
|
||||||
requests
|
requests
|
||||||
six
|
|
||||||
pycryptodome
|
pycryptodome
|
||||||
pyinstaller
|
pyinstaller
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue