Compare commits

...

35 Commits

Author SHA1 Message Date
OpenGnSys Support Team cf9577a40e fs: call wipefs partition before formatting partition
call wipefs before formatting partition, to remove any labels and stale data.

call wipefs if formatting fails, to leave partition in consistent state.

moreover, remove unnecessary exception handling on get_partition_device().
2025-02-14 15:48:53 +01:00
OpenGnSys Support Team 6503d0ffe7 live: call wipefs if partition or format fails
wipe disk if partition or format fails to leave the disk in consistent state.
2025-02-14 15:48:13 +01:00
Alejandro Sirgo Rica 0ca16bc46c live: remove unused variable in cache_delete()
Remove unused variable delted_images in the function cache_delete()
2025-02-14 13:15:27 +01:00
Alejandro Sirgo Rica bd190f8d44 live: send refresh payload in image restore
Send the refresh payload after a completed image restore operation.

The fields sent to ogServer are not enough to update the status of
the client and the OS installed in a partition does not appear in
the database.
2025-02-13 12:40:53 +01:00
Alejandro Sirgo Rica e0ba9cc98c uefi: log efibootmgr errors as warnings
Log as warning when efibootmgr fails to update the NVRAM.

Raise an exception when the command is not available or when
there are not enough permissions to execute. Provide contextual
information in the error message.
2025-02-13 09:06:38 +01:00
Alejandro Sirgo Rica ccdcb7bfc7 uefi: add missing f-string prefix in _find_bootentry() error
Add missing f-string prefix for proper string interpolation in
the error message of _find_bootentry()
2025-02-13 09:06:25 +01:00
Alejandro Sirgo Rica 72406a7d89 live: configure OS on first disk only
Call configure_os only for first disk, any other disks are only
supported to restore data images by now.

Call os_probe only for disk 1 in refresh.
2025-02-11 11:06:29 +01:00
Alejandro Sirgo Rica 0f519ecfeb grub: move disabled boot entries at the end of boot order
The GRUB entry is always set as the second boot option, assuming
PXE is first. This is not always true, as disabled entries before
PXE IPv4 can make it the first valid but not the first defined
entry in the boot order.

Move every disabled boot entry at the end of boot order.
2025-02-05 09:42:38 +01:00
Alejandro Sirgo Rica c260534534 live: remove unneeded string formatting in image_create
Replace f-string wf'{image_path}' with the variable image_path.
2025-02-03 11:27:18 +01:00
Alejandro Sirgo Rica f67f3c598a rest: register image/update as a valid request
Accept both image/create and image/update requests from ogServer.
2025-01-31 14:31:17 +01:00
OpenGnSys Support Team 574822907d grub: skip OS guess if partition cannot be mounted
otherwise, _get_os_entries() fails when it finds a swap partition:

  (2025-01-23 17:44:30) ogClient: [ERROR] - Error generating /mnt/nvme0n1p4/EFI/grub/Boot/grub.cfg: Unable to mount /dev/nvme0n1p3 into /mnt/nvme0n1p3
2025-01-24 15:15:57 +01:00
Alejandro Sirgo Rica 30e0e1dca3 src: run session check thread if the event socket is available
Disable session check thread if the event socket is not in an
initialized state.
2025-01-24 15:15:57 +01:00
OpenGnSys Support Team 59d642f6b5 ogclient: rename event handler
no functional changes intended.
2025-01-15 11:41:41 +01:00
Alejandro Sirgo Rica 24568356bc winreg: move disk id functions into disk.py
Move uuid_to_bytes, get_disk_id_bytes and get_part_id_bytes from
winreg.py to the more fitting location disk.py
2025-01-07 15:56:55 +01:00
Alejandro Sirgo Rica 40e4545bb7 live: fix image restore backtrace
import the missing function is_hibernation_enabled() into
ogOperations.py to prevent a backtrace in each restore operation.
2025-01-07 15:02:10 +01:00
Alejandro Sirgo Rica d29b601f17 uefi: remove dependency with probe.py
Reduce interdependency between imports by checking the correct OS for
copy_windows_efi_bootloader() from the code invoking the operation.

Break circular dependency where:
probe.py imports from winreg.py
winreg.py imports from uefi.py
uefi.py imports from probe.py
2025-01-07 15:02:04 +01:00
Alejandro Sirgo Rica 476d82e6a9 ogclient-systray: add new systray program for ogclient
Add make.bat for an easier building process in Windows. This
script generates ogclient.exe and ogclient-systray binaries in
a ./dist directory.

Add ogclient-systray program. This python program polls the
existence of the ogclient process and shows a systray if the
ogclient service is active.

Update utils/create_version_file.py to generate information for
the systray binary.
2024-12-16 16:01:06 +01:00
Alejandro Sirgo Rica e91f6c5e2c windows: remove dead systray code
Remove dead systray code and unused imports in ogclient for
Windows.
2024-12-16 15:08:22 +01:00
Alejandro Sirgo Rica 4465c6a25a ogclient: remove session event cmd commands
Remove commands to send session events as the session report
logic is now implemented in the ogOperations.py file of each
platform.
2024-12-12 13:14:48 +01:00
Alejandro Sirgo Rica 203f3e5533 live: add additional error checks for shell run
Add checks for invalid arguments and permission errors.
2024-12-12 11:20:23 +01:00
Alejandro Sirgo Rica bf15491435 hw_inventory: fix json parsing
Add support for both lshw -json return formats.
The json structure may follow one of the following.

output:list flag enabled:
[{content}]

output:list flag disabled:
{content}

The output:list flag was defined in the commit 2b1c730 of
https://ezix.org/src/pkg/lshw
2024-12-11 15:17:02 +01:00
Alejandro Sirgo Rica 855768e144 src: refactor windows hive code
Remove usage of hivexget as a subprocess and use Python hivex to
inspect the Windows Registry.

Use registry path constants defined in src.utils.winreg

Remove windows_is64bit() funcion as the code to identify the
architecture relies on a broken Registry query. Fixing the query
proved to be a challenge and the only implication is the removal
of the string "64 bits" at the end of the listed Windows OS
installed in each partition.

Use utility function in src.utils.winreg to make the software
inventory code more compact.

Rewrite onliner in _fill_package_set function and parse the
registry with a for loop.
2024-12-11 15:16:34 +01:00
Alejandro Sirgo Rica aa570e66e6 log: use INFO as default logging mode for Windows 2024-11-28 16:58:41 +01:00
Alejandro Sirgo Rica ffaf2aac05 install: add script to generate the windows metadata information file
Add create_version_file.py, running this script creates a file
version_info.txt with the data required for the Windows ogClient
binary metadata.
2024-11-28 16:58:41 +01:00
OpenGnSys Support Team 62b52ff364 src: update license header 2024-11-28 16:45:56 +01:00
Alejandro Sirgo Rica e4be5c34eb src: add support for direct command execution
Update live shell run mode for the new REST API interface.
Evaluate the "inline" field to diferentiate between execution of
script in /opt/opengnsys/shell/ and a cmd execution.

Remove usage of echo argument of the API REST.

Update Windows and Linux mode for direct command execution.
Set OutputEncoding environment variable to 'utf-8' in Windows to
unify the encoding of stdout for the invoked programs.

Decode stdout to utf-8-sig to remove potential BOM.

While at this, remove strange legacy ;|\n\r terminator.
2024-11-27 13:53:19 +01:00
Alejandro Sirgo Rica a36c4daa23 src: add user session detection implementation
Detect user login and logout for Linux and Windows.

Report an active interactive session through the /refresh response
so a new ogserver instance can update the session status.

Poll the session change in 5 second intervals in a thread. Use the
same event socket previously used by the old session detection
mechanism to notify a session change.

Use the method check_interactive_session_change in each
ogOperations.py to report the session status.
Return values:
	None: no session changes are found
	True: login
	False: logout

Windows
Verify if psutil.users() has any value.

Linux
Verify all the psutil.users() asociated to a terminal.
2024-11-26 13:02:45 +01:00
Alejandro Sirgo Rica a1bd0c36f3 linux: remove leftover prove() method 2024-11-26 13:02:45 +01:00
Alejandro Sirgo Rica c8674a4e93 windows: bind the systray process lifetime to ogClient
Add daemon=True to the systray process in order to make it close
when the ogClient process closes.
2024-11-26 13:02:45 +01:00
Alejandro Sirgo Rica 179d17cae8 windows: make the systray reference local to OgWindowsOperations 2024-11-26 13:02:45 +01:00
Alejandro Sirgo Rica bc7fe848ac windows: use a better systray default icon
Draw a blue circle as default icon in the systray. Use the same
blue color as the one shown in ogCP for an ogClient Windows instance.
2024-11-26 13:02:45 +01:00
Alejandro Sirgo Rica 525958ae85 src: update functions for every ogClient mode
Add missing ogClient mode functions and show an error in every
unimplemented function.
2024-11-26 13:02:17 +01:00
Alejandro Sirgo Rica d7658f03ab utils: disable fcntl in ogClient for Windows
Add conditional import for fcntl as it is only supported for
Linux and causes ogClient for Windows fail to start.
2024-11-15 10:47:05 +01:00
OpenGnSys Support Team 90a9ba9543 live: bogus error in logs when connecting to ogserver
This error is bogus:

 (2024-11-14 09:05:37) ogClient: [ERROR] - Partition query error for /dev/sdb: No medium found

skip if device cannot be opened instead.
2024-11-14 18:12:56 +01:00
Alejandro Sirgo Rica f5f8771b6f grub: fix failed grub configuration when a device is not found
Fix "No medium found" error aborting the grub configuration
process. Just log it and continue.
2024-11-14 18:12:56 +01:00
42 changed files with 570 additions and 342 deletions

8
make.bat 100644
View File

@ -0,0 +1,8 @@
:: Create version_info.txt
python utils\create_version_file.py
:: Build the service binary with clean
pyinstaller --onefile --noconsole --version-file=ogclient-version-info.txt --clean ogclient
:: Build the systray binary with clean
pyinstaller --onefile --noconsole --version-file=systray-version-info.txt --clean systray\ogclient-systray

View File

@ -1,7 +1,7 @@
#!/usr/bin/python3
#
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
@ -24,27 +24,9 @@ from src.ogClient import *
from src.log import *
def send_event_dgram(msg, ip='127.0.0.1', port=55885):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(bytes(msg, "utf-8"), (ip, port))
def create_parser():
events = ['login', 'logout']
parser = argparse.ArgumentParser()
parser.set_defaults(func=None)
subparsers = parser.add_subparsers()
parser_event = subparsers.add_parser('event')
subparsers_event = parser_event.add_subparsers()
parser_event_login = subparsers_event.add_parser('login')
parser_event_login.set_defaults(func=lambda x: send_event_dgram(f'session start {x.user}'))
parser_event_login.add_argument('user', type=str)
parser_event_logout = subparsers_event.add_parser('logout')
parser_event_logout.set_defaults(func=lambda x: send_event_dgram(f'session stop {x.user}'))
parser_event_logout.add_argument('user', type=str)
parser.add_argument('-c', '--config', default="",
help='ogClient JSON config file path')
parser.add_argument('--debug', default=False,

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
@ -7,14 +7,18 @@
# (at your option) any later version.
import os
import shlex
import psutil
import subprocess
from subprocess import CalledProcessError
from src.log import OgError
from src.ogRest import ThreadState
class OgLinuxOperations:
def __init__(self):
self.session = False
def _restartBrowser(self, url):
raise OgError('Function not implemented')
@ -26,25 +30,30 @@ class OgLinuxOperations:
def shellrun(self, request, ogRest):
cmd = request.getrun()
is_inline = request.get_inline()
if not is_inline:
raise OgError("Only inline mode is supported on Linux")
try:
result = subprocess.run(cmd,
shell=True,
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
check=True)
except CalledProcessError as error:
if error.stderr:
return error.stderr
if error.stdout:
return error.stdout
return "{Non zero exit code and empty output}"
return result.stdout
ogRest.proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
shell=True)
(output, error) = ogRest.proc.communicate()
except (OSError, subprocess.SubprocessError) as e:
raise OgError(f'Error when running "shell run" subprocess: {e}') from e
output = output.decode('utf-8-sig', errors='replace')
return (ogRest.proc.returncode, cmd, output)
def session(self, request, ogRest):
raise OgError('Function not implemented')
def hardware(self, path, ogRest):
def software(self, request, ogRest):
raise OgError('Function not implemented')
def hardware(self, ogRest):
raise OgError('Function not implemented')
def setup(self, request, ogRest):
@ -53,7 +62,7 @@ class OgLinuxOperations:
def image_restore(self, request, ogRest):
raise OgError('Function not implemented')
def image_create(self, path, request, ogRest):
def image_create(self, request, ogRest):
raise OgError('Function not implemented')
def cache_delete(self, request, ogRest):
@ -63,7 +72,21 @@ class OgLinuxOperations:
raise OgError('Function not implemented')
def refresh(self, ogRest):
return {"status": "LINUX"}
if self.session:
session_value = 'LINUXS'
else:
session_value = 'LINUX'
return {"status": session_value}
def probe(self, ogRest):
return {'status': 'LINUX' if ogRest.state != ThreadState.BUSY else 'BSY'}
def check_interactive_session_change(self):
old_status = self.session
has_logged_user = False
for user in psutil.users():
if user.terminal:
has_logged_user = True
break
self.session = has_logged_user
if self.session != old_status:
return self.session
return None

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
@ -27,7 +27,7 @@ from src.utils.postinstall import configure_os
from src.utils.net import *
from src.utils.menu import generate_menu
from src.utils.fs import *
from src.utils.probe import os_probe, get_cache_dev_path
from src.utils.probe import *
from src.utils.disk import *
from src.utils.cache import *
from src.utils.tiptorrent import *
@ -38,8 +38,6 @@ from src.utils.hw_inventory import get_hardware_inventory, legacy_list_hardware_
from src.log import OgError
OG_SHELL = '/bin/bash'
class OgLiveOperations:
def __init__(self, config):
self._url = config['opengnsys']['url']
@ -84,8 +82,10 @@ class OgLiveOperations:
code = int(pa.parttype, base=16)
if mount_mkdir(pa.padev, target):
probe_result = os_probe(target)
part_setup['os'] = probe_result
part_setup['os'] = ''
if part_setup['disk'] == '1':
probe_result = os_probe(target)
part_setup['os'] = probe_result
total, used, free = shutil.disk_usage(target)
part_setup['used_size'] = used
@ -381,32 +381,46 @@ class OgLiveOperations:
def shellrun(self, request, ogRest):
cmd = request.getrun()
cmds = cmd.split(";|\n\r")
is_inline = request.get_inline()
if not cmd:
raise OgError("No command provided for shell run")
self._restartBrowser(self._url_log)
shell_path = '/opt/opengnsys/shell/'
if is_inline:
cmds = cmd
else:
cmds = shlex.split(cmd)
shell_path = '/opt/opengnsys/shell/'
restricted_mode = False
if not cmds:
raise OgError("Parsed shell command list is empty")
for file_name in os.listdir(shell_path):
file_path = os.path.join(shell_path, file_name)
try:
shell_path_files = os.listdir(shell_path)
except OSError as e:
raise OgError(f'Error accessing {shell_path}: {e}') from e
if cmds[0] == file_name:
cmds[0] = file_path
restricted_mode = True
break
for file_name in shell_path_files:
file_path = os.path.join(shell_path, file_name)
try:
if restricted_mode:
ogRest.proc = subprocess.Popen(cmds, stdout=subprocess.PIPE)
if cmds[0] == file_name:
cmds[0] = file_path
cmd = " ".join(cmds)
break
else:
ogRest.proc = subprocess.Popen(cmds,
stdout=subprocess.PIPE,
shell=True,
executable=OG_SHELL)
raise OgError(f'Script {cmds[0]} not found in {shell_path}')
if not os.path.isfile(cmds[0]) or not os.access(cmds[0], os.X_OK):
raise OgError(f"Command '{cmds[0]}' is not a valid executable")
try:
ogRest.proc = subprocess.Popen(
cmds,
stdout=subprocess.PIPE,
shell=is_inline)
(output, error) = ogRest.proc.communicate()
except OSError as e:
except (OSError, subprocess.SubprocessError) as e:
raise OgError(f'Error when running "shell run" subprocess: {e}') from e
if ogRest.proc.returncode != 0:
@ -416,7 +430,8 @@ class OgLiveOperations:
self.refresh(ogRest)
return (ogRest.proc.returncode, " ".join(cmds), output.decode('utf-8'))
output = output.decode('utf-8-sig', errors='replace')
return (ogRest.proc.returncode, cmd, output)
def session(self, request, ogRest):
disk = request.getDisk()
@ -513,7 +528,12 @@ class OgLiveOperations:
raise OgError(f'Invalid disk number {disk}, {len(get_disks())} disks available.')
diskname = get_disks()[disk-1]
self._partition(diskname, table_type, partlist)
try:
self._partition(diskname, table_type, partlist)
except Exception as e:
ret = subprocess.run(['wipefs', '-af', f'/dev/{diskname}'])
logging.warning(f'wipefs on /dev/{diskname} after failure for consistency, reports {ret.returncode}')
raise
ret = subprocess.run(['partprobe', f'/dev/{diskname}'])
logging.info(f'first partprobe /dev/{diskname} reports {ret.returncode}')
@ -581,19 +601,14 @@ class OgLiveOperations:
extend_filesystem(disk, partition)
configure_os(disk, partition)
if disk == 1:
configure_os(disk, partition)
self.refresh(ogRest)
result = self.refresh(ogRest)
logging.info('Image restore command OK')
json_dict = {
'disk': request.getDisk(),
'partition': request.getPartition(),
'image_id': request.getId(),
'cache': self._get_cache_contents(),
}
return json_dict
return result
def image_create(self, request, ogRest):
disk = int(request.getDisk())
@ -642,6 +657,7 @@ class OgLiveOperations:
raise OgError(f'Cannot mount {ro_mountpoint} as readonly')
try:
is_windows = get_os_family(ro_mountpoint) == OSFamily.WINDOWS
if is_hibernation_enabled(ro_mountpoint):
raise OgError(f'Target system in {padev} has hibernation enabled')
finally:
@ -653,10 +669,12 @@ class OgLiveOperations:
if os.access(f'/opt/opengnsys/images', os.R_OK | os.W_OK) == False:
raise OgError('Cannot access /opt/opengnsys/images in read and write mode, check permissions')
if os.access(f'{image_path}', os.R_OK) == True:
if os.access(image_path, os.R_OK) == True:
logging.info(f'image file {image_path} already exists, updating.')
copy_windows_efi_bootloader(disk, partition)
if is_windows and is_uefi_supported():
copy_windows_efi_bootloader(disk, partition)
if ogReduceFs(disk, partition) == -1:
raise OgError(f'Failed to shrink {fstype} filesystem in {padev}')
@ -728,7 +746,6 @@ class OgLiveOperations:
def cache_delete(self, request, ogRest):
images = request.getImages()
deleted_images = []
logging.info(f'Request to remove files from cache')
@ -814,7 +831,6 @@ class OgLiveOperations:
partitions = get_partition_data(device=f'/dev/{disk}')
disk_data = get_disk_data(device=f'/dev/{disk}')
except Exception as e:
logging.error(e)
continue
self._refresh_payload_disk(disk_data, part_setup, num_disk)
@ -838,3 +854,6 @@ class OgLiveOperations:
logging.info('Sending response to refresh request')
return json_body
def check_interactive_session_change(self):
return False

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2023 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2023 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
@ -98,7 +98,7 @@ def _default_logging_win():
},
'handlers': {
'console': {
'level': 'DEBUG',
'level': 'INFO',
'class': 'logging.StreamHandler',
'formatter': 'formatter.console',
'stream': 'ext://sys.stdout',
@ -107,7 +107,7 @@ def _default_logging_win():
'loggers': {
'': {
'handlers': ['console'],
'level': 'DEBUG',
'level': 'INFO',
},
}
}

View File

@ -1,11 +1,12 @@
#
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
# Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
import threading
import errno
import select
import socket
@ -26,6 +27,8 @@ class State(Enum):
class ogClient:
OG_PATH = '/opt/opengnsys/'
SESSION_POLL_INTERVAL = 5
EVENT_SOCKET_PORT = 55885
def __init__(self, config):
self.CONFIG = config
@ -36,7 +39,7 @@ class ogClient:
if self.mode in {'linux', 'windows'}:
self.event_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.event_sock.setblocking(0)
self.event_sock.bind(('127.0.0.1', 55885))
self.event_sock.bind(('127.0.0.1', ogClient.EVENT_SOCKET_PORT))
else:
self.event_sock = None
@ -49,6 +52,26 @@ class ogClient:
self.ogrest = ogRest(self.CONFIG)
self.seq = None
if self.event_sock:
self.session_check_thread = threading.Thread(target=self._session_check_loop, daemon=True)
self.session_check_thread.start()
def _session_check_loop(self):
while True:
session_status = self.ogrest.check_interactive_session_change()
if session_status is True:
message = "session start user"
elif session_status is False:
message = "session stop user"
else:
message = None
if message:
self.event_sock.sendto(message.encode('utf-8'),
('127.0.0.1', ogClient.EVENT_SOCKET_PORT))
time.sleep(ogClient.SESSION_POLL_INTERVAL)
def get_socket(self):
return self.sock
@ -58,7 +81,7 @@ class ogClient:
def get_state(self):
return self.state
def send_event_hint(self, message):
def handle_session_event(self, message):
try:
event, action, user = message.split(" ")
logging.debug('Sending event: %s, %s, %s', event, action, user)
@ -188,6 +211,6 @@ class ogClient:
self.connect2()
elif state == State.RECEIVING and event_sock in readable:
message = event_sock.recv(4096).decode('utf-8').rstrip()
self.send_event_hint(message)
self.handle_session_event(message)
else:
raise OgError(f'Invalid ogClient run state: {str(state)}.')

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
@ -97,16 +97,12 @@ class ogThread():
ogRest.send_internal_server_error(client, exc=e)
return
if request.getEcho():
json_body = jsonBody()
json_body.add_element('cmd', cmd)
json_body.add_element('out', shellout)
json_body.add_element('retcode', retcode)
response = restResponse(ogResponses.OK, json_body, seq=client.seq)
client.send(response.get())
else:
response = restResponse(ogResponses.OK, seq=client.seq)
client.send(response.get())
json_body = jsonBody()
json_body.add_element('cmd', cmd)
json_body.add_element('out', shellout)
json_body.add_element('retcode', retcode)
response = restResponse(ogResponses.OK, json_body, seq=client.seq)
client.send(response.get())
ogRest.state = ThreadState.IDLE
@ -347,7 +343,7 @@ class ogRest():
self.process_imagerestore(client, request)
elif ("stop" in URI):
self.process_stop(client)
elif ("image/create" in URI):
elif ("image/create" in URI or "image/update" in URI):
self.process_imagecreate(client, request)
elif ("cache/delete" in URI):
self.process_cache_delete(client, request)
@ -443,3 +439,6 @@ class ogRest():
def process_refresh(self, client):
threading.Thread(target=ogThread.refresh, args=(client, self,)).start()
def check_interactive_session_change(self):
return self.operations.check_interactive_session_change()

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
@ -33,7 +33,7 @@ class restRequest:
self.type = None
self.profile = None
self.id = None
self.echo = None
self.inline = None
self.code = None
self.seq = None
self.backup = None
@ -72,7 +72,7 @@ class restRequest:
if "run" in json_param:
self.run = json_param["run"]
try:
self.echo = json_param["echo"]
self.inline = json_param["inline"]
except:
pass
@ -160,8 +160,8 @@ class restRequest:
def getId(self):
return self.id
def getEcho(self):
return self.echo
def get_inline(self):
return self.inline
def getCode(self):
return self.code

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2023 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2023 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2022 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2022 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
@ -11,6 +11,7 @@ import logging
import shlex
import subprocess
import json
from uuid import UUID
from src.log import OgError
import fdisk
@ -190,3 +191,38 @@ def get_partition_start_offset(disk, partition):
raise OgError(f'Error while trying to parse sfdisk: {e}') from e
return start_offset
def uuid_to_bytes(uuid):
uuid = uuid.replace('-', '')
group0 = f'{uuid[6:8]}{uuid[4:6]}{uuid[2:4]}{uuid[0:2]}'
group1 = f'{uuid[10:12]}{uuid[8:10]}'
group2 = f'{uuid[14:16]}{uuid[12:14]}'
group3 = uuid[16:20]
group4 = uuid[20:32]
res = f'{group0}-{group1}-{group2}-{group3}-{group4}'
return UUID(res).bytes
def get_disk_id_bytes(disk):
from src.utils.uefi import is_uefi_supported
disk_id = get_disk_id(disk)
if is_uefi_supported():
return uuid_to_bytes(disk_id)
return bytes.fromhex(disk_id)[::-1]
def get_part_id_bytes(disk, partition):
from src.utils.uefi import is_uefi_supported
if is_uefi_supported():
part_id = get_partition_id(disk, partition)
return uuid_to_bytes(part_id)
partition_start_offset = get_partition_start_offset(disk, partition)
sector_size = get_sector_size(disk)
byte_offset = partition_start_offset * sector_size
byte_offset = "{0:016x}".format(byte_offset)
return bytes.fromhex(byte_offset)[::-1]

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2022 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
@ -153,13 +153,19 @@ def mkfs(fs, disk, partition, label=None):
if fs not in fsdict:
raise OgError(f'mkfs failed, unsupported target filesystem {fs}')
try:
partdev = get_partition_device(disk, partition)
except ValueError as e:
raise OgError(f'mkfs aborted: {e}') from e
partdev = get_partition_device(disk, partition)
return fsdict[fs](partdev, label)
ret = subprocess.run(['wipefs', '-af', f'{partdev}'])
if ret.returncode != 0:
logging.warning(f'wipefs on {partdev}, fails with {ret.returncode}')
err = fsdict[fs](partdev, label)
if err != 0:
ret = subprocess.run(['wipefs', '-af', f'{partdev}'])
if ret.returncode != 0:
logging.warning(f'wipefs on {partdev} for consistency, fails with {ret.returncode}')
return err
def mkfs_ext4(partdev, label=None):
err = -1

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2024 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2024 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
@ -266,7 +266,10 @@ def _get_os_entries(esp_mountpoint):
available_disks = get_disks()
for disk_num, diskname in enumerate(available_disks, start=1):
disk_device = f'/dev/{diskname}'
partitions_data = get_partition_data(device=disk_device)
try:
partitions_data = get_partition_data(device=disk_device)
except OgError as e:
continue
for p in partitions_data:
part_num = p.partno + 1
@ -276,7 +279,7 @@ def _get_os_entries(esp_mountpoint):
continue
if not mount_mkdir(p.padev, mountpoint):
raise OgError(f'Unable to mount {p.padev} into {mountpoint}')
continue
try:
os_family = get_os_family(mountpoint)
@ -331,6 +334,7 @@ def get_disk_part_type(disk_num):
def _update_nvram(esp_disk, esp_part_number):
loader_path = '/EFI/grub/Boot/shimx64.efi'
bootlabel = 'grub'
egibootmgr_reorder_disabled_entries()
efibootmgr_delete_bootentry(bootlabel)
efibootmgr_create_bootentry(esp_disk, esp_part_number, loader_path, bootlabel, add_to_bootorder=False)
efibootmgr_set_entry_order(bootlabel, 1)

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2023 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
@ -293,10 +293,10 @@ def legacy_list_hardware_inventory(inventory):
def get_hardware_inventory():
proc = subprocess.run(['lshw', '-json'], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
j = json.loads(proc.stdout)
root = json.loads(proc.stdout)
if type(j) is list:
root = j[0]
if type(root) is list:
root = root[0]
if type(root) is not dict:
raise OgError('Invalid lshw json output')

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2023 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2022 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2022 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
@ -7,12 +7,15 @@
# (at your option) any later version.
import array
import fcntl
import sys
import socket
import struct
import psutil
import logging
if sys.platform != "win32":
import fcntl
def is_ethernet(interface):
SIOCGIFHWADDR = 0x8927
ARPHRD_ETHER = 1

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2022 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2022 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
@ -12,6 +12,7 @@ import platform
import logging
import sys
from src.utils.winreg import *
from enum import Enum
from subprocess import PIPE
@ -45,35 +46,23 @@ def getlinuxversion(osrelease):
def getwindowsversion(winreghives):
"""
Tries to obtain windows version information by
querying the SOFTWARE registry hive. Registry
hives path is a required parameter.
Runs hivexget(1) to fetch ProductName and
ReleaseId. If something fails (hivexget is
not installed, or registry is not found) it
returns a generic "Microsoft Windows" string.
Try to obtain windows version information by querying the SOFTWARE registry
hive to fetch ProductName and ReleaseId.
Return a generic "Microsoft Windows" string if something fails.
"""
# XXX: 3.6 friendly
try:
proc_prodname = subprocess.run(['hivexget',
f'{winreghives}/SOFTWARE',
'microsoft\windows nt\currentversion',
'ProductName'], stdout=PIPE)
proc_releaseid = subprocess.run(['hivexget',
f'{winreghives}/SOFTWARE',
'microsoft\windows nt\currentversion',
'ReleaseId'], stdout=PIPE)
hivepath = f'{winreghives}/SOFTWARE'
hive = hive_handler_open(hivepath, write = False)
root_node = hive.root()
version_node = get_node_child_from_path(hive, root_node, 'Microsoft/Windows NT/CurrentVersion')
prodname = proc_prodname.stdout.decode().replace('\n', '')
releaseid = proc_releaseid.stdout.decode().replace('\n', '')
bits = ' 64 bits' if windows_is64bit(winreghives) else ''
prodname = get_value_from_node(hive, version_node, 'ProductName')
releaseid = get_value_from_node(hive, version_node, 'ReleaseId')
if proc_prodname.returncode == 0 and proc_releaseid.returncode == 0:
return f'{prodname} {releaseid}{bits}'
except FileNotFoundError: # hivexget command not found
pass
return f'{prodname} {releaseid}'
except (RuntimeError, OgError) as e:
logging.error(f'Hivex was not able to operate over {hivepath}. Reported: {e}')
return 'Microsoft Windows'
@ -81,28 +70,6 @@ def interpreter_is64bit():
return sys.maxsize > 2**32
def windows_is64bit(winreghives):
"""
Check for 64 bit Windows by means of retrieving the value of
ProgramW6432Dir. This key is set if Windows is running 64 bit.
If set returns True.
If not set or hivexget exits with non-zero, returns False.
"""
try:
proc_hivexget = subprocess.run(['hivexget',
f'{winreghives}/SOFTWARE',
'Microsoft\Windows\CurrentVersion',
'ProgramW6432Dir'], stdout=PIPE)
stdout = proc_hivexget.stdout.decode().replace('\n', '')
if proc_hivexget.returncode == 0 and stdout:
return True
except FileNotFoundError: # hivexget command not found
pass
return False
def linux_is64bit(mountpoint):
"""
If /sbin/init is detected, check if compiled for 64-bit machine.
@ -174,7 +141,7 @@ def os_probe(mountpoint):
Returns a string depending on the OS it detects.
"""
winreghives = f'{mountpoint}/Windows/System32/config'
winreghives = f'{mountpoint}{WINDOWS_HIVES_PATH}'
osrelease = f'{mountpoint}/etc/os-release'
if os.path.exists(osrelease):

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2023 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
@ -16,33 +16,39 @@ from collections import namedtuple
import hivex
from src.utils.probe import os_probe
from src.utils.winreg import *
Package = namedtuple('Package', ['name', 'version'])
Package.__str__ = lambda pkg: f'{pkg.name} {pkg.version}'
WINDOWS_HIVES_PATH = '/Windows/System32/config'
WINDOWS_HIVES_SOFTWARE = f'{WINDOWS_HIVES_PATH}/SOFTWARE'
DPKG_STATUS_PATH = '/var/lib/dpkg/status'
OSRELEASE_PATH = '/etc/os-release'
def _fill_package_set(h, key, pkg_set):
def _fill_package_set(hive, key, pkg_set):
"""
Fill the package set looking for entries at the current registry
node childs.
Any valid node child must have "DisplayVersion" or "DisplayName" keys.
"""
childs = h.node_children(key)
valid_childs = [h.node_get_child(key, h.node_name(child))
for child in childs
for value in h.node_values(child) if h.value_key(value) == 'DisplayVersion']
childs = hive.node_children(key)
valid_childs = []
for child in childs:
child_name = hive.node_name(child)
values = hive.node_values(child)
for value in values:
if hive.value_key(value) == 'DisplayVersion':
valid_child = hive.node_get_child(key, child_name)
valid_childs.append(valid_child)
for ch in valid_childs:
try:
name = h.value_string(h.node_get_value(ch, 'DisplayName'))
value = h.node_get_value(ch, 'DisplayVersion')
version = h.value_string(value)
name = hive.value_string(hive.node_get_value(ch, 'DisplayName'))
value = hive.node_get_value(ch, 'DisplayVersion')
version = hive.value_string(value)
pkg = Package(name, version)
pkg_set.add(pkg)
except RuntimeError:
@ -50,22 +56,19 @@ def _fill_package_set(h, key, pkg_set):
pass
def _fill_package_set_1(h, pkg_set):
def _fill_package_set_1(hive, pkg_set):
"""
Looks for entries in registry path
/Microsoft/Windows/CurrentVersion/Uninstall
Fills the given set with Package instances for each program found.
"""
key = h.root()
key = h.node_get_child(key, 'Microsoft')
key = h.node_get_child(key, 'Windows')
key = h.node_get_child(key, 'CurrentVersion')
key = h.node_get_child(key, 'Uninstall')
_fill_package_set(h, key, pkg_set)
root_node = hive.root()
key = get_node_child_from_path(hive, root_node, 'Microsoft/Windows/CurrentVersion/Uninstall')
_fill_package_set(hive, key, pkg_set)
def _fill_package_set_2(h, pkg_set):
def _fill_package_set_32_bit_compat(hive, pkg_set):
"""
Looks for entries in registry path
/Wow6432Node/Microsoft/Windows/CurrentVersion/Uninstall
@ -73,23 +76,22 @@ def _fill_package_set_2(h, pkg_set):
Fills the given set with Package instances for each program found.
"""
key = h.root()
key = h.node_get_child(key, 'Wow6432Node')
key = h.node_get_child(key, 'Microsoft')
key = h.node_get_child(key, 'Windows')
key = h.node_get_child(key, 'CurrentVersion')
key = h.node_get_child(key, 'Uninstall')
_fill_package_set(h, key, pkg_set)
root_node = hive.root()
key = get_node_child_from_path(hive, root_node, 'Wow6432Node/Windows/CurrentVersion/Uninstall')
_fill_package_set(hive, key, pkg_set)
def _get_package_set_windows(hivepath):
packages = set()
try:
h = hivex.Hivex(hivepath)
h = hive_handler_open(hivepath, write = False)
_fill_package_set_1(h, packages)
_fill_package_set_2(h, packages)
except RuntimeError as e:
except (RuntimeError, OgError) as e:
logging.error(f'Hivex was not able to operate over {hivepath}. Reported: {e}')
try:
_fill_package_set_32_bit_compat(h, packages)
except (RuntimeError, OgError) as e:
pass
return packages
@ -119,7 +121,7 @@ def _get_package_set_dpkg(dpkg_status_path):
def get_package_set(mountpoint):
dpkg_status_path = f'{mountpoint}{DPKG_STATUS_PATH}'
softwarehive = f'{mountpoint}{WINDOWS_HIVES_SOFTWARE}'
softwarehive = f'{mountpoint}{WINDOWS_HIVE_SOFTWARE}'
if os.path.exists(softwarehive):
pkgset = _get_package_set_windows(softwarehive)
elif os.path.exists(dpkg_status_path):

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2023 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2023 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
@ -14,7 +14,6 @@ import subprocess
import shutil
from src.utils.disk import *
from src.utils.fs import *
from src.utils.probe import *
from src.log import OgError
import fdisk
@ -26,7 +25,7 @@ def _find_bootentry(entries, label):
if entry['description'] == label:
return entry
else:
raise OgError('Boot entry {label} not found')
raise OgError(f'Boot entry {label} not found')
def _strip_boot_prefix(entry):
@ -96,8 +95,13 @@ def efibootmgr_bootnext(description):
entry = _find_bootentry(boot_entries, description)
num = _strip_boot_prefix(entry) # efibootmgr output uses BootXXXX for each entry, remove the "Boot" prefix.
bootnext_cmd = bootnext_cmd.format(bootnum=num, efibootmgr=EFIBOOTMGR_BIN)
subprocess.run(shlex.split(bootnext_cmd), check=True,
stdout=subprocess.DEVNULL)
try:
subprocess.run(shlex.split(bootnext_cmd), check=True,
stdout=subprocess.DEVNULL)
except subprocess.CalledProcessError:
logging.warning("Failed to update BootNext. UEFI firmware might be buggy")
except OSError as e:
raise OgError(f"Unexpected error updating BootNext: {e}") from e
def efibootmgr_delete_bootentry(label):
@ -108,7 +112,12 @@ def efibootmgr_delete_bootentry(label):
if entry['description'] == label:
num = entry['name'][4:] # Remove "Boot" prefix to extract num
efibootmgr_cmd = efibootmgr_cmd.format(bootnum=num, efibootmgr=EFIBOOTMGR_BIN)
subprocess.run(shlex.split(efibootmgr_cmd), check=True)
try:
subprocess.run(shlex.split(efibootmgr_cmd), check=True)
except subprocess.CalledProcessError:
logging.warning(f"Failed to delete boot entry {label}. UEFI firmware might be buggy")
except OSError as e:
raise OgError(f"Unexpected error deleting boot entry {label}: {e}") from e
break
else:
logging.info(f'Cannot delete boot entry {label} because it was not found.')
@ -122,8 +131,10 @@ def efibootmgr_create_bootentry(disk, part, loader, label, add_to_bootorder=True
logging.info(f'{EFIBOOTMGR_BIN} command creating boot entry: {efibootmgr_cmd}')
try:
proc = subprocess.run(shlex.split(efibootmgr_cmd), check=True, text=True)
except subprocess.CalledProcessError:
logging.warning(f"Failed to add boot entry {label} to NVRAM. UEFI firmware might be buggy")
except OSError as e:
raise OgError(f'Unexpected error adding boot entry to nvram. UEFI firmware might be buggy') from e
raise OgError(f"Unexpected error adding boot entry {label}: {e}") from e
def efibootmgr_set_entry_order(label, position):
logging.info(f'Setting {label} entry to position {position} of boot order')
@ -141,8 +152,29 @@ def efibootmgr_set_entry_order(label, position):
try:
proc = subprocess.run([EFIBOOTMGR_BIN, "-o", ",".join(boot_order)], check=True, text=True)
except subprocess.CalledProcessError:
logging.warning("Failed to set boot order to NVRAM. UEFI firmware might be buggy")
except OSError as e:
raise OgError(f'Unexpected error setting boot order to NVRAM. UEFI firmware might be buggy') from e
raise OgError(f"Unexpected error updating boot order: {e}") from e
def egibootmgr_reorder_disabled_entries():
logging.info(f'Setting disabled entries at the end of boot order')
boot_info = run_efibootmgr_json(validate=False)
boot_entries = boot_info.get('vars', [])
boot_order = boot_info.get('BootOrder', [])
for entry in boot_entries:
entry_number = _strip_boot_prefix(entry)
if not entry['active'] and entry_number in boot_order:
boot_order.remove(entry_number)
boot_order.append(entry_number)
try:
proc = subprocess.run([EFIBOOTMGR_BIN, "-o", ",".join(boot_order)], check=True, text=True)
except subprocess.CalledProcessError:
logging.warning("Failed to update boot order. UEFI firmware might be buggy")
except OSError as e:
raise OgError(f"Unexpected error updating boot order: {e}") from e
def _find_efi_loader(loader_paths):
for efi_app in loader_paths:
@ -170,13 +202,7 @@ def copy_windows_efi_bootloader(disk, partition):
device = get_partition_device(disk, partition)
mountpoint = device.replace('dev', 'mnt')
if not mount_mkdir(device, mountpoint):
raise OgError(f'Cannot probe OS family. Unable to mount {device} into {mountpoint}')
os_family = get_os_family(mountpoint)
is_uefi = is_uefi_supported()
if not is_uefi or os_family != OSFamily.WINDOWS:
return
raise OgError(f'Unable to mount {device} into {mountpoint}')
bootlabel = f'Part-{disk:02d}-{partition:02d}'
esp, esp_disk, esp_part_number = get_efi_partition(disk, enforce_gpt=True)
@ -209,7 +235,7 @@ def restore_windows_efi_bootloader(disk, partition):
device = get_partition_device(disk, partition)
mountpoint = device.replace('dev', 'mnt')
if not mount_mkdir(device, mountpoint):
raise OgError(f'Cannot probe OS family. Unable to mount {device} into {mountpoint}')
raise OgError(f'Unable to mount {device} into {mountpoint}')
bootlabel = f'Part-{disk:02d}-{partition:02d}'
esp, esp_disk, esp_part_number = get_efi_partition(disk, enforce_gpt=True)
@ -233,4 +259,4 @@ def restore_windows_efi_bootloader(disk, partition):
raise OgError(f'Failed to copy {loader_dir} into {destination_dir}: {e}') from e
finally:
umount(mountpoint)
umount(esp_mountpoint)
umount(esp_mountpoint)

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2022 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
@ -10,9 +10,7 @@ import libhivexmod
import hivex
from enum import Enum
from src.log import OgError
from uuid import UUID
from src.utils.disk import *
from src.utils.uefi import is_uefi_supported
WINDOWS_HIVES_PATH = '/Windows/System32/config'
@ -102,36 +100,3 @@ def get_node_child_from_path(hive, node, path):
child_node = get_node_child(hive, child_node, node_name)
return child_node
def uuid_to_bytes(uuid):
uuid = uuid.replace('-', '')
group0 = f'{uuid[6:8]}{uuid[4:6]}{uuid[2:4]}{uuid[0:2]}'
group1 = f'{uuid[10:12]}{uuid[8:10]}'
group2 = f'{uuid[14:16]}{uuid[12:14]}'
group3 = uuid[16:20]
group4 = uuid[20:32]
res = f'{group0}-{group1}-{group2}-{group3}-{group4}'
return UUID(res).bytes
def get_disk_id_bytes(disk):
disk_id = get_disk_id(disk)
if is_uefi_supported():
return uuid_to_bytes(disk_id)
return bytes.fromhex(disk_id)[::-1]
def get_part_id_bytes(disk, partition):
if is_uefi_supported():
part_id = get_partition_id(disk, partition)
return uuid_to_bytes(part_id)
partition_start_offset = get_partition_start_offset(disk, partition)
sector_size = get_sector_size(disk)
byte_offset = partition_start_offset * sector_size
byte_offset = "{0:016x}".format(byte_offset)
return bytes.fromhex(byte_offset)[::-1]

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
@ -253,6 +253,9 @@ class OgVirtualOperations:
qemu = OgVM(part_path)
qemu.run_vm()
def software(self, request, ogRest):
raise OgError('Function not implemented')
def partitions_cfg_to_json(self, data):
for part in data['partition_setup']:
part.pop('virt-drive')
@ -469,6 +472,9 @@ class OgVirtualOperations:
return json_dict
def image_create(self, request, ogRest):
raise OgError('Function not implemented')
def cache_delete(self, request, ogRest):
raise OgError('Function not implemented')
@ -623,3 +629,6 @@ class OgVirtualOperations:
for k, v in device_names.items():
f.write(f'{k}={v}\n')
f.truncate()
def check_interactive_session_change(self):
return None

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2023 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
@ -7,99 +7,56 @@
# (at your option) any later version.
import os
import ctypes
import psutil
import subprocess
from subprocess import CalledProcessError
import multiprocessing as mp
from multiprocessing import Process, freeze_support
from src.log import OgError
from PIL import Image, ImageDraw
from pystray import Icon, Menu, MenuItem
from src.ogRest import ThreadState
def _create_default_image():
"""
Creates a default image for the tray icon. Use in case
no favicon.ico is found.
"""
width = height = 250
color1 = (255, 255, 255)
color2 = (255, 0, 255)
image = Image.new('RGB', (width, height), color1)
dc = ImageDraw.Draw(image)
dc.rectangle(
(width // 2, 0, width, height // 2),
fill=color2)
dc.rectangle(
(0, height // 2, width // 2, height),
fill=color2)
return image
def create_image():
try:
image = Image.open(r'./favicon.ico')
image = Image.composite(image, Image.new('RGB', image.size, 'white'), image)
except:
image = _create_default_image()
return image
def create_systray():
menu = Menu(MenuItem('Powered by Soleta Networks!',
lambda icon, item: 1))
icon = Icon('ogClient', create_image(), menu=menu)
assert icon.icon
icon.run()
systray_p = Process(target=create_systray)
class OgWindowsOperations:
def __init__(self):
freeze_support()
mp.set_start_method('spawn')
systray_p.start()
self.session = False
def _restartBrowser(self, url):
raise OgError('Function not implemented')
def poweroff(self):
systray_p.terminate()
os.system('shutdown -s -t 0')
def reboot(self):
systray_p.terminate()
os.system('shutdown -r -t 0')
def shellrun(self, request, ogRest):
cmd = request.getrun()
is_inline = request.get_inline()
if not is_inline:
raise OgError("Only inline mode is supported on Windows")
env = os.environ.copy()
env['OutputEncoding'] = 'utf8'
try:
result = subprocess.run(cmd,
shell=True,
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
check=True)
except CalledProcessError as error:
if error.stderr:
return error.stderr
if error.stdout:
return error.stdout
return "{Non zero exit code and empty output}"
return (result.returncode, cmd, result.stdout)
ogRest.proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
shell=True,
env=env,
)
output, error = ogRest.proc.communicate()
except (OSError, subprocess.SubprocessError) as e:
raise OgError(f'Error when running "shell run" subprocess: {e}') from e
output = output.decode('utf-8-sig', errors='replace')
return (ogRest.proc.returncode, cmd, output)
def session(self, request, ogRest):
raise OgError('Function not implemented')
def hardware(self, path, ogRest):
def software(self, request, ogRest):
raise OgError('Function not implemented')
def hardware(self, ogRest):
raise OgError('Function not implemented')
def setup(self, request, ogRest):
@ -108,7 +65,7 @@ class OgWindowsOperations:
def image_restore(self, request, ogRest):
raise OgError('Function not implemented')
def image_create(self, path, request, ogRest):
def image_create(self, request, ogRest):
raise OgError('Function not implemented')
def cache_delete(self, request, ogRest):
@ -118,4 +75,20 @@ class OgWindowsOperations:
raise OgError('Function not implemented')
def refresh(self, ogRest):
return {"status": "WIN"}
if self.session:
session_value = 'WINS'
else:
session_value = 'WIN'
return {"status": session_value}
def check_interactive_session_change(self):
old_status = self.session
has_logged_user = False
for user in psutil.users():
has_logged_user = True
break
self.session = has_logged_user
if self.session != old_status:
return self.session
return None

View File

@ -0,0 +1,99 @@
#!/usr/bin/python3
#
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
# Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
import time
import multiprocessing as mp
import psutil
from PIL import Image, ImageDraw
from pystray import Icon, Menu, MenuItem
SERVICE_POLL_INTERVAL = 5
def _create_default_image():
"""
Creates a default image for the tray icon. Use in case
no favicon.ico is found. The image will be a blue circle.
"""
width = height = 250
circle_color = (45, 158, 251)
# Create a new image with a transparent background
image = Image.new('RGBA', (width, height), (0, 0, 0, 0))
dc = ImageDraw.Draw(image)
# Draw circle
circle_radius = min(width, height) // 2 - 10
circle_center = (width // 2, height // 2)
dc.ellipse(
(circle_center[0] - circle_radius, circle_center[1] - circle_radius,
circle_center[0] + circle_radius, circle_center[1] + circle_radius),
fill=circle_color
)
return image
def create_icon_image():
try:
image = Image.open(r'./favicon.ico')
image = Image.composite(image, Image.new('RGB', image.size, 'white'), image)
except FileNotFoundError:
image = _create_default_image()
return image
def create_systray():
menu = Menu(MenuItem('ogclient service running',
lambda icon, item: None))
icon = Icon('ogClient', create_icon_image(), menu=menu)
icon.run()
def is_ogclient_service_active():
service_name = 'ogclient'
try:
service = psutil.win_service_get(service_name)
service = service.as_dict()
return service.get('status') == 'running'
except Exception:
return False
def _session_check_loop():
systray_process = None
try:
while True:
session_status = is_ogclient_service_active()
is_systray_active = systray_process and systray_process.is_alive()
if session_status and not is_systray_active:
systray_process = mp.Process(target=create_systray, daemon=True)
systray_process.start()
elif not session_status and is_systray_active:
systray_process.terminate()
systray_process.join()
systray_process = None
time.sleep(SERVICE_POLL_INTERVAL)
except KeyboardInterrupt:
if systray_process and systray_process.is_alive():
systray_process.terminate()
systray_process.join()
def main():
mp.freeze_support()
mp.set_start_method('spawn')
_session_check_loop()
if __name__ == "__main__":
main()

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the

View File

@ -0,0 +1,84 @@
#!/usr/bin/python3
#
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
# Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
import subprocess
from datetime import datetime
version_template = """
VSVersionInfo(
ffi=FixedFileInfo(
filevers=({major}, {minor}, {patch}, 0),
prodvers=({major}, {minor}, {patch}, 0),
mask=0x3f,
flags=0x0,
OS=0x40004,
fileType=0x1,
subtype=0x0,
date=(0, 0)
),
kids=[
StringFileInfo(
[
StringTable(
'040904B0',
[
StringStruct('CompanyName', 'Soleta Networks'),
StringStruct('FileDescription', '{appname} - OpenGnsys Client Application'),
StringStruct('FileVersion', '{version}'),
StringStruct('InternalName', '{appname}'),
StringStruct('LegalCopyright', 'Copyright © {year} Soleta Networks'),
StringStruct('OriginalFilename', '{appname}.exe'),
StringStruct('ProductName', '{appname}'),
StringStruct('ProductVersion', '{version}')
]
)
]
),
VarFileInfo([VarStruct('Translation', [1033, 1200])])
]
)
"""
def get_git_version():
try:
version = subprocess.check_output(
["git", "describe", "--tags", "--always"],
stderr=subprocess.STDOUT,
text=True
).strip()
return version
except subprocess.CalledProcessError:
return "0.0.0"
def version_to_tuple(version):
parts = version.lstrip("v").split("-")[0].split(".")
version_tuple = []
for part in parts:
if part.isdigit():
version_tuple.append(int(part))
else:
version_tuple.append(0)
return tuple(version_tuple)
if __name__ == "__main__":
version = get_git_version()
major, minor, patch = version_to_tuple(version)
current_year = datetime.now().year
version_file = version_template.format(major=major, minor=minor,
patch=patch, version=version,
year=current_year, appname='ogclient')
with open('ogclient-version-info.txt', 'w', encoding='utf-8') as f:
f.write(version_file)
version_file = version_template.format(major=major, minor=minor,
patch=patch, version=version,
year=current_year, appname='ogclient-systray')
with open('systray-version-info.txt', 'w', encoding='utf-8') as f:
f.write(version_file)