def store(self, data): """ Store given data to the file system. This method is supposed to be called only before a VM is started. Contingent stale data, if present, is removed before `data` is stored. The method is not intended to be used for an atomic live data replacement and such a use is not guaranteed to work properly. :param data: encoded data as previously returned from `retrieve()` :type data: string :raises: exception.ExternalDataFailed if data could not be decoded """ byte_data = data.encode('ascii') # Remove line-ends; this is for backward compatibility with legacy # base64 methods used in oVirt 4.4.4 and could possibly be dropped the # in future byte_data = byte_data.translate(None, delete=b'\n') # Detect if the content is compressed and decompress it if yes. For # backward compatibility we don't treat it as error if content is not # compressed. # # Base64 (RFC 3548) uses the alphabet [A-Za-z0-9+/] for encoding # data and a special character '=' for padding at the end of # output. Since the '=' character cannot appear at the beginning of # base64 encoded data we use the string '=X=' to store the format of # encoded data. Currently used values of X are: # 0 bzip2 compressed data # data_format = None if len(byte_data) > 3 and byte_data[0] == ord('=') and \ byte_data[2] == ord('='): data_format = byte_data[1] byte_data = byte_data[3:] # Decode base64 error = None try: decoded_data = base64.b64decode(byte_data, validate=True) except binascii.Error as e: error = e if error is not None: raise exception.ExternalDataFailed( 'Failed to decode base64 data', exception=error) # Uncompress if data_format == ord('0'): error = None try: final_data = bz2.decompress(decoded_data) except Exception as e: error = e if error is not None: raise exception.ExternalDataFailed( 'Failed to decompress bzip2 content', exception=error) elif data_format is None: final_data = decoded_data else: raise exception.ExternalDataFailed( 'Invalid data format', data_format=data_format) self._store(final_data)
def write_tpm_data(vm_id, tpm_data): """ Write TPM data for the given VM. :param vm_id: VM id :type vm_id: string :param tpm_data: encoded TPM data as previously obtained from `read_tpm_data()` :type tpm_data: ProtectedPassword """ tpm_data = password.unprotect(tpm_data) # Permit only archives with plain files and directories to prevent various # kinds of attacks. with tempfile.TemporaryDirectory() as d: accessor = filedata.DirectoryData(os.path.join(d, 'check')) accessor.store(tpm_data) for root, dirs, files in os.walk(d): for f in files: path = os.path.join(root, f) if not os.path.isfile(path): logging.error("Special file in TPM data: %s", path) raise exception.ExternalDataFailed( reason="Cannot write TPM data with non-regular files", path=path) # OK, write the data to the target location accessor = filedata.DirectoryData(filedata.tpm_path(vm_id)) accessor.store(tpm_data)
def nvram_path(vm_id): """ Return path to NVRAM file for a VM or a path where to store a template for NVRAM of the VM. :param vm_id: VM id :type vm_id: string :returns: path to the NVRAM file :rtype: string :raises: exception.ExternalDataFailed -- if the VM id has invalid format, OSError -- when NVRAM directory cannot be created """ if _VM_ID_REGEXP.match(vm_id) is None: raise exception.ExternalDataFailed("Invalid VM id", vm_id=vm_id) if not os.path.exists(constants.P_LIBVIRT_NVRAM): # The directory is normally created by libvirt, but this may not # have happened yet. We can try to create it on our own. The # parents however should be part of libvirt RPM and if they # are missing it is not our problem. uid = pwd.getpwnam(constants.QEMU_PROCESS_USER).pw_uid gid = grp.getgrnam(constants.QEMU_PROCESS_GROUP).gr_gid os.mkdir(constants.P_LIBVIRT_NVRAM, mode=0o755) os.chown(constants.P_LIBVIRT_NVRAM, uid, gid) path = os.path.join(constants.P_LIBVIRT_NVRAM, vm_id + ".fd") return path
def _retrieve(self): with open(self._path, 'rb') as f: data = f.read() if len(data) == 0 and not self._allow_empty: raise exception.ExternalDataFailed( 'File with zero size is not allowed', path=self._path) return data
def retrieve(self, last_modified=-1): """ Retrieve and return data from the file system. If the data is not newer than `last_modified`, don't retrieve it. :param last_modified: retrieve data only when `last_modified()` returns a value newer than this one :type last_modified: float :returns: encoded data, which can be later used as a `store()` argument; None if data is unchanged :rtype: string or None :raises: `ExternalDataFailed` if the data doesn't exist """ if not self._exists(): logging.debug("Data path doesn't exist: %s", self._path) raise exception.ExternalDataFailed( reason="Data path doesn't exist", path=self._path ) currently_modified = self.last_modified() if currently_modified <= last_modified and \ last_modified <= time.time(): # last_modified in future? no! return None data = self._retrieve() data_format = '' if self._compress: # Compress data with bzip2. See the store() method for description # of the prepended string. data_format = '=0=' data = bz2.compress(data, compresslevel=9) return data_format + base64.b64encode(data).decode('ascii')
def tpm_path(vm_id): """ Return path to TPM data for a VM with the given id. :param vm_id: VM id :type vm_id: string :returns: path to the TPM data directory :rtype: string :raises: exception.ExternalDataFailed -- if the VM id has invalid format """ # vm_id is used as a subdirectory path by supervdsm, so we must be safe # here if _VM_ID_REGEXP.match(vm_id) is None: raise exception.ExternalDataFailed("Invalid VM id", vm_id=vm_id) return os.path.join(constants.P_LIBVIRT_SWTPM, vm_id)
def _update_internal(self, force=False): error = None try: new_data = self._monitor.data(force=force) except Exception as e: self._log.error("Failed to read %s data: %s", self._kind, e) if isinstance(e, exception.ExternalDataFailed): raise else: # Let's not leak data from the exception error = e if error is not None: raise exception.ExternalDataFailed( reason="Failed to read %s data" % self._kind, exception=error ) monitor_hash = self._monitor.data_hash() if new_data is None: # Data is unchanged, we can report it stable_data = self._data.current_data if stable_data is not self._data.stable_data: data = self._data._replace( stable_data=stable_data, engine_hash=ExternalData.secure_hash(stable_data) ) else: # No change at all data = self._data elif force or monitor_hash == self._data.monitor_hash: # New stable data, replace old data completely data = ExternalData.Data( stable_data=new_data, current_data=new_data, monitor_hash=monitor_hash, engine_hash=ExternalData.secure_hash(new_data) ) else: # New unstable data, store it but don't report it data = self._data._replace( current_data=new_data, monitor_hash=monitor_hash ) return data
def _retrieve(self): if len(os.listdir(self._path)) == 0 and not self._allow_empty: raise exception.ExternalDataFailed( 'Empty directory is not allowed', path=self._path) return _make_tar_archive(self._path)