Merge "Implement basic libsnapshot functionality."
This commit is contained in:
commit
23a87716b5
8 changed files with 946 additions and 65 deletions
|
@ -210,6 +210,18 @@ bool DmTargetSnapshot::ParseStatusText(const std::string& text, Status* status)
|
|||
return false;
|
||||
}
|
||||
|
||||
bool DmTargetSnapshot::GetDevicesFromParams(const std::string& params, std::string* base_device,
|
||||
std::string* cow_device) {
|
||||
auto pieces = android::base::Split(params, " ");
|
||||
if (pieces.size() < 2) {
|
||||
LOG(ERROR) << "Parameter string is invalid: " << params;
|
||||
return false;
|
||||
}
|
||||
*base_device = pieces[0];
|
||||
*cow_device = pieces[1];
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string DmTargetCrypt::GetParameterString() const {
|
||||
std::vector<std::string> argv = {
|
||||
cipher_,
|
||||
|
|
|
@ -47,6 +47,8 @@ namespace dm {
|
|||
|
||||
enum class DmDeviceState { INVALID, SUSPENDED, ACTIVE };
|
||||
|
||||
static constexpr uint64_t kSectorSize = 512;
|
||||
|
||||
class DeviceMapper final {
|
||||
public:
|
||||
class DmBlockDevice final {
|
||||
|
|
|
@ -219,6 +219,8 @@ class DmTargetSnapshot final : public DmTarget {
|
|||
static double MergePercent(const Status& status, uint64_t sectors_initial = 0);
|
||||
static bool ParseStatusText(const std::string& text, Status* status);
|
||||
static bool ReportsOverflow(const std::string& target_type);
|
||||
static bool GetDevicesFromParams(const std::string& params, std::string* base_device,
|
||||
std::string* cow_device);
|
||||
|
||||
private:
|
||||
std::string base_device_;
|
||||
|
|
|
@ -14,15 +14,13 @@
|
|||
// limitations under the License.
|
||||
//
|
||||
|
||||
cc_library {
|
||||
name: "libsnapshot",
|
||||
recovery_available: true,
|
||||
cc_defaults {
|
||||
name: "libsnapshot_defaults",
|
||||
defaults: ["fs_mgr_defaults"],
|
||||
cppflags: [
|
||||
cflags: [
|
||||
"-D_FILE_OFFSET_BITS=64",
|
||||
],
|
||||
srcs: [
|
||||
"snapshot.cpp",
|
||||
"-Wall",
|
||||
"-Werror",
|
||||
],
|
||||
shared_libs: [
|
||||
"libbase",
|
||||
|
@ -30,7 +28,53 @@ cc_library {
|
|||
],
|
||||
static_libs: [
|
||||
"libdm",
|
||||
],
|
||||
whole_static_libs: [
|
||||
"libext2_uuid",
|
||||
"libext4_utils",
|
||||
"libfiemap",
|
||||
],
|
||||
export_include_dirs: ["include"],
|
||||
}
|
||||
|
||||
filegroup {
|
||||
name: "libsnapshot_sources",
|
||||
srcs: [
|
||||
"snapshot.cpp",
|
||||
],
|
||||
}
|
||||
|
||||
cc_library_static {
|
||||
name: "libsnapshot",
|
||||
defaults: ["libsnapshot_defaults"],
|
||||
srcs: [":libsnapshot_sources"],
|
||||
static_libs: [
|
||||
"libfiemap_binder",
|
||||
],
|
||||
}
|
||||
|
||||
cc_library_static {
|
||||
name: "libsnapshot_nobinder",
|
||||
defaults: ["libsnapshot_defaults"],
|
||||
srcs: [":libsnapshot_sources"],
|
||||
recovery_available: true,
|
||||
}
|
||||
|
||||
cc_test {
|
||||
name: "libsnapshot_test",
|
||||
defaults: ["libsnapshot_defaults"],
|
||||
srcs: [
|
||||
"snapshot_test.cpp",
|
||||
],
|
||||
shared_libs: [
|
||||
"libbinder",
|
||||
"libutils",
|
||||
],
|
||||
static_libs: [
|
||||
"libcutils",
|
||||
"libcrypto",
|
||||
"libfs_mgr",
|
||||
"liblp",
|
||||
"libsnapshot",
|
||||
],
|
||||
}
|
||||
|
|
|
@ -19,14 +19,28 @@
|
|||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <android-base/unique_fd.h>
|
||||
#include <libdm/dm_target.h>
|
||||
#include <libfiemap/image_manager.h>
|
||||
|
||||
#ifndef FRIEND_TEST
|
||||
#define FRIEND_TEST(test_set_name, individual_test) \
|
||||
friend class test_set_name##_##individual_test##_Test
|
||||
#define DEFINED_FRIEND_TEST
|
||||
#endif
|
||||
|
||||
namespace android {
|
||||
namespace snapshot {
|
||||
|
||||
enum class UpdateStatus {
|
||||
enum class UpdateState {
|
||||
// No update or merge is in progress.
|
||||
None,
|
||||
|
||||
// An update is applying; snapshots may already exist.
|
||||
Initiated,
|
||||
|
||||
// An update is pending, but has not been successfully booted yet.
|
||||
Unverified,
|
||||
|
||||
|
@ -34,36 +48,39 @@ enum class UpdateStatus {
|
|||
Merging,
|
||||
|
||||
// Merging is complete, and needs to be acknowledged.
|
||||
MergeCompleted
|
||||
MergeCompleted,
|
||||
|
||||
// Merging failed due to an unrecoverable error.
|
||||
MergeFailed
|
||||
};
|
||||
|
||||
class SnapshotManager final {
|
||||
public:
|
||||
// Return a new SnapshotManager instance, or null on error.
|
||||
static std::unique_ptr<SnapshotManager> New();
|
||||
// Dependency injection for testing.
|
||||
class IDeviceInfo {
|
||||
public:
|
||||
virtual ~IDeviceInfo() {}
|
||||
virtual std::string GetGsidDir() const = 0;
|
||||
virtual std::string GetMetadataDir() const = 0;
|
||||
|
||||
// Create a new snapshot device with the given name, base device, and COW device
|
||||
// size. The new device path will be returned in |dev_path|. If timeout_ms is
|
||||
// greater than zero, this function will wait the given amount of time for
|
||||
// |dev_path| to become available, and fail otherwise. If timeout_ms is 0, then
|
||||
// no wait will occur and |dev_path| may not yet exist on return.
|
||||
bool CreateSnapshot(const std::string& name, const std::string& base_device, uint64_t cow_size,
|
||||
std::string* dev_path, const std::chrono::milliseconds& timeout_ms);
|
||||
// Return true if the device is currently running off snapshot devices,
|
||||
// indicating that we have booted after applying (but not merging) an
|
||||
// OTA.
|
||||
virtual bool IsRunningSnapshot() const = 0;
|
||||
};
|
||||
|
||||
// Map a snapshot device that was previously created with CreateSnapshot.
|
||||
// If a merge was previously initiated, the device-mapper table will have a
|
||||
// snapshot-merge target instead of a snapshot target. The timeout parameter
|
||||
// is the same as in CreateSnapshotDevice.
|
||||
bool MapSnapshotDevice(const std::string& name, const std::string& base_device,
|
||||
const std::chrono::milliseconds& timeout_ms, std::string* dev_path);
|
||||
// Return a new SnapshotManager instance, or null on error. The device
|
||||
// pointer is owned for the lifetime of SnapshotManager. If null, a default
|
||||
// instance will be created.
|
||||
static std::unique_ptr<SnapshotManager> New(IDeviceInfo* device = nullptr);
|
||||
|
||||
// Unmap a snapshot device previously mapped with MapSnapshotDevice().
|
||||
bool UnmapSnapshotDevice(const std::string& name);
|
||||
// Begin an update. This must be called before creating any snapshots. It
|
||||
// will fail if GetUpdateState() != None.
|
||||
bool BeginUpdate();
|
||||
|
||||
// Remove the backing copy-on-write image for the named snapshot. If the
|
||||
// device is still mapped, this will attempt an Unmap, and fail if the
|
||||
// unmap fails.
|
||||
bool DeleteSnapshot(const std::string& name);
|
||||
// Cancel an update; any snapshots will be deleted. This will fail if the
|
||||
// state != Initiated or None.
|
||||
bool CancelUpdate();
|
||||
|
||||
// Initiate a merge on all snapshot devices. This should only be used after an
|
||||
// update has been marked successful after booting.
|
||||
|
@ -77,12 +94,129 @@ class SnapshotManager final {
|
|||
// Find the status of the current update, if any.
|
||||
//
|
||||
// |progress| depends on the returned status:
|
||||
// None: 0
|
||||
// Unverified: 0
|
||||
// Merging: Value in the range [0, 100)
|
||||
// Merging: Value in the range [0, 100]
|
||||
// MergeCompleted: 100
|
||||
UpdateStatus GetUpdateStatus(double* progress);
|
||||
// Other: 0
|
||||
UpdateState GetUpdateState(double* progress = nullptr);
|
||||
|
||||
private:
|
||||
FRIEND_TEST(SnapshotTest, CreateSnapshot);
|
||||
FRIEND_TEST(SnapshotTest, MapSnapshot);
|
||||
FRIEND_TEST(SnapshotTest, MapPartialSnapshot);
|
||||
friend class SnapshotTest;
|
||||
|
||||
using IImageManager = android::fiemap::IImageManager;
|
||||
|
||||
explicit SnapshotManager(IDeviceInfo* info);
|
||||
|
||||
// This is created lazily since it connects via binder.
|
||||
bool EnsureImageManager();
|
||||
|
||||
// Helper function for tests.
|
||||
IImageManager* image_manager() const { return images_.get(); }
|
||||
|
||||
// Since libsnapshot is included into multiple processes, we flock() our
|
||||
// files for simple synchronization. LockedFile is a helper to assist with
|
||||
// this. It also serves as a proof-of-lock for some functions.
|
||||
class LockedFile final {
|
||||
public:
|
||||
LockedFile(const std::string& path, android::base::unique_fd&& fd)
|
||||
: path_(path), fd_(std::move(fd)) {}
|
||||
~LockedFile();
|
||||
|
||||
const std::string& path() const { return path_; }
|
||||
int fd() const { return fd_; }
|
||||
|
||||
private:
|
||||
std::string path_;
|
||||
android::base::unique_fd fd_;
|
||||
};
|
||||
std::unique_ptr<LockedFile> OpenFile(const std::string& file, int open_flags, int lock_flags);
|
||||
bool Truncate(LockedFile* file);
|
||||
|
||||
// Create a new snapshot record. This creates the backing COW store and
|
||||
// persists information needed to map the device. The device can be mapped
|
||||
// with MapSnapshot().
|
||||
//
|
||||
// |device_size| should be the size of the base_device that will be passed
|
||||
// via MapDevice(). |snapshot_size| should be the number of bytes in the
|
||||
// base device, starting from 0, that will be snapshotted. The cow_size
|
||||
// should be the amount of space that will be allocated to store snapshot
|
||||
// deltas.
|
||||
//
|
||||
// If |snapshot_size| < device_size, then the device will always
|
||||
// be mapped with two table entries: a dm-snapshot range covering
|
||||
// snapshot_size, and a dm-linear range covering the remainder.
|
||||
//
|
||||
// All sizes are specified in bytes, and the device and snapshot sizes
|
||||
// must be a multiple of the sector size (512 bytes). |cow_size| will
|
||||
// be rounded up to the nearest sector.
|
||||
bool CreateSnapshot(LockedFile* lock, const std::string& name, uint64_t device_size,
|
||||
uint64_t snapshot_size, uint64_t cow_size);
|
||||
|
||||
// Map a snapshot device that was previously created with CreateSnapshot.
|
||||
// If a merge was previously initiated, the device-mapper table will have a
|
||||
// snapshot-merge target instead of a snapshot target. If the timeout
|
||||
// parameter greater than zero, this function will wait the given amount
|
||||
// of time for |dev_path| to become available, and fail otherwise. If
|
||||
// timeout_ms is 0, then no wait will occur and |dev_path| may not yet
|
||||
// exist on return.
|
||||
bool MapSnapshot(LockedFile* lock, const std::string& name, const std::string& base_device,
|
||||
const std::chrono::milliseconds& timeout_ms, std::string* dev_path);
|
||||
|
||||
// Remove the backing copy-on-write image for the named snapshot. If the
|
||||
// device is still mapped, this will attempt an Unmap, and fail if the
|
||||
// unmap fails.
|
||||
bool DeleteSnapshot(LockedFile* lock, const std::string& name);
|
||||
|
||||
// Unmap a snapshot device previously mapped with MapSnapshotDevice().
|
||||
bool UnmapSnapshot(LockedFile* lock, const std::string& name);
|
||||
|
||||
// Unmap and remove all known snapshots.
|
||||
bool RemoveAllSnapshots(LockedFile* lock);
|
||||
|
||||
// List the known snapshot names.
|
||||
bool ListSnapshots(LockedFile* lock, std::vector<std::string>* snapshots);
|
||||
|
||||
// Interact with /metadata/ota/state.
|
||||
std::unique_ptr<LockedFile> OpenStateFile(int open_flags, int lock_flags);
|
||||
std::unique_ptr<LockedFile> LockShared();
|
||||
std::unique_ptr<LockedFile> LockExclusive();
|
||||
UpdateState ReadUpdateState(LockedFile* file);
|
||||
bool WriteUpdateState(LockedFile* file, UpdateState state);
|
||||
|
||||
// This state is persisted per-snapshot in /metadata/ota/snapshots/.
|
||||
struct SnapshotStatus {
|
||||
std::string state;
|
||||
uint64_t device_size;
|
||||
uint64_t snapshot_size;
|
||||
// These are non-zero when merging.
|
||||
uint64_t sectors_allocated = 0;
|
||||
uint64_t metadata_sectors = 0;
|
||||
};
|
||||
|
||||
// Interact with status files under /metadata/ota/snapshots.
|
||||
std::unique_ptr<LockedFile> OpenSnapshotStatusFile(const std::string& name, int open_flags,
|
||||
int lock_flags);
|
||||
bool WriteSnapshotStatus(LockedFile* file, const SnapshotStatus& status);
|
||||
bool ReadSnapshotStatus(LockedFile* file, SnapshotStatus* status);
|
||||
|
||||
// Return the name of the device holding the "snapshot" or "snapshot-merge"
|
||||
// target. This may not be the final device presented via MapSnapshot(), if
|
||||
// for example there is a linear segment.
|
||||
std::string GetSnapshotDeviceName(const std::string& snapshot_name,
|
||||
const SnapshotStatus& status);
|
||||
|
||||
std::string gsid_dir_;
|
||||
std::string metadata_dir_;
|
||||
std::unique_ptr<IDeviceInfo> device_;
|
||||
std::unique_ptr<IImageManager> images_;
|
||||
};
|
||||
|
||||
} // namespace snapshot
|
||||
} // namespace android
|
||||
|
||||
#ifdef DEFINED_FRIEND_TEST
|
||||
#undef DEFINED_FRIEND_TEST
|
||||
#undef FRIEND_TEST
|
||||
#endif
|
||||
|
|
|
@ -14,45 +14,309 @@
|
|||
|
||||
#include <libsnapshot/snapshot.h>
|
||||
|
||||
#include <dirent.h>
|
||||
#include <sys/file.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/unistd.h>
|
||||
|
||||
#include <android-base/file.h>
|
||||
#include <android-base/logging.h>
|
||||
#include <android-base/parseint.h>
|
||||
#include <android-base/strings.h>
|
||||
#include <android-base/unique_fd.h>
|
||||
#include <ext4_utils/ext4_utils.h>
|
||||
#include <libdm/dm.h>
|
||||
|
||||
namespace android {
|
||||
namespace snapshot {
|
||||
|
||||
std::unique_ptr<SnapshotManager> SnapshotManager::New() {
|
||||
return std::make_unique<SnapshotManager>();
|
||||
using android::base::unique_fd;
|
||||
using android::dm::DeviceMapper;
|
||||
using android::dm::DmDeviceState;
|
||||
using android::dm::DmTable;
|
||||
using android::dm::DmTargetLinear;
|
||||
using android::dm::DmTargetSnapshot;
|
||||
using android::dm::kSectorSize;
|
||||
using android::dm::SnapshotStorageMode;
|
||||
using android::fiemap::IImageManager;
|
||||
using namespace std::chrono_literals;
|
||||
using namespace std::string_literals;
|
||||
|
||||
// Unit is sectors, this is a 4K chunk.
|
||||
static constexpr uint32_t kSnapshotChunkSize = 8;
|
||||
|
||||
class DeviceInfo final : public SnapshotManager::IDeviceInfo {
|
||||
public:
|
||||
std::string GetGsidDir() const override { return "ota"s; }
|
||||
std::string GetMetadataDir() const override { return "/metadata/ota/test"s; }
|
||||
bool IsRunningSnapshot() const override;
|
||||
};
|
||||
|
||||
bool DeviceInfo::IsRunningSnapshot() const {
|
||||
// :TODO: implement this check.
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SnapshotManager::CreateSnapshot(const std::string& name, const std::string& base_device,
|
||||
uint64_t cow_size, std::string* dev_path,
|
||||
const std::chrono::milliseconds& timeout_ms) {
|
||||
// (1) Create COW device using libgsi_image.
|
||||
// (2) Create snapshot device using libdm + DmTargetSnapshot.
|
||||
// (3) Record partition in /metadata/ota.
|
||||
(void)name;
|
||||
(void)base_device;
|
||||
(void)cow_size;
|
||||
(void)dev_path;
|
||||
(void)timeout_ms;
|
||||
return false;
|
||||
std::unique_ptr<SnapshotManager> SnapshotManager::New(IDeviceInfo* info) {
|
||||
if (!info) {
|
||||
info = new DeviceInfo();
|
||||
}
|
||||
return std::unique_ptr<SnapshotManager>(new SnapshotManager(info));
|
||||
}
|
||||
|
||||
bool SnapshotManager::MapSnapshotDevice(const std::string& name, const std::string& base_device,
|
||||
const std::chrono::milliseconds& timeout_ms,
|
||||
std::string* dev_path) {
|
||||
(void)name;
|
||||
(void)base_device;
|
||||
(void)dev_path;
|
||||
(void)timeout_ms;
|
||||
return false;
|
||||
SnapshotManager::SnapshotManager(IDeviceInfo* device) : device_(device) {
|
||||
gsid_dir_ = device_->GetGsidDir();
|
||||
metadata_dir_ = device_->GetMetadataDir();
|
||||
}
|
||||
|
||||
bool SnapshotManager::UnmapSnapshotDevice(const std::string& name) {
|
||||
(void)name;
|
||||
return false;
|
||||
static std::string GetCowName(const std::string& snapshot_name) {
|
||||
return snapshot_name + "-cow";
|
||||
}
|
||||
|
||||
bool SnapshotManager::DeleteSnapshot(const std::string& name) {
|
||||
(void)name;
|
||||
return false;
|
||||
bool SnapshotManager::BeginUpdate() {
|
||||
auto file = LockExclusive();
|
||||
if (!file) return false;
|
||||
|
||||
auto state = ReadUpdateState(file.get());
|
||||
if (state != UpdateState::None) {
|
||||
LOG(ERROR) << "An update is already in progress, cannot begin a new update";
|
||||
return false;
|
||||
}
|
||||
return WriteUpdateState(file.get(), UpdateState::Initiated);
|
||||
}
|
||||
|
||||
bool SnapshotManager::CancelUpdate() {
|
||||
auto file = LockExclusive();
|
||||
if (!file) return false;
|
||||
|
||||
UpdateState state = ReadUpdateState(file.get());
|
||||
if (state == UpdateState::None) return true;
|
||||
if (state != UpdateState::Initiated) {
|
||||
LOG(ERROR) << "Cannot cancel update after it has completed or started merging";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!RemoveAllSnapshots(file.get())) {
|
||||
LOG(ERROR) << "Could not remove all snapshots";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!WriteUpdateState(file.get(), UpdateState::None)) {
|
||||
LOG(ERROR) << "Could not write new update state";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SnapshotManager::CreateSnapshot(LockedFile* lock, const std::string& name,
|
||||
uint64_t device_size, uint64_t snapshot_size,
|
||||
uint64_t cow_size) {
|
||||
CHECK(lock);
|
||||
if (!EnsureImageManager()) return false;
|
||||
|
||||
// Sanity check these sizes. Like liblp, we guarantee the partition size
|
||||
// is respected, which means it has to be sector-aligned. (This guarantee
|
||||
// is useful for locating avb footers correctly). The COW size, however,
|
||||
// can be arbitrarily larger than specified, so we can safely round it up.
|
||||
if (device_size % kSectorSize != 0) {
|
||||
LOG(ERROR) << "Snapshot " << name
|
||||
<< " device size is not a multiple of the sector size: " << device_size;
|
||||
return false;
|
||||
}
|
||||
if (snapshot_size % kSectorSize != 0) {
|
||||
LOG(ERROR) << "Snapshot " << name
|
||||
<< " snapshot size is not a multiple of the sector size: " << snapshot_size;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Round the COW size up to the nearest sector.
|
||||
cow_size += kSectorSize - 1;
|
||||
cow_size &= ~(kSectorSize - 1);
|
||||
|
||||
LOG(INFO) << "Snapshot " << name << " will have COW size " << cow_size;
|
||||
|
||||
auto status_file = OpenSnapshotStatusFile(name, O_RDWR | O_CREAT, LOCK_EX);
|
||||
if (!status_file) return false;
|
||||
|
||||
// Note, we leave the status file hanging around if we fail to create the
|
||||
// actual backing image. This is harmless, since it'll get removed when
|
||||
// CancelUpdate is called.
|
||||
SnapshotStatus status = {
|
||||
.state = "created",
|
||||
.device_size = device_size,
|
||||
.snapshot_size = snapshot_size,
|
||||
};
|
||||
if (!WriteSnapshotStatus(status_file.get(), status)) {
|
||||
PLOG(ERROR) << "Could not write snapshot status: " << name;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto cow_name = GetCowName(name);
|
||||
int cow_flags = IImageManager::CREATE_IMAGE_ZERO_FILL;
|
||||
return images_->CreateBackingImage(cow_name, cow_size, cow_flags);
|
||||
}
|
||||
|
||||
bool SnapshotManager::MapSnapshot(LockedFile* lock, const std::string& name,
|
||||
const std::string& base_device,
|
||||
const std::chrono::milliseconds& timeout_ms,
|
||||
std::string* dev_path) {
|
||||
CHECK(lock);
|
||||
if (!EnsureImageManager()) return false;
|
||||
|
||||
auto status_file = OpenSnapshotStatusFile(name, O_RDWR, LOCK_EX);
|
||||
if (!status_file) return false;
|
||||
|
||||
SnapshotStatus status;
|
||||
if (!ReadSnapshotStatus(status_file.get(), &status)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate the block device size, as well as the requested snapshot size.
|
||||
// During this we also compute the linear sector region if any.
|
||||
{
|
||||
unique_fd fd(open(base_device.c_str(), O_RDONLY | O_CLOEXEC));
|
||||
if (fd < 0) {
|
||||
PLOG(ERROR) << "open failed: " << base_device;
|
||||
return false;
|
||||
}
|
||||
auto dev_size = get_block_device_size(fd);
|
||||
if (!dev_size) {
|
||||
PLOG(ERROR) << "Could not determine block device size: " << base_device;
|
||||
return false;
|
||||
}
|
||||
if (status.device_size != dev_size) {
|
||||
LOG(ERROR) << "Block device size for " << base_device << " does not match"
|
||||
<< "(expected " << status.device_size << ", got " << dev_size << ")";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (status.device_size % kSectorSize != 0) {
|
||||
LOG(ERROR) << "invalid blockdev size for " << base_device << ": " << status.device_size;
|
||||
return false;
|
||||
}
|
||||
if (status.snapshot_size % kSectorSize != 0 || status.snapshot_size > status.device_size) {
|
||||
LOG(ERROR) << "Invalid snapshot size for " << base_device << ": " << status.snapshot_size;
|
||||
return false;
|
||||
}
|
||||
uint64_t snapshot_sectors = status.snapshot_size / kSectorSize;
|
||||
uint64_t linear_sectors = (status.device_size - status.snapshot_size) / kSectorSize;
|
||||
|
||||
auto cow_name = GetCowName(name);
|
||||
|
||||
std::string cow_dev;
|
||||
if (!images_->MapImageDevice(cow_name, timeout_ms, &cow_dev)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& dm = DeviceMapper::Instance();
|
||||
|
||||
// Merging is a global state, not per-snapshot. We do however track the
|
||||
// progress of individual snapshots' merges.
|
||||
SnapshotStorageMode mode;
|
||||
UpdateState update_state = ReadUpdateState(lock);
|
||||
if (update_state == UpdateState::Merging || update_state == UpdateState::MergeCompleted) {
|
||||
mode = SnapshotStorageMode::Merge;
|
||||
} else {
|
||||
mode = SnapshotStorageMode::Persistent;
|
||||
}
|
||||
|
||||
// The kernel (tested on 4.19) crashes horribly if a device has both a snapshot
|
||||
// and a linear target in the same table. Instead, we stack them, and give the
|
||||
// snapshot device a different name. It is not exposed to the caller in this
|
||||
// case.
|
||||
auto snap_name = (linear_sectors > 0) ? name + "-inner" : name;
|
||||
|
||||
DmTable table;
|
||||
table.Emplace<DmTargetSnapshot>(0, snapshot_sectors, base_device, cow_dev, mode,
|
||||
kSnapshotChunkSize);
|
||||
if (!dm.CreateDevice(snap_name, table, dev_path, timeout_ms)) {
|
||||
LOG(ERROR) << "Could not create snapshot device: " << snap_name;
|
||||
images_->UnmapImageDevice(cow_name);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (linear_sectors) {
|
||||
// Our stacking will looks like this:
|
||||
// [linear, linear] ; to snapshot, and non-snapshot region of base device
|
||||
// [snapshot-inner]
|
||||
// [base device] [cow]
|
||||
DmTable table;
|
||||
table.Emplace<DmTargetLinear>(0, snapshot_sectors, *dev_path, 0);
|
||||
table.Emplace<DmTargetLinear>(snapshot_sectors, linear_sectors, base_device,
|
||||
snapshot_sectors);
|
||||
if (!dm.CreateDevice(name, table, dev_path, timeout_ms)) {
|
||||
LOG(ERROR) << "Could not create outer snapshot device: " << name;
|
||||
dm.DeleteDevice(snap_name);
|
||||
images_->UnmapImageDevice(cow_name);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// :TODO: when merging is implemented, we need to add an argument to the
|
||||
// status indicating how much progress is left to merge. (device-mapper
|
||||
// does not retain the initial values, so we can't derive them.)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SnapshotManager::UnmapSnapshot(LockedFile* lock, const std::string& name) {
|
||||
CHECK(lock);
|
||||
if (!EnsureImageManager()) return false;
|
||||
|
||||
auto status_file = OpenSnapshotStatusFile(name, O_RDWR, LOCK_EX);
|
||||
if (!status_file) return false;
|
||||
|
||||
SnapshotStatus status;
|
||||
if (!ReadSnapshotStatus(status_file.get(), &status)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& dm = DeviceMapper::Instance();
|
||||
if (dm.GetState(name) != DmDeviceState::INVALID && !dm.DeleteDevice(name)) {
|
||||
LOG(ERROR) << "Could not delete snapshot device: " << name;
|
||||
return false;
|
||||
}
|
||||
|
||||
// There may be an extra device, since the kernel doesn't let us have a
|
||||
// snapshot and linear target in the same table.
|
||||
auto dm_name = GetSnapshotDeviceName(name, status);
|
||||
if (name != dm_name && !dm.DeleteDevice(dm_name)) {
|
||||
LOG(ERROR) << "Could not delete inner snapshot device: " << dm_name;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto cow_name = GetCowName(name);
|
||||
if (images_->IsImageMapped(cow_name) && !images_->UnmapImageDevice(cow_name)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SnapshotManager::DeleteSnapshot(LockedFile* lock, const std::string& name) {
|
||||
CHECK(lock);
|
||||
if (!EnsureImageManager()) return false;
|
||||
|
||||
if (!UnmapSnapshot(lock, name)) {
|
||||
LOG(ERROR) << "Snapshot could not be unmapped for deletion: " << name;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Take the snapshot's lock after Unmap, since it will also try to lock.
|
||||
auto status_file = OpenSnapshotStatusFile(name, O_RDONLY, LOCK_EX);
|
||||
if (!status_file) return false;
|
||||
|
||||
auto cow_name = GetCowName(name);
|
||||
if (!images_->BackingImageExists(cow_name)) {
|
||||
return true;
|
||||
}
|
||||
if (!images_->DeleteBackingImage(cow_name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!android::base::RemoveFileIfExists(status_file->path())) {
|
||||
LOG(ERROR) << "Failed to remove status file: " << status_file->path();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SnapshotManager::InitiateMerge() {
|
||||
|
@ -63,9 +327,242 @@ bool SnapshotManager::WaitForMerge() {
|
|||
return false;
|
||||
}
|
||||
|
||||
UpdateStatus SnapshotManager::GetUpdateStatus(double* progress) {
|
||||
*progress = 0.0f;
|
||||
return UpdateStatus::None;
|
||||
bool SnapshotManager::RemoveAllSnapshots(LockedFile* lock) {
|
||||
std::vector<std::string> snapshots;
|
||||
if (!ListSnapshots(lock, &snapshots)) {
|
||||
LOG(ERROR) << "Could not list snapshots";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = true;
|
||||
for (const auto& name : snapshots) {
|
||||
ok &= DeleteSnapshot(lock, name);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
UpdateState SnapshotManager::GetUpdateState(double* progress) {
|
||||
auto file = LockShared();
|
||||
if (!file) {
|
||||
return UpdateState::None;
|
||||
}
|
||||
|
||||
auto state = ReadUpdateState(file.get());
|
||||
if (progress) {
|
||||
*progress = 0.0;
|
||||
if (state == UpdateState::Merging) {
|
||||
// :TODO: When merging is implemented, set progress_val.
|
||||
} else if (state == UpdateState::MergeCompleted) {
|
||||
*progress = 100.0;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
bool SnapshotManager::ListSnapshots(LockedFile* lock, std::vector<std::string>* snapshots) {
|
||||
CHECK(lock);
|
||||
|
||||
auto dir_path = metadata_dir_ + "/snapshots"s;
|
||||
std::unique_ptr<DIR, decltype(&closedir)> dir(opendir(dir_path.c_str()), closedir);
|
||||
if (!dir) {
|
||||
PLOG(ERROR) << "opendir failed: " << dir_path;
|
||||
return false;
|
||||
}
|
||||
|
||||
struct dirent* dp;
|
||||
while ((dp = readdir(dir.get())) != nullptr) {
|
||||
if (dp->d_type != DT_REG) continue;
|
||||
snapshots->emplace_back(dp->d_name);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
auto SnapshotManager::OpenFile(const std::string& file, int open_flags, int lock_flags)
|
||||
-> std::unique_ptr<LockedFile> {
|
||||
unique_fd fd(open(file.c_str(), open_flags | O_CLOEXEC | O_NOFOLLOW | O_SYNC, 0660));
|
||||
if (fd < 0) {
|
||||
PLOG(ERROR) << "Open failed: " << file;
|
||||
return nullptr;
|
||||
}
|
||||
if (flock(fd, lock_flags) < 0) {
|
||||
PLOG(ERROR) << "Acquire flock failed: " << file;
|
||||
return nullptr;
|
||||
}
|
||||
return std::make_unique<LockedFile>(file, std::move(fd));
|
||||
}
|
||||
|
||||
SnapshotManager::LockedFile::~LockedFile() {
|
||||
if (flock(fd_, LOCK_UN) < 0) {
|
||||
PLOG(ERROR) << "Failed to unlock file: " << path_;
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<SnapshotManager::LockedFile> SnapshotManager::OpenStateFile(int open_flags,
|
||||
int lock_flags) {
|
||||
auto state_file = metadata_dir_ + "/state"s;
|
||||
return OpenFile(state_file, open_flags, lock_flags);
|
||||
}
|
||||
|
||||
std::unique_ptr<SnapshotManager::LockedFile> SnapshotManager::LockShared() {
|
||||
return OpenStateFile(O_RDONLY, LOCK_SH);
|
||||
}
|
||||
|
||||
std::unique_ptr<SnapshotManager::LockedFile> SnapshotManager::LockExclusive() {
|
||||
return OpenStateFile(O_RDWR | O_CREAT, LOCK_EX);
|
||||
}
|
||||
|
||||
UpdateState SnapshotManager::ReadUpdateState(LockedFile* file) {
|
||||
// Reset position since some calls read+write.
|
||||
if (lseek(file->fd(), 0, SEEK_SET) < 0) {
|
||||
PLOG(ERROR) << "lseek state file failed";
|
||||
return UpdateState::None;
|
||||
}
|
||||
|
||||
std::string contents;
|
||||
if (!android::base::ReadFdToString(file->fd(), &contents)) {
|
||||
PLOG(ERROR) << "Read state file failed";
|
||||
return UpdateState::None;
|
||||
}
|
||||
|
||||
if (contents.empty() || contents == "none") {
|
||||
return UpdateState::None;
|
||||
} else if (contents == "initiated") {
|
||||
return UpdateState::Initiated;
|
||||
} else if (contents == "unverified") {
|
||||
return UpdateState::Unverified;
|
||||
} else if (contents == "merging") {
|
||||
return UpdateState::Merging;
|
||||
} else if (contents == "merge-completed") {
|
||||
return UpdateState::MergeCompleted;
|
||||
} else {
|
||||
LOG(ERROR) << "Unknown merge state in update state file";
|
||||
return UpdateState::None;
|
||||
}
|
||||
}
|
||||
|
||||
bool SnapshotManager::WriteUpdateState(LockedFile* file, UpdateState state) {
|
||||
std::string contents;
|
||||
switch (state) {
|
||||
case UpdateState::None:
|
||||
contents = "none";
|
||||
break;
|
||||
case UpdateState::Initiated:
|
||||
contents = "initiated";
|
||||
break;
|
||||
case UpdateState::Unverified:
|
||||
contents = "unverified";
|
||||
break;
|
||||
case UpdateState::Merging:
|
||||
contents = "merging";
|
||||
break;
|
||||
case UpdateState::MergeCompleted:
|
||||
contents = "merge-completed";
|
||||
break;
|
||||
default:
|
||||
LOG(ERROR) << "Unknown update state";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Truncate(file)) return false;
|
||||
if (!android::base::WriteStringToFd(contents, file->fd())) {
|
||||
PLOG(ERROR) << "Could not write to state file";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
auto SnapshotManager::OpenSnapshotStatusFile(const std::string& name, int open_flags,
|
||||
int lock_flags) -> std::unique_ptr<LockedFile> {
|
||||
auto file = metadata_dir_ + "/snapshots/"s + name;
|
||||
return OpenFile(file, open_flags, lock_flags);
|
||||
}
|
||||
|
||||
bool SnapshotManager::ReadSnapshotStatus(LockedFile* file, SnapshotStatus* status) {
|
||||
// Reset position since some calls read+write.
|
||||
if (lseek(file->fd(), 0, SEEK_SET) < 0) {
|
||||
PLOG(ERROR) << "lseek status file failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string contents;
|
||||
if (!android::base::ReadFdToString(file->fd(), &contents)) {
|
||||
PLOG(ERROR) << "read status file failed";
|
||||
return false;
|
||||
}
|
||||
auto pieces = android::base::Split(contents, " ");
|
||||
if (pieces.size() != 5) {
|
||||
LOG(ERROR) << "Invalid status line for snapshot: " << file->path();
|
||||
return false;
|
||||
}
|
||||
|
||||
status->state = pieces[0];
|
||||
if (!android::base::ParseUint(pieces[1], &status->device_size)) {
|
||||
LOG(ERROR) << "Invalid device size in status line for: " << file->path();
|
||||
return false;
|
||||
}
|
||||
if (!android::base::ParseUint(pieces[2], &status->snapshot_size)) {
|
||||
LOG(ERROR) << "Invalid snapshot size in status line for: " << file->path();
|
||||
return false;
|
||||
}
|
||||
if (!android::base::ParseUint(pieces[3], &status->sectors_allocated)) {
|
||||
LOG(ERROR) << "Invalid snapshot size in status line for: " << file->path();
|
||||
return false;
|
||||
}
|
||||
if (!android::base::ParseUint(pieces[4], &status->metadata_sectors)) {
|
||||
LOG(ERROR) << "Invalid snapshot size in status line for: " << file->path();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SnapshotManager::WriteSnapshotStatus(LockedFile* file, const SnapshotStatus& status) {
|
||||
std::vector<std::string> pieces = {
|
||||
status.state,
|
||||
std::to_string(status.device_size),
|
||||
std::to_string(status.snapshot_size),
|
||||
std::to_string(status.sectors_allocated),
|
||||
std::to_string(status.metadata_sectors),
|
||||
};
|
||||
auto contents = android::base::Join(pieces, " ");
|
||||
|
||||
if (!Truncate(file)) return false;
|
||||
if (!android::base::WriteStringToFd(contents, file->fd())) {
|
||||
PLOG(ERROR) << "write to status file failed: " << file->path();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SnapshotManager::Truncate(LockedFile* file) {
|
||||
if (lseek(file->fd(), 0, SEEK_SET) < 0) {
|
||||
PLOG(ERROR) << "lseek file failed: " << file->path();
|
||||
return false;
|
||||
}
|
||||
if (ftruncate(file->fd(), 0) < 0) {
|
||||
PLOG(ERROR) << "truncate failed: " << file->path();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string SnapshotManager::GetSnapshotDeviceName(const std::string& snapshot_name,
|
||||
const SnapshotStatus& status) {
|
||||
if (status.device_size != status.snapshot_size) {
|
||||
return snapshot_name + "-inner";
|
||||
}
|
||||
return snapshot_name;
|
||||
}
|
||||
|
||||
bool SnapshotManager::EnsureImageManager() {
|
||||
if (images_) return true;
|
||||
|
||||
// For now, use a preset timeout.
|
||||
images_ = android::fiemap::IImageManager::Open(gsid_dir_, 15000ms);
|
||||
if (!images_) {
|
||||
LOG(ERROR) << "Could not open ImageManager";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace snapshot
|
||||
|
|
189
fs_mgr/libsnapshot/snapshot_test.cpp
Normal file
189
fs_mgr/libsnapshot/snapshot_test.cpp
Normal file
|
@ -0,0 +1,189 @@
|
|||
// Copyright (C) 2018 The Android Open Source Project
|
||||
//
|
||||
// Licensed 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.
|
||||
|
||||
#include <libsnapshot/snapshot.h>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/file.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
|
||||
#include <android-base/strings.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace android {
|
||||
namespace snapshot {
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace std::string_literals;
|
||||
|
||||
class TestDeviceInfo : public SnapshotManager::IDeviceInfo {
|
||||
public:
|
||||
std::string GetGsidDir() const override { return "ota/test"s; }
|
||||
std::string GetMetadataDir() const override { return "/metadata/ota/test"s; }
|
||||
bool IsRunningSnapshot() const override { return is_running_snapshot_; }
|
||||
|
||||
void set_is_running_snapshot(bool value) { is_running_snapshot_ = value; }
|
||||
|
||||
private:
|
||||
bool is_running_snapshot_;
|
||||
};
|
||||
|
||||
std::unique_ptr<SnapshotManager> sm;
|
||||
TestDeviceInfo* test_device = nullptr;
|
||||
|
||||
class SnapshotTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
test_device->set_is_running_snapshot(false);
|
||||
|
||||
if (sm->GetUpdateState() != UpdateState::None) {
|
||||
ASSERT_TRUE(sm->CancelUpdate());
|
||||
}
|
||||
ASSERT_TRUE(sm->BeginUpdate());
|
||||
ASSERT_TRUE(sm->EnsureImageManager());
|
||||
|
||||
image_manager_ = sm->image_manager();
|
||||
ASSERT_NE(image_manager_, nullptr);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
lock_ = nullptr;
|
||||
|
||||
if (sm->GetUpdateState() != UpdateState::None) {
|
||||
ASSERT_TRUE(sm->CancelUpdate());
|
||||
}
|
||||
for (const auto& temp_image : temp_images_) {
|
||||
image_manager_->UnmapImageDevice(temp_image);
|
||||
image_manager_->DeleteBackingImage(temp_image);
|
||||
}
|
||||
}
|
||||
|
||||
bool AcquireLock() {
|
||||
lock_ = sm->OpenStateFile(O_RDWR, LOCK_EX);
|
||||
return !!lock_;
|
||||
}
|
||||
|
||||
bool CreateTempDevice(const std::string& name, uint64_t size, std::string* path) {
|
||||
if (!image_manager_->CreateBackingImage(name, size, false)) {
|
||||
return false;
|
||||
}
|
||||
temp_images_.emplace_back(name);
|
||||
return image_manager_->MapImageDevice(name, 10s, path);
|
||||
}
|
||||
|
||||
std::unique_ptr<SnapshotManager::LockedFile> lock_;
|
||||
std::vector<std::string> temp_images_;
|
||||
android::fiemap::IImageManager* image_manager_ = nullptr;
|
||||
};
|
||||
|
||||
TEST_F(SnapshotTest, CreateSnapshot) {
|
||||
ASSERT_TRUE(AcquireLock());
|
||||
|
||||
static const uint64_t kDeviceSize = 1024 * 1024;
|
||||
ASSERT_TRUE(sm->CreateSnapshot(lock_.get(), "test-snapshot", kDeviceSize, kDeviceSize,
|
||||
kDeviceSize));
|
||||
|
||||
std::vector<std::string> snapshots;
|
||||
ASSERT_TRUE(sm->ListSnapshots(lock_.get(), &snapshots));
|
||||
ASSERT_EQ(snapshots.size(), 1);
|
||||
ASSERT_EQ(snapshots[0], "test-snapshot");
|
||||
|
||||
// Scope so delete can re-acquire the status file lock.
|
||||
{
|
||||
auto file = sm->OpenSnapshotStatusFile("test-snapshot", O_RDONLY, LOCK_SH);
|
||||
ASSERT_NE(file, nullptr);
|
||||
|
||||
SnapshotManager::SnapshotStatus status;
|
||||
ASSERT_TRUE(sm->ReadSnapshotStatus(file.get(), &status));
|
||||
ASSERT_EQ(status.state, "created");
|
||||
ASSERT_EQ(status.device_size, kDeviceSize);
|
||||
ASSERT_EQ(status.snapshot_size, kDeviceSize);
|
||||
}
|
||||
|
||||
ASSERT_TRUE(sm->DeleteSnapshot(lock_.get(), "test-snapshot"));
|
||||
}
|
||||
|
||||
TEST_F(SnapshotTest, MapSnapshot) {
|
||||
ASSERT_TRUE(AcquireLock());
|
||||
|
||||
static const uint64_t kDeviceSize = 1024 * 1024;
|
||||
ASSERT_TRUE(sm->CreateSnapshot(lock_.get(), "test-snapshot", kDeviceSize, kDeviceSize,
|
||||
kDeviceSize));
|
||||
|
||||
std::string base_device;
|
||||
ASSERT_TRUE(CreateTempDevice("base-device", kDeviceSize, &base_device));
|
||||
|
||||
std::string snap_device;
|
||||
ASSERT_TRUE(sm->MapSnapshot(lock_.get(), "test-snapshot", base_device, 10s, &snap_device));
|
||||
ASSERT_TRUE(android::base::StartsWith(snap_device, "/dev/block/dm-"));
|
||||
}
|
||||
|
||||
TEST_F(SnapshotTest, MapPartialSnapshot) {
|
||||
ASSERT_TRUE(AcquireLock());
|
||||
|
||||
static const uint64_t kSnapshotSize = 1024 * 1024;
|
||||
static const uint64_t kDeviceSize = 1024 * 1024 * 2;
|
||||
ASSERT_TRUE(sm->CreateSnapshot(lock_.get(), "test-snapshot", kDeviceSize, kSnapshotSize,
|
||||
kSnapshotSize));
|
||||
|
||||
std::string base_device;
|
||||
ASSERT_TRUE(CreateTempDevice("base-device", kDeviceSize, &base_device));
|
||||
|
||||
std::string snap_device;
|
||||
ASSERT_TRUE(sm->MapSnapshot(lock_.get(), "test-snapshot", base_device, 10s, &snap_device));
|
||||
ASSERT_TRUE(android::base::StartsWith(snap_device, "/dev/block/dm-"));
|
||||
}
|
||||
|
||||
} // namespace snapshot
|
||||
} // namespace android
|
||||
|
||||
using namespace android::snapshot;
|
||||
|
||||
bool Mkdir(const std::string& path) {
|
||||
if (mkdir(path.c_str(), 0700) && errno != EEXIST) {
|
||||
std::cerr << "Could not mkdir " << path << ": " << strerror(errno) << std::endl;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
|
||||
std::vector<std::string> paths = {
|
||||
"/data/gsi/ota/test",
|
||||
"/metadata/gsi/ota/test",
|
||||
"/metadata/ota/test",
|
||||
"/metadata/ota/test/snapshots",
|
||||
};
|
||||
for (const auto& path : paths) {
|
||||
if (!Mkdir(path)) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Create this once, otherwise, gsid will start/stop between each test.
|
||||
test_device = new TestDeviceInfo();
|
||||
sm = SnapshotManager::New(test_device);
|
||||
if (!sm) {
|
||||
std::cerr << "Could not create snapshot manager";
|
||||
return 1;
|
||||
}
|
||||
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
|
@ -415,6 +415,7 @@ on post-fs
|
|||
chmod 0700 /metadata/vold
|
||||
mkdir /metadata/password_slots 0771 root system
|
||||
mkdir /metadata/ota 0700 root system
|
||||
mkdir /metadata/ota/snapshots 0700 root system
|
||||
|
||||
mkdir /metadata/apex 0700 root system
|
||||
mkdir /metadata/apex/sessions 0700 root system
|
||||
|
|
Loading…
Reference in a new issue