Files

119 lines
6.0 KiB
Python
Raw Permalink Normal View History

"""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