Merge remote-tracking branch 'origin/pr/389'
* origin/pr/389: app: save qubes.xml with utils.replace_file() app: use suppress() in simple cases firewall: save firewall.xml with utils.replace_file() utils: take tweaked helper functions from storage/reflink storage/reflink: quote logged filenames
This commit is contained in:
commit
e1991d5c33
36
qubes/app.py
36
qubes/app.py
@ -29,10 +29,10 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
|
from contextlib import suppress
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import jinja2
|
import jinja2
|
||||||
@ -280,11 +280,9 @@ class QubesHost:
|
|||||||
|
|
||||||
self.app.log.debug('QubesHost: no_cpus={} memory_total={}'.format(
|
self.app.log.debug('QubesHost: no_cpus={} memory_total={}'.format(
|
||||||
self.no_cpus, self.memory_total))
|
self.no_cpus, self.memory_total))
|
||||||
try:
|
with suppress(NotImplementedError):
|
||||||
self.app.log.debug('QubesHost: xen_free_memory={}'.format(
|
self.app.log.debug('QubesHost: xen_free_memory={}'.format(
|
||||||
self.get_free_xen_memory()))
|
self.get_free_xen_memory()))
|
||||||
except NotImplementedError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def memory_total(self):
|
def memory_total(self):
|
||||||
@ -1103,18 +1101,12 @@ class Qubes(qubes.PropertyHolder):
|
|||||||
if not self.__locked_fh:
|
if not self.__locked_fh:
|
||||||
self._acquire_lock(for_save=True)
|
self._acquire_lock(for_save=True)
|
||||||
|
|
||||||
fh_new = tempfile.NamedTemporaryFile(
|
with qubes.utils.replace_file(self._store, permissions=0o660,
|
||||||
prefix=self._store, delete=False)
|
close_on_success=False) as fh_new:
|
||||||
lxml.etree.ElementTree(self.__xml__()).write(
|
lxml.etree.ElementTree(self.__xml__()).write(
|
||||||
fh_new, encoding='utf-8', pretty_print=True)
|
fh_new, encoding='utf-8', pretty_print=True)
|
||||||
fh_new.flush()
|
with suppress(KeyError): # group not found
|
||||||
try:
|
os.fchown(fh_new.fileno(), -1, grp.getgrnam('qubes').gr_gid)
|
||||||
os.chown(fh_new.name, -1, grp.getgrnam('qubes').gr_gid)
|
|
||||||
os.chmod(fh_new.name, 0o660)
|
|
||||||
except KeyError: # group 'qubes' not found
|
|
||||||
# don't change mode if no 'qubes' group in the system
|
|
||||||
pass
|
|
||||||
os.rename(fh_new.name, self._store)
|
|
||||||
|
|
||||||
# update stored mtime, in case of multiple save() calls without
|
# update stored mtime, in case of multiple save() calls without
|
||||||
# loading qubes.xml again
|
# loading qubes.xml again
|
||||||
@ -1324,10 +1316,8 @@ class Qubes(qubes.PropertyHolder):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# first search for index, verbatim
|
# first search for index, verbatim
|
||||||
try:
|
with suppress(KeyError):
|
||||||
return self.labels[label]
|
return self.labels[label]
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# then search for name
|
# then search for name
|
||||||
for i in self.labels.values():
|
for i in self.labels.values():
|
||||||
@ -1335,10 +1325,8 @@ class Qubes(qubes.PropertyHolder):
|
|||||||
return i
|
return i
|
||||||
|
|
||||||
# last call, if label is a number represented as str, search in indices
|
# last call, if label is a number represented as str, search in indices
|
||||||
try:
|
with suppress(KeyError, ValueError):
|
||||||
return self.labels[int(label)]
|
return self.labels[int(label)]
|
||||||
except (KeyError, ValueError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
raise qubes.exc.QubesLabelNotFoundError(label)
|
raise qubes.exc.QubesLabelNotFoundError(label)
|
||||||
|
|
||||||
@ -1477,7 +1465,7 @@ class Qubes(qubes.PropertyHolder):
|
|||||||
# allow removed VM to reference itself
|
# allow removed VM to reference itself
|
||||||
continue
|
continue
|
||||||
for prop in obj.property_list():
|
for prop in obj.property_list():
|
||||||
try:
|
with suppress(AttributeError):
|
||||||
if isinstance(prop, qubes.vm.VMProperty) and \
|
if isinstance(prop, qubes.vm.VMProperty) and \
|
||||||
getattr(obj, prop.__name__) == vm:
|
getattr(obj, prop.__name__) == vm:
|
||||||
self.log.error(
|
self.log.error(
|
||||||
@ -1489,8 +1477,6 @@ class Qubes(qubes.PropertyHolder):
|
|||||||
"see 'journalctl -u qubesd -e' in dom0 for "
|
"see 'journalctl -u qubesd -e' in dom0 for "
|
||||||
'details'.format(
|
'details'.format(
|
||||||
vm.name))
|
vm.name))
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
assignments = vm.get_provided_assignments()
|
assignments = vm.get_provided_assignments()
|
||||||
if assignments:
|
if assignments:
|
||||||
@ -1512,11 +1498,9 @@ class Qubes(qubes.PropertyHolder):
|
|||||||
'updatevm',
|
'updatevm',
|
||||||
'default_template',
|
'default_template',
|
||||||
):
|
):
|
||||||
try:
|
with suppress(AttributeError):
|
||||||
if getattr(self, propname) == vm:
|
if getattr(self, propname) == vm:
|
||||||
delattr(self, propname)
|
delattr(self, propname)
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@qubes.events.handler('property-pre-set:clockvm')
|
@qubes.events.handler('property-pre-set:clockvm')
|
||||||
def on_property_pre_set_clockvm(self, event, name, newvalue, oldvalue=None):
|
def on_property_pre_set_clockvm(self, event, name, newvalue, oldvalue=None):
|
||||||
|
@ -31,6 +31,7 @@ import asyncio
|
|||||||
import lxml.etree
|
import lxml.etree
|
||||||
|
|
||||||
import qubes
|
import qubes
|
||||||
|
import qubes.utils
|
||||||
import qubes.vm.qubesvm
|
import qubes.vm.qubesvm
|
||||||
|
|
||||||
|
|
||||||
@ -577,14 +578,13 @@ class Firewall:
|
|||||||
xml_tree = lxml.etree.ElementTree(xml_root)
|
xml_tree = lxml.etree.ElementTree(xml_root)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
old_umask = os.umask(0o002)
|
with qubes.utils.replace_file(firewall_conf,
|
||||||
with open(firewall_conf, 'wb') as firewall_xml:
|
permissions=0o664) as tmp_io:
|
||||||
xml_tree.write(firewall_xml, encoding="UTF-8",
|
xml_tree.write(tmp_io, encoding='UTF-8', pretty_print=True)
|
||||||
pretty_print=True)
|
|
||||||
os.umask(old_umask)
|
|
||||||
except EnvironmentError as err:
|
except EnvironmentError as err:
|
||||||
self.vm.log.error("save error: {}".format(err))
|
msg='firewall save error: {}'.format(err)
|
||||||
raise qubes.exc.QubesException('save error: {}'.format(err))
|
self.vm.log.error(msg)
|
||||||
|
raise qubes.exc.QubesException(msg)
|
||||||
|
|
||||||
self.vm.fire_event('firewall-changed')
|
self.vm.fire_event('firewall-changed')
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ import subprocess
|
|||||||
import tempfile
|
import tempfile
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
from contextlib import contextmanager, suppress
|
from contextlib import suppress
|
||||||
|
|
||||||
import qubes.storage
|
import qubes.storage
|
||||||
import qubes.utils
|
import qubes.utils
|
||||||
@ -234,7 +234,7 @@ class ReflinkVolume(qubes.storage.Volume):
|
|||||||
def _commit(self, path_from):
|
def _commit(self, path_from):
|
||||||
self._add_revision()
|
self._add_revision()
|
||||||
self._prune_revisions()
|
self._prune_revisions()
|
||||||
_fsync_path(path_from)
|
qubes.utils.fsync_path(path_from)
|
||||||
_rename_file(path_from, self._path_clean)
|
_rename_file(path_from, self._path_clean)
|
||||||
|
|
||||||
def _add_revision(self):
|
def _add_revision(self):
|
||||||
@ -364,32 +364,16 @@ class ReflinkVolume(qubes.storage.Volume):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def _replace_file(dst):
|
def _replace_file(dst):
|
||||||
''' Yield a tempfile whose name starts with dst, creating the last
|
_make_dir(os.path.dirname(dst))
|
||||||
directory component if necessary. If the block does not raise
|
return qubes.utils.replace_file(
|
||||||
an exception, safely rename the tempfile to dst.
|
dst, permissions=0o600, log_level=logging.INFO)
|
||||||
'''
|
|
||||||
tmp_dir, prefix = os.path.split(dst + '~')
|
|
||||||
_make_dir(tmp_dir)
|
|
||||||
tmp = tempfile.NamedTemporaryFile(dir=tmp_dir, prefix=prefix, delete=False)
|
|
||||||
try:
|
|
||||||
yield tmp
|
|
||||||
tmp.flush()
|
|
||||||
os.fsync(tmp.fileno())
|
|
||||||
tmp.close()
|
|
||||||
_rename_file(tmp.name, dst)
|
|
||||||
except:
|
|
||||||
tmp.close()
|
|
||||||
_remove_file(tmp.name)
|
|
||||||
raise
|
|
||||||
|
|
||||||
def _fsync_path(path):
|
_rename_file = functools.partial(
|
||||||
fd = os.open(path, os.O_RDONLY) # works for a file or a directory
|
qubes.utils.rename_file, log_level=logging.INFO)
|
||||||
try:
|
|
||||||
os.fsync(fd)
|
_remove_file = functools.partial(
|
||||||
finally:
|
qubes.utils.remove_file, log_level=logging.INFO)
|
||||||
os.close(fd)
|
|
||||||
|
|
||||||
def _make_dir(path):
|
def _make_dir(path):
|
||||||
''' mkdir path, ignoring FileExistsError; return whether we
|
''' mkdir path, ignoring FileExistsError; return whether we
|
||||||
@ -397,35 +381,20 @@ def _make_dir(path):
|
|||||||
'''
|
'''
|
||||||
with suppress(FileExistsError):
|
with suppress(FileExistsError):
|
||||||
os.mkdir(path)
|
os.mkdir(path)
|
||||||
_fsync_path(os.path.dirname(path))
|
qubes.utils.fsync_path(os.path.dirname(path))
|
||||||
LOGGER.info('Created directory: %s', path)
|
LOGGER.info('Created directory: %r', path)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _remove_file(path):
|
|
||||||
with suppress(FileNotFoundError):
|
|
||||||
os.remove(path)
|
|
||||||
_fsync_path(os.path.dirname(path))
|
|
||||||
LOGGER.info('Removed file: %s', path)
|
|
||||||
|
|
||||||
def _remove_empty_dir(path):
|
def _remove_empty_dir(path):
|
||||||
try:
|
try:
|
||||||
os.rmdir(path)
|
os.rmdir(path)
|
||||||
_fsync_path(os.path.dirname(path))
|
qubes.utils.fsync_path(os.path.dirname(path))
|
||||||
LOGGER.info('Removed empty directory: %s', path)
|
LOGGER.info('Removed empty directory: %r', path)
|
||||||
except OSError as ex:
|
except OSError as ex:
|
||||||
if ex.errno not in (errno.ENOENT, errno.ENOTEMPTY):
|
if ex.errno not in (errno.ENOENT, errno.ENOTEMPTY):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _rename_file(src, dst):
|
|
||||||
os.rename(src, dst)
|
|
||||||
dst_dir = os.path.dirname(dst)
|
|
||||||
src_dir = os.path.dirname(src)
|
|
||||||
_fsync_path(dst_dir)
|
|
||||||
if src_dir != dst_dir:
|
|
||||||
_fsync_path(src_dir)
|
|
||||||
LOGGER.info('Renamed file: %s -> %s', src, dst)
|
|
||||||
|
|
||||||
def _resize_file(path, size):
|
def _resize_file(path, size):
|
||||||
''' Resize an existing file. '''
|
''' Resize an existing file. '''
|
||||||
with open(path, 'rb+') as file:
|
with open(path, 'rb+') as file:
|
||||||
@ -436,7 +405,7 @@ def _create_sparse_file(path, size):
|
|||||||
''' Create an empty sparse file. '''
|
''' Create an empty sparse file. '''
|
||||||
with _replace_file(path) as tmp:
|
with _replace_file(path) as tmp:
|
||||||
tmp.truncate(size)
|
tmp.truncate(size)
|
||||||
LOGGER.info('Created sparse file: %s', tmp.name)
|
LOGGER.info('Created sparse file: %r', tmp.name)
|
||||||
|
|
||||||
def _update_loopdev_sizes(img):
|
def _update_loopdev_sizes(img):
|
||||||
''' Resolve img; update the size of loop devices backed by it. '''
|
''' Resolve img; update the size of loop devices backed by it. '''
|
||||||
@ -466,9 +435,9 @@ def _copy_file(src, dst):
|
|||||||
with _replace_file(dst) as tmp_io:
|
with _replace_file(dst) as tmp_io:
|
||||||
with open(src, 'rb') as src_io:
|
with open(src, 'rb') as src_io:
|
||||||
if _attempt_ficlone(src_io, tmp_io):
|
if _attempt_ficlone(src_io, tmp_io):
|
||||||
LOGGER.info('Reflinked file: %s -> %s', src, tmp_io.name)
|
LOGGER.info('Reflinked file: %r -> %r', src, tmp_io.name)
|
||||||
return True
|
return True
|
||||||
LOGGER.info('Copying file: %s -> %s', src, tmp_io.name)
|
LOGGER.info('Copying file: %r -> %r', src, tmp_io.name)
|
||||||
cmd = 'cp', '--sparse=always', src, tmp_io.name
|
cmd = 'cp', '--sparse=always', src, tmp_io.name
|
||||||
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||||
check=False)
|
check=False)
|
||||||
|
@ -22,12 +22,16 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import logging
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
import os
|
import os
|
||||||
|
import os.path
|
||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from contextlib import contextmanager, suppress
|
||||||
|
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
|
||||||
@ -36,6 +40,8 @@ import docutils.core
|
|||||||
import docutils.io
|
import docutils.io
|
||||||
import qubes.exc
|
import qubes.exc
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger('qubes.utils')
|
||||||
|
|
||||||
|
|
||||||
def get_timezone():
|
def get_timezone():
|
||||||
# fc18
|
# fc18
|
||||||
@ -186,6 +192,58 @@ def match_vm_name_with_special(vm, name):
|
|||||||
return name[len('@type:'):] == vm.__class__.__name__
|
return name[len('@type:'):] == vm.__class__.__name__
|
||||||
return name == vm.name
|
return name == vm.name
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def replace_file(dst, *, permissions, close_on_success=True,
|
||||||
|
logger=LOGGER, log_level=logging.DEBUG):
|
||||||
|
''' Yield a tempfile whose name starts with dst. If the block does
|
||||||
|
not raise an exception, apply permissions and persist the
|
||||||
|
tempfile to dst (which is allowed to already exist). Otherwise
|
||||||
|
ensure that the tempfile is cleaned up.
|
||||||
|
'''
|
||||||
|
tmp_dir, prefix = os.path.split(dst + '~')
|
||||||
|
tmp = tempfile.NamedTemporaryFile(dir=tmp_dir, prefix=prefix, delete=False)
|
||||||
|
try:
|
||||||
|
yield tmp
|
||||||
|
tmp.flush()
|
||||||
|
os.fchmod(tmp.fileno(), permissions)
|
||||||
|
os.fsync(tmp.fileno())
|
||||||
|
if close_on_success:
|
||||||
|
tmp.close()
|
||||||
|
rename_file(tmp.name, dst, logger=logger, log_level=log_level)
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
tmp.close()
|
||||||
|
finally:
|
||||||
|
remove_file(tmp.name, logger=logger, log_level=log_level)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def rename_file(src, dst, *, logger=LOGGER, log_level=logging.DEBUG):
|
||||||
|
''' Durably rename src to dst. '''
|
||||||
|
os.rename(src, dst)
|
||||||
|
dst_dir = os.path.dirname(dst)
|
||||||
|
src_dir = os.path.dirname(src)
|
||||||
|
fsync_path(dst_dir)
|
||||||
|
if src_dir != dst_dir:
|
||||||
|
fsync_path(src_dir)
|
||||||
|
logger.log(log_level, 'Renamed file: %r -> %r', src, dst)
|
||||||
|
|
||||||
|
def remove_file(path, *, logger=LOGGER, log_level=logging.DEBUG):
|
||||||
|
''' Durably remove the file at path, if it exists. Return whether
|
||||||
|
we removed it. '''
|
||||||
|
with suppress(FileNotFoundError):
|
||||||
|
os.remove(path)
|
||||||
|
fsync_path(os.path.dirname(path))
|
||||||
|
logger.log(log_level, 'Removed file: %r', path)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def fsync_path(path):
|
||||||
|
fd = os.open(path, os.O_RDONLY) # works for a file or a directory
|
||||||
|
try:
|
||||||
|
os.fsync(fd)
|
||||||
|
finally:
|
||||||
|
os.close(fd)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def coro_maybe(value):
|
def coro_maybe(value):
|
||||||
if asyncio.iscoroutine(value):
|
if asyncio.iscoroutine(value):
|
||||||
|
Loading…
Reference in New Issue
Block a user