Commit e8f8a3e8 by Szeberényi Imre

update FIX

parent 566e5f53
...@@ -22,20 +22,21 @@ VM kontextualizálását végzi. A felhő menedzserrel egy virtuális soros vona ...@@ -22,20 +22,21 @@ VM kontextualizálását végzi. A felhő menedzserrel egy virtuális soros vona
``` ```
sudo -i sudo -i
cd /root cd /root
chmod oug+x . # fontos a pont!
mkvirtualenv agent mkvirtualenv agent
workon agent workon agent
git clone https://git.ik.bme.hu/CIRCLE3/agent.git git clone https://git.ik.bme.hu/CIRCLE3/agent.git
chmod oug+rx agent
cd agent cd agent
python agent.py --install python agent.py --install
``` ```
Ez utóbbi parancs Ez utóbbi parancs
- bemásolja az agent.service-t /etc/systemd/system -be, engedélyezi, de nem indítja el - bemásolja az agent.service-t /etc/systemd/system -be, engedélyezi, de nem indítja el
- bemásolja a vm_renewal-t a /usr/bin/ -be - bemásolja a vm_renewal-t a /usr/bin/ -be
* Célszerűen a cloud usernek van sudo joga jelszó nélkül. * Célszerűen a cloud usernek van sudo joga jelszó nélkül.
Ha nincs, akkor a /root/.virtualenv/agent és /root/agent katalógusoknak kereshetőknek kell lenniük, és a benne levő Ha nincs, akkor a /root/.virtualenv/agent és /root/agent katalógusoknak kereshetőknek kell lenniük, és a benne levő
fájloknak olvashatóknak kell lennie a cloud user számára, hogy a vm_reneval működjön. fájloknak olvashatóknak kell lennie a cloud user számára, hogy a vm_reneval működjön.
Ekkor a wm_renewal-bol kivehető a sudo. (Esetleg spec sudo engedéllyel is megoldhato a dolog) Ekkor a wm_renewal-bol kivehető a sudo. (Esetleg spec sudo engedéllyel is megoldhato a dolog)
## Windows ## ## Windows ##
* Bundled python alkalmazások, melyekből az első kettő szervízként fut, a harmadik a cloud user belépésekor indul * Bundled python alkalmazások, melyekből az első kettő szervízként fut, a harmadik a cloud user belépésekor indul
......
...@@ -12,10 +12,12 @@ import win32service ...@@ -12,10 +12,12 @@ import win32service
import win32serviceutil import win32serviceutil
from utils import setup_logging from utils import setup_logging
from windows.winutils import getRegistryVal, get_windows_version, servicePostUpdate from windows.winutils import getRegistryVal, get_windows_version, update_component
workdir = r"C:\circle"
if getattr(sys, "frozen", False): if getattr(sys, "frozen", False):
logger = setup_logging(logfile=r"C:\Circle\watchdog.log") logger = setup_logging(logfile=workdir + r"\watchdog.log")
else: else:
logger = setup_logging() logger = setup_logging()
fh = NTEventLogHandler("CIRCLE Watchdog") fh = NTEventLogHandler("CIRCLE Watchdog")
...@@ -24,7 +26,7 @@ formatter = logging.Formatter( ...@@ -24,7 +26,7 @@ formatter = logging.Formatter(
fh.setFormatter(formatter) fh.setFormatter(formatter)
logger.addHandler(fh) logger.addHandler(fh)
level = getRegistryVal( level = getRegistryVal(
r"SYSTEM\\CurrentControlSet\\Services\\CIRCLE-Agent\\Parameters", r"SYSTEM\\CurrentControlSet\\Services\\CIRCLE-Watchdog\\Parameters",
"LogLevel", "LogLevel",
"INFO" "INFO"
) )
...@@ -55,7 +57,7 @@ class AppServerSvc (win32serviceutil.ServiceFramework): ...@@ -55,7 +57,7 @@ class AppServerSvc (win32serviceutil.ServiceFramework):
timo = timo_base timo = timo_base
sleep(6*timo) # boot process may have triggered the agent, so we are patient sleep(6*timo) # boot process may have triggered the agent, so we are patient
while not self._stopped: while not self._stopped:
logger.debug("checking....(timo: %d", timo) logger.debug("checking....(timo: %d)", timo)
if not check_service(checked_service): if not check_service(checked_service):
logger.info("Service %s is not running.", checked_service) logger.info("Service %s is not running.", checked_service)
try: try:
...@@ -82,7 +84,7 @@ class AppServerSvc (win32serviceutil.ServiceFramework): ...@@ -82,7 +84,7 @@ class AppServerSvc (win32serviceutil.ServiceFramework):
exe = "circle-watchdog.exe" exe = "circle-watchdog.exe"
exe_path = join(working_dir, exe) exe_path = join(working_dir, exe)
logger.debug("hahooo %s %s", self._svc_name_, exe_path) logger.debug("hahooo %s %s", self._svc_name_, exe_path)
if servicePostUpdate(self._svc_name_, exe_path): if update_component(self._svc_name_ + ".state", workdir) == "exit":
# Service updated, Restart needed # Service updated, Restart needed
logger.debug("update....") logger.debug("update....")
self.ReportServiceStatus( self.ReportServiceStatus(
......
{
"img": "circle-watchdog.exe",
"img_pending": "agent-wdog-winservice.exe",
"service": "circle-watchdog",
"state": "idle",
"last_checked": "2026-01-15T18:08:08.331288+00:00Z",
"status": "idle"
}
\ No newline at end of file
pyinstaller --clean -F --path . --hidden-import pkg_resources --hidden-import infi --hidden-import win32timezone --hidden-import win32traceutil -F agent-wdog-winservice.py pyinstaller --clean -F --path . --hidden-import pkg_resources --hidden-import infi --hidden-import win32timezone --hidden-import win32traceutil -F agent-wdog-winservice.py
pyinstaller --clean -F --path . --hidden-import pkg_resources --hidden-import infi --hidden-import win32timezone --hidden-import win32traceutil -F agent-winservice.py pyinstaller --clean -F --path . --hidden-import pkg_resources --hidden-import infi --hidden-import win32timezone --hidden-import win32traceutil -F agent-winservice.py
pyinstaller --clean -F --path . --hidden-import pkg_resources --hidden-import infi --hidden-import win32timezone --hidden-import win32traceutil -F circle-notify.pyw pyinstaller --clean -F --path . --hidden-import pkg_resources --hidden-import infi --hidden-import win32timezone --hidden-import win32traceutil -F agent-notify.pyw
\ No newline at end of file \ No newline at end of file
...@@ -20,7 +20,8 @@ from .network import change_ip_windows ...@@ -20,7 +20,8 @@ from .network import change_ip_windows
from context import BaseContext from context import BaseContext
from windows.winutils import ( from windows.winutils import (
is_frozen_exe, copy_running_exe, is_frozen_exe, copy_running_exe,
update_service_binpath, servicePostUpdate update_service_binpath, servicePostUpdate,
run_with_powershell
) )
try: try:
...@@ -34,12 +35,13 @@ logger = logging.getLogger(__name__) ...@@ -34,12 +35,13 @@ logger = logging.getLogger(__name__)
class Context(BaseContext): class Context(BaseContext):
service_name = "CIRCLE-agent" service_name = "CIRCLE-agent"
working_dir = r"C:\circle" workdir = r"C:\circle"
update_cmd = "update.ps1"
exe = "circle-agent.exe" exe = "circle-agent.exe"
@staticmethod @staticmethod
def postUpdate(): def postUpdate():
exe_path = join(Context.working_dir, Context.exe) exe_path = join(Context.workdir, Context.exe)
return servicePostUpdate(Context.service_name, exe_path) return servicePostUpdate(Context.service_name, exe_path)
@staticmethod @staticmethod
...@@ -119,15 +121,24 @@ class Context(BaseContext): ...@@ -119,15 +121,24 @@ class Context(BaseContext):
raise Exception("Checksum missmatch the file is damaged.") raise Exception("Checksum missmatch the file is damaged.")
decoded = BytesIO(b64decode(data)) decoded = BytesIO(b64decode(data))
try: try:
tar = tarfile.TarFile.open("dummy", fileobj=decoded, mode='r|gz') tar = tarfile.TarFile.open("dummy", fileobj=decoded, mode='r:gz')
tar.extractall(Context.working_dir) tar.extractall(Context.workdir)
logger.debug("%s file extracted", filename)
except tarfile.ReadError as e: except tarfile.ReadError as e:
logger.error(e) logger.error(e)
return
logger.info("Transfer completed!") logger.info("Transfer completed!")
old_exe = update_service_binpath("CIRCLE-agent", join(Context.working_dir, executable)) if executable.startswith("0000_"):
logger.info('%s Updated', old_exe) logger.debug("starting %s in %s", executable, Context.workdir)
Context.exit_code = 1 start_process_async(executable, workdir=Context.workdir)
reactor.callLater(0, reactor.stop) else:
old_exe = update_service_binpath("CIRCLE-agent", join(Context.workdir, executable))
logger.info('%s Updated', old_exe)
if os.path.exists(join(Context.workdir, Context.update_cmd)):
logger.debug("starting %s in %s", Context.update_cmd, Context.workdir)
start_process_async(Context.update_cmd, workdir=Context.workdir, delay_seconds=60)
Context.exit_code = 1
reactor.callLater(0, reactor.stop)
@staticmethod @staticmethod
def ipaddresses(): def ipaddresses():
...@@ -148,7 +159,7 @@ class Context(BaseContext): ...@@ -148,7 +159,7 @@ class Context(BaseContext):
@staticmethod @staticmethod
def get_agent_version(): def get_agent_version():
try: try:
with open(join(Context.working_dir, 'version.txt')) as f: with open(join(Context.workdir, 'version.txt')) as f:
return f.readline() return f.readline()
except IOError: except IOError:
return None return None
import os import os
import sys import sys
import logging import logging
import subprocess
import json
import tempfile
from datetime import datetime, timezone
from shutil import copy from shutil import copy
from os.path import join, normcase, normpath from os.path import join, normcase, normpath
...@@ -9,7 +13,7 @@ from winreg import ( ...@@ -9,7 +13,7 @@ from winreg import (
HKEY_LOCAL_MACHINE, KEY_ALL_ACCESS, KEY_READ HKEY_LOCAL_MACHINE, KEY_ALL_ACCESS, KEY_READ
) )
logger = logging.getLogger() logger = logging.getLogger(__name__)
def is_frozen_exe() -> bool: def is_frozen_exe() -> bool:
return bool(getattr(sys, "frozen", False)) return bool(getattr(sys, "frozen", False))
...@@ -33,7 +37,7 @@ def update_service_binpath(service_name: str, exe_path: str) -> str: ...@@ -33,7 +37,7 @@ def update_service_binpath(service_name: str, exe_path: str) -> str:
def copy_running_exe(dest: str) -> bool: def copy_running_exe(dest: str) -> bool:
""" """
Startup helper: Startup helper:
- If the runnin executable is not - If the running executable image is not the dest
then copy it to dest (overwriting old dest if present), then copy it to dest (overwriting old dest if present),
- Otherwise do nothing. - Otherwise do nothing.
...@@ -74,11 +78,233 @@ def getRegistryVal(reg_path: str, name: str, default=None): ...@@ -74,11 +78,233 @@ def getRegistryVal(reg_path: str, name: str, default=None):
with OpenKeyEx(HKEY_LOCAL_MACHINE, reg_path, 0, KEY_READ) as key: with OpenKeyEx(HKEY_LOCAL_MACHINE, reg_path, 0, KEY_READ) as key:
value, _ = QueryValueEx(key, name) value, _ = QueryValueEx(key, name)
except Exception as e: except Exception as e:
logging.debug("Registry read failed %s\\%s: %s", logger.debug("Registry read failed %s\\%s: %s",
reg_path, name, e reg_path, name, e
) )
return value return value
def start_process_async(path, args=None, workdir=None, delay_seconds=0):
"""
Starts a process asynchronously (fire-and-forget) using cmd.exe.
Parameters:
path (str):
Supported types:
- .exe, .bat, .cmd → started directly by cmd.exe
- .ps1 → started via powershell.exe (-File)
args Command-line arguments passed to the target process.
Arguments are appended as-is; quoting is the caller's responsibility.
workdir Working directory for the started process.
delay_seconds
Notes:
- The function does not wait for the process to finish.
- No stdout/stderr is captured; all output is discarded.
"""
if args is None:
args = []
ext = os.path.splitext(path)[1].lower()
if ext == ".ps1":
cmdline = f'powershell -NoProfile -ExecutionPolicy Bypass -File "{path}"'
if args:
cmdline += " " + " ".join(args)
else:
cmdline = f'"{path}"'
if args:
cmdline += " " + " ".join(args)
parts = []
if workdir:
parts.append(f'cd /d "{workdir}"')
parts.append(f'timeout /t {int(delay_seconds)} /nobreak >nul')
parts.append(cmdline)
cmd = " & ".join(parts)
try:
subprocess.Popen(
["cmd.exe", "/c", cmd],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
except Exception:
logger.exception(f"Execution failed for: {path} (workdir={workdir})")
def file_is_newer(file_a, file_b):
"""
Returns True if file_a is newer than file_b.
Raises FileNotFoundError if any file does not exist.
"""
if not os.path.exists(file_a):
raise FileNotFoundError(file_a)
if not os.path.exists(file_b):
raise FileNotFoundError(file_b)
return os.path.getmtime(file_a) > os.path.getmtime(file_b)
def start_delayed_process(command, delay_seconds, workdir=None):
"""
Starts a delayed process asynchronously using cmd.exe (no threads).
"""
if isinstance(command, list):
exe = command[0]
args = " ".join(command[1:])
else:
exe = command
args = ""
cmd = (
f'timeout /t {int(delay_seconds)} /nobreak >nul '
f'& "{exe}" {args}'
)
subprocess.Popen(
["cmd.exe", "/c", cmd],
cwd=workdir,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
def load_json(path, default=None):
"""
Loads a JSON file and returns a dict.
If file does not exist, returns default (or empty dict).
"""
if not os.path.exists(path):
return default if default is not None else {}
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def save_json(path, data):
"""
Saves dict to JSON file atomically.
"""
directory = os.path.dirname(path)
if directory:
os.makedirs(directory, exist_ok=True)
fd, temp_path = tempfile.mkstemp(
dir=directory,
prefix=".tmp_",
suffix=".json"
)
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
f.flush()
os.fsync(f.fileno())
os.replace(temp_path, path)
finally:
if os.path.exists(temp_path):
try:
os.remove(temp_path)
except OSError:
pass
import os
import sys
import shutil
def update_component(state_file: str, base_dir: str) -> str:
"""
Component self-update state handler.
Uses a per-component JSON state file to coordinate a two-phase update:
1) If a newer pending image is detected, transitions from IDLE → PENDING
and schedules the pending image to start (or updates the service binpath).
2) When running from the pending image, copies it over the standard image,
updates the state to COPIED, and schedules the standard image to restart.
The function is restart-safe and supports both service-managed components
and user-mode processes. The running image is identified by comparing
sys.executable with the configured image names.
"""
state_file = os.path.join(base_dir, state_file)
state = load_json(state_file, default={})
state["last_checked"] = datetime.now(timezone.utc).isoformat() + "Z"
save_json(state_file, state)
img = state.get("img")
img_pending = state.get("img_pending")
status = (state.get("status") or "idle").lower()
service = state.get("service")
if not img or not img_pending:
raise ValueError("status json must contain 'img' and 'img_pending'")
img_path = os.path.join(base_dir, img)
img_pending_path = os.path.join(base_dir, img_pending)
# Determine what THIS running process is (by basename)
self_image = os.path.basename(sys.executable)
is_self_img = self_image.lower() == os.path.basename(img).lower()
is_self_img_pending = self_image.lower() == os.path.basename(img_pending).lower()
logger.debug("executable: %s is_self: %s is_self_pending: %s", self_image, is_self_img, is_self_img_pending)
# Compare mtimes (img_pending newer than img?)
try:
newer = file_is_newer(img_pending_path, img_path)
except FileNotFoundError:
newer = False
logger.debug("One of teh img is not found: %s %s", img_path, img_pending_path)
if newer:
logger.debug("newer: %s", status);
if status == "idle" and is_self_img_pending:
status = "pending" # img_pending started first
if status == "idle":
state["status"] = "pending"
save_json(state_file, state)
if service:
# set pending image as service image
update_service_binpath(service, img_pending_path)
return "exit"
else:
# start pending image after 60 sec, then the caller should exit
logger.debug("Start porcess: %s", imp_pending)
start_delayed_process(img_pending, workdir=base_dir, delay_seconds=60)
return "set_pending_and_started_img_pending"
if status == "pending" and is_self_img_pending:
# we are running as img_pending -> safe to overwrite img (img is not running)
shutil.copy2(img_pending_path, img_path)
logger.debug("Copy: %s ---> %s", img_pending_path, img_path)
state["status"] = "copied"
save_json(state_file, state)
if service:
# set standard image as service image
update_service_binpath(service, img_path)
return "exit"
else:
# start standard image after 60 sec
logger.debug("Start porcess: %s", imp)
start_delayed_process(img, workdir=base_dir, delay_seconds=60)
return "copied_img_pending_to_img_and_started_img"
return "newer_no_action"
else:
if status != "idle":
state["status"] = "idle"
save_json(state_file, state)
return "reset_to_idle"
return "idle_no_change"
def get_windows_version(): def get_windows_version():
if sys.platform != "win32": if sys.platform != "win32":
return None return None
...@@ -106,3 +332,10 @@ def get_windows_version(): ...@@ -106,3 +332,10 @@ def get_windows_version():
return f"Windows_{major}_{minor}_{build})" return f"Windows_{major}_{minor}_{build})"
if __name__ == '__main__':
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s %(name)s %(levelname)s %(message)s"
)
print(update_component("circle-watchdog.state", r"c:\circle"))
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment