mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 18:32:13 +00:00
119 lines
6.0 KiB
Python
119 lines
6.0 KiB
Python
"""Apply CPU/RAM-only proposals to the existing guest, without an image archive."""
|
|
import copy
|
|
import hashlib
|
|
from pathlib import Path
|
|
import uuid
|
|
|
|
import oci_instances as instances
|
|
import oci_instance_transaction as transaction
|
|
from oci_installation_state import parse_config, sha
|
|
from oci_ui import translate, msg_info, msg_ok, msg_warn
|
|
|
|
CONFIG_DIR = Path('/etc/pve/lxc')
|
|
|
|
|
|
def changes(record, candidate, config):
|
|
"""Return a narrow native edit, or None when the normal installer is needed."""
|
|
if (any(key in record for key in ('stack', 'stack_member', 'native_stack_intent'))
|
|
or record['deployment'].get('stack_managed')
|
|
or instances.identity(config) != record['installation_id']):
|
|
return None
|
|
previous = transaction.with_default_healthcheck(copy.deepcopy(record))
|
|
candidate = transaction.with_default_healthcheck(copy.deepcopy(candidate))
|
|
old, new = previous['deployment'], candidate['deployment']
|
|
if previous['template'] != candidate['template'] or old.get('host_monitor'):
|
|
return None
|
|
if {k: v for k, v in old.items() if k != 'resources'} != {
|
|
k: v for k, v in new.items() if k != 'resources'}:
|
|
return None
|
|
original, desired = old.get('resources', {}), new.get('resources', {})
|
|
changed = {key for key in original.keys() | desired.keys() if original.get(key) != desired.get(key)}
|
|
if not changed or changed - {'cores', 'memory_mb'}:
|
|
return None
|
|
cfg = parse_config(config)
|
|
cpu = 'cpulimit' if 'cpulimit' in cfg and 'cores' not in cfg else 'cores'
|
|
parameters = {}
|
|
for key in sorted(changed):
|
|
value = desired.get(key)
|
|
if isinstance(value, bool) or not isinstance(value, int) or value < (128 if key == 'memory_mb' else 1):
|
|
return None
|
|
native = 'memory' if key == 'memory_mb' else cpu
|
|
if native not in cfg:
|
|
return None
|
|
parameters[native] = str(value)
|
|
return parameters
|
|
|
|
|
|
def guarded_set(vmid, installation_id, expected, parameters):
|
|
# pct uses this SHA1 under its own configuration lock. Sample the native
|
|
# file before checking pct's view, so a concurrent edit cannot be accepted
|
|
# accidentally by taking a fresh digest after validation.
|
|
native = (CONFIG_DIR / f'{vmid}.conf').read_bytes()
|
|
current = instances.command('pct', 'config', str(vmid))
|
|
if current != expected or instances.identity(current) != installation_id:
|
|
raise ValueError(translate('The instance changed while it was being edited; open Modify again'))
|
|
command = ['pct', 'set', str(vmid), '--digest', hashlib.sha1(native).hexdigest()]
|
|
for key, value in sorted(parameters.items()):
|
|
command += ['--' + key, value]
|
|
transaction.run(*command)
|
|
|
|
|
|
def expected_config(before, after, parameters):
|
|
def unrelated(config):
|
|
return b''.join(line for line in config.splitlines(keepends=True)
|
|
if line.split(b':', 1)[0].decode() not in parameters)
|
|
new = parse_config(after)
|
|
return unrelated(before) == unrelated(after) and all(new.get(key) == value for key, value in parameters.items())
|
|
|
|
|
|
def apply(root, record, candidate, config, parameters):
|
|
"""The caller holds the instance lock and has checked image identity/preflight."""
|
|
if changes(record, candidate, config) != parameters:
|
|
raise ValueError(translate('The instance changed while it was being edited; open Modify again'))
|
|
if transaction.external_changes(record, config):
|
|
raise ValueError(translate('The instance changed while it was being edited; open Modify again'))
|
|
vmid = record['vmid']
|
|
directory = instances.location(root, vmid).parent / 'modifications' / uuid.uuid4().hex
|
|
instances.write(directory / 'before.json', {'record': record, 'config': config.decode()})
|
|
transaction.open_log(directory)
|
|
msg_info(translate('Applying CPU and RAM changes without rebuilding the container...'))
|
|
applied = False
|
|
after = None
|
|
try:
|
|
guarded_set(vmid, record['installation_id'], config, parameters)
|
|
applied = True
|
|
after = instances.command('pct', 'config', str(vmid))
|
|
if not expected_config(config, after, parameters):
|
|
raise ValueError(translate('The instance changed while it was being edited; open Modify again'))
|
|
observed = record['observed']
|
|
fresh = instances.observe(vmid, record['installation_id'], observed['archive_path'],
|
|
observed['resolved_registry_digest'], observed['image'])
|
|
if fresh['config_sha256'] != sha(after):
|
|
raise ValueError(translate('The instance changed while it was being edited; open Modify again'))
|
|
updated = copy.deepcopy(record)
|
|
updated.update(deployment=copy.deepcopy(candidate['deployment']), observed=fresh)
|
|
instances.write(instances.location(root, vmid), updated)
|
|
except Exception:
|
|
if applied:
|
|
try:
|
|
current = instances.command('pct', 'config', str(vmid))
|
|
if not expected_config(config, current, parameters):
|
|
raise ValueError('Concurrent configuration change; resource rollback refused')
|
|
previous = parse_config(config)
|
|
guarded_set(vmid, record['installation_id'], current,
|
|
{key: previous[key] for key in parameters})
|
|
instances.write(instances.location(root, vmid), record)
|
|
except Exception as error:
|
|
transaction.log(f'resource rollback: {type(error).__name__}')
|
|
msg_warn(translate('The resource edit could not be fully rolled back. Open Manage again to review and import the current Proxmox settings.'))
|
|
raise
|
|
# Updating the carried record is best effort here, as it is after other
|
|
# management operations. The normal menu also refreshes it on return.
|
|
try:
|
|
import oci_carried_record
|
|
oci_carried_record.carry(root, vmid, mount_stopped=False)
|
|
except (OSError, ValueError, RuntimeError):
|
|
transaction.log('carried record will be refreshed on the next management entry')
|
|
msg_ok(translate('Changes applied. Data kept.'))
|
|
return directory
|