cloudstack/scripts/vm/hypervisor/kvm/imageserver/backends/file.py

124 lines
3.9 KiB
Python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import os
from io import BufferedReader
from typing import Any, Dict, List, Optional
from ..constants import CHUNK_SIZE
from .base import BackendSession, ImageBackend
class FileSession(BackendSession):
"""
Holds a single file handle open for the duration of a streaming read.
Returns empty bytes at EOF (file semantics).
"""
def __init__(self, path: str):
self._path = path
self._fh: Optional[BufferedReader] = open(path, "rb")
self._size = os.path.getsize(path)
def size(self) -> int:
return self._size
def read(self, offset: int, length: int) -> bytes:
if self._fh is None:
raise RuntimeError("session is closed")
self._fh.seek(offset)
return self._fh.read(length)
def close(self) -> None:
if self._fh is not None:
self._fh.close()
self._fh = None
class FileBackend(ImageBackend):
"""
ImageBackend implementation backed by a local file (qcow2 or raw).
Supports full read/write and flush. Does not support extents or range writes.
"""
def __init__(self, file_path: str):
self._path = file_path
@property
def supports_extents(self) -> bool:
return False
@property
def supports_range_write(self) -> bool:
return False
def size(self) -> int:
return os.path.getsize(self._path)
def read(self, offset: int, length: int) -> bytes:
with open(self._path, "rb") as f:
f.seek(offset)
return f.read(length)
def write(self, data: bytes, offset: int) -> None:
raise NotImplementedError("file backend does not support range writes")
def write_full(self, stream: Any, content_length: int, flush: bool) -> int:
bytes_written = 0
remaining = content_length
with open(self._path, "wb") as f:
while remaining > 0:
chunk = stream.read(min(CHUNK_SIZE, remaining))
if not chunk:
raise IOError(
f"request body ended early at {bytes_written} bytes"
)
f.write(chunk)
bytes_written += len(chunk)
remaining -= len(chunk)
if flush:
f.flush()
os.fsync(f.fileno())
return bytes_written
def flush(self) -> None:
with open(self._path, "rb") as f:
f.flush()
os.fsync(f.fileno())
def zero(self, offset: int, length: int) -> None:
raise NotImplementedError("file backend does not support zero")
def get_capabilities(self) -> Dict[str, bool]:
return {
"read_only": False,
"can_flush": True,
"can_zero": False,
}
def get_allocation_extents(self) -> List[Dict[str, Any]]:
raise NotImplementedError("file backend does not support extents")
def get_dirty_extents(self, dirty_bitmap_context: str) -> List[Dict[str, Any]]:
raise NotImplementedError("file backend does not support extents")
def open_session(self) -> FileSession:
return FileSession(self._path)
def close(self) -> None:
pass