Implement backup in terminal
1. Add an option to backup in recovery
2. If it is set, preserve root_part as root_part_backup
3. If backup exists, terminal app mounts it in /mnt/backup
4. Provide remove-backup button as well
5. Remove every file in linux during uninstallation
Bug: 366386783
Test: backup & check
Change-Id: Ic7e2d38172615358253d92e26a63ec6b368d3b94
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt
index 95bcbbc..e2bb28f 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt
@@ -18,12 +18,16 @@
import android.content.Intent
import android.os.Bundle
import android.util.Log
+import android.view.View
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.android.virtualization.vmlauncher.InstallUtils
import com.google.android.material.card.MaterialCardView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.snackbar.Snackbar
import java.io.IOException
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -34,30 +38,90 @@
super.onCreate(savedInstanceState)
setContentView(R.layout.settings_recovery)
val resetCard = findViewById<MaterialCardView>(R.id.settings_recovery_reset_card)
- val dialog = MaterialAlertDialogBuilder(this)
- .setTitle(R.string.settings_recovery_reset_dialog_title)
- .setMessage(R.string.settings_recovery_reset_dialog_message)
- .setPositiveButton(R.string.settings_recovery_reset_dialog_confirm) { _, _ ->
- // This coroutine will be killed when the activity is killed. The behavior is both acceptable
- // either removing is done or not
- lifecycleScope.launch(Dispatchers.IO) {
- try {
- InstallUtils.unInstall(this@SettingsRecoveryActivity)
- // Restart terminal
- val intent =
- baseContext.packageManager.getLaunchIntentForPackage(baseContext.packageName)
- intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
- finish()
- startActivity(intent)
- } catch (e: IOException) {
- Log.e(TAG, "VM image reset failed.")
+ resetCard.setOnClickListener {
+ var backupRootfs = false
+ val dialog = MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.settings_recovery_reset_dialog_title)
+ .setMultiChoiceItems(arrayOf(getString(R.string.settings_recovery_reset_dialog_backup_option)), booleanArrayOf(backupRootfs)) {_, _, checked ->
+ backupRootfs = checked
+ }
+ .setPositiveButton(R.string.settings_recovery_reset_dialog_confirm) { _, _ ->
+ // This coroutine will be killed when the activity is killed. The behavior is both acceptable
+ // either removing is done or not
+ runInBackgroundAndRestartApp {
+ uninstall(backupRootfs)
}
}
- }
- .setNegativeButton(R.string.settings_recovery_reset_dialog_cancel) { dialog, _ -> dialog.dismiss() }
- .create()
- resetCard.setOnClickListener {
+ .setNegativeButton(R.string.settings_recovery_reset_dialog_cancel) { dialog, _ -> dialog.dismiss() }
+ .create()
dialog.show()
}
+ val resetBackupCard = findViewById<View>(R.id.settings_recovery_reset_backup_card)
+ resetBackupCard.isVisible = InstallUtils.getBackupFile(this).exists()
+
+ resetBackupCard.setOnClickListener {
+ val dialog = MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.settings_recovery_remove_backup_title)
+ .setMessage(R.string.settings_recovery_remove_backup_sub_title)
+ .setPositiveButton(R.string.settings_recovery_reset_dialog_confirm) { _, _ ->
+ runInBackgroundAndRestartApp {
+ removeBackup()
+ }
+ }
+ .setNegativeButton(R.string.settings_recovery_reset_dialog_cancel) { dialog, _ -> dialog.dismiss() }
+ .create()
+ dialog.show()
+ }
+ }
+
+ private fun removeBackup(): Unit {
+ if (!InstallUtils.getBackupFile(this@SettingsRecoveryActivity).delete()) {
+ Snackbar.make(
+ findViewById(android.R.id.content),
+ R.string.settings_recovery_error_during_removing_backup,
+ Snackbar.LENGTH_SHORT
+ ).show();
+ Log.e(TAG, "cannot remove backup")
+ }
+ }
+
+ private fun uninstall(backupRootfs: Boolean): Unit {
+ var backupDone = false
+ try {
+ if (backupRootfs) {
+ InstallUtils.backupRootFs(this@SettingsRecoveryActivity)
+ backupDone = true
+ }
+ InstallUtils.deleteInstallation(this@SettingsRecoveryActivity)
+ } catch (e: IOException) {
+ val errorMsgId = if (backupRootfs && !backupDone) R.string.settings_recovery_error_due_to_backup
+ else R.string.settings_recovery_error;
+ Snackbar.make(
+ findViewById(android.R.id.content),
+ errorMsgId,
+ Snackbar.LENGTH_SHORT
+ ).show();
+ Log.e(TAG, "cannot recovery ", e)
+ }
+ }
+
+ private fun runInBackgroundAndRestartApp(backgroundWork: suspend CoroutineScope.() -> Unit): Unit {
+ findViewById<View>(R.id.setting_recovery_card_container).visibility = View.INVISIBLE
+ findViewById<View>(R.id.recovery_boot_progress).visibility = View.VISIBLE
+ lifecycleScope.launch(Dispatchers.IO) {
+ backgroundWork()
+ }.invokeOnCompletion {
+ runOnUiThread {
+ findViewById<View>(R.id.setting_recovery_card_container).visibility =
+ View.VISIBLE
+ findViewById<View>(R.id.recovery_boot_progress).visibility = View.INVISIBLE
+ // Restart terminal
+ val intent =
+ baseContext.packageManager.getLaunchIntentForPackage(baseContext.packageName)
+ intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ finish()
+ startActivity(intent)
+ }
+ }
}
}
\ No newline at end of file
diff --git a/android/TerminalApp/res/layout/settings_recovery.xml b/android/TerminalApp/res/layout/settings_recovery.xml
index 8b13ea5..3f83588 100644
--- a/android/TerminalApp/res/layout/settings_recovery.xml
+++ b/android/TerminalApp/res/layout/settings_recovery.xml
@@ -16,7 +16,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_marginEnd="24dp"
@@ -30,46 +30,98 @@
android:textSize="48sp"
android:layout_marginStart="24dp"
android:layout_marginBottom="24dp"/>
-
- <com.google.android.material.card.MaterialCardView
- android:id="@+id/settings_recovery_reset_card"
- app:strokeWidth="0dp"
- app:cardCornerRadius="0dp"
- app:checkedIcon="@null"
- android:focusable="true"
- android:checkable="true"
- android:layout_height="wrap_content"
- android:layout_width="match_parent">
-
- <androidx.constraintlayout.widget.ConstraintLayout
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <com.google.android.material.progressindicator.CircularProgressIndicator
+ android:id="@+id/recovery_boot_progress"
+ android:indeterminate="true"
+ android:layout_gravity="center"
+ android:visibility="invisible"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+ <LinearLayout
+ android:id="@+id/setting_recovery_card_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginEnd="16dp"
- android:layout_marginStart="24dp">
-
- <TextView
- android:id="@+id/settings_recovery_reset_title"
- android:layout_width="0dp"
+ android:orientation="vertical" >
+ <!-- TODO: consider custom view for settings item -->
+ <com.google.android.material.card.MaterialCardView
+ android:id="@+id/settings_recovery_reset_card"
+ app:strokeWidth="0dp"
+ app:cardCornerRadius="0dp"
+ app:checkedIcon="@null"
+ android:focusable="true"
+ android:checkable="true"
android:layout_height="wrap_content"
- android:layout_marginTop="20dp"
- android:layout_marginStart="24dp"
- android:textSize="20sp"
- android:text="@string/settings_recovery_reset_title"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toTopOf="@+id/settings_recovery_reset_sub_title"
- app:layout_constraintStart_toStartOf="parent" />
-
- <TextView
- android:id="@+id/settings_recovery_reset_sub_title"
- android:layout_width="0dp"
+ android:layout_width="match_parent">
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="16dp"
+ android:layout_marginStart="24dp">
+ <TextView
+ android:id="@+id/settings_recovery_reset_title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="20dp"
+ android:layout_marginStart="24dp"
+ android:textSize="20sp"
+ android:text="@string/settings_recovery_reset_title"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@+id/settings_recovery_reset_sub_title"
+ app:layout_constraintStart_toStartOf="parent" />
+ <TextView
+ android:id="@+id/settings_recovery_reset_sub_title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textSize="14sp"
+ android:layout_marginBottom="20dp"
+ android:layout_marginStart="24dp"
+ android:text="@string/settings_recovery_reset_sub_title"
+ app:layout_constraintTop_toBottomOf="@+id/settings_recovery_reset_title"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+ </com.google.android.material.card.MaterialCardView>
+ <com.google.android.material.card.MaterialCardView
+ android:id="@+id/settings_recovery_reset_backup_card"
+ app:strokeWidth="0dp"
+ app:cardCornerRadius="0dp"
+ app:checkedIcon="@null"
+ android:focusable="true"
+ android:checkable="true"
android:layout_height="wrap_content"
- android:textSize="14sp"
- android:layout_marginBottom="20dp"
- android:layout_marginStart="24dp"
- android:text="@string/settings_recovery_reset_sub_title"
- app:layout_constraintTop_toBottomOf="@+id/settings_recovery_reset_title"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toStartOf="parent" />
- </androidx.constraintlayout.widget.ConstraintLayout>
- </com.google.android.material.card.MaterialCardView>
-</LinearLayout>
\ No newline at end of file
+ android:layout_width="match_parent">
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="16dp"
+ android:layout_marginStart="24dp">
+ <TextView
+ android:id="@+id/settings_recovery_reset_backup_title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="20dp"
+ android:layout_marginStart="24dp"
+ android:textSize="20sp"
+ android:text="@string/settings_recovery_remove_backup_title"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@+id/settings_recovery_reset_backup_sub_title"
+ app:layout_constraintStart_toStartOf="parent" />
+ <TextView
+ android:id="@+id/settings_recovery_reset_backup_sub_title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textSize="14sp"
+ android:layout_marginBottom="20dp"
+ android:layout_marginStart="24dp"
+ android:text="@string/settings_recovery_remove_backup_sub_title"
+ app:layout_constraintTop_toBottomOf="@+id/settings_recovery_reset_backup_title"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+ </com.google.android.material.card.MaterialCardView>
+ </LinearLayout>
+ </FrameLayout>
+</LinearLayout>
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
index fb75533..e18b0d1 100644
--- a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -62,7 +62,7 @@
mExecutorService = Executors.newCachedThreadPool();
ConfigJson json = ConfigJson.from(VM_CONFIG_PATH);
- VirtualMachineConfig config = json.toConfig(this);
+ VirtualMachineConfig config = json.toConfigBuilder(this).build();
Runner runner;
try {
diff --git a/build/debian/fai_config/files/etc/systemd/system/backup_mount.service/AVF b/build/debian/fai_config/files/etc/systemd/system/backup_mount.service/AVF
new file mode 100644
index 0000000..966df49
--- /dev/null
+++ b/build/debian/fai_config/files/etc/systemd/system/backup_mount.service/AVF
@@ -0,0 +1,14 @@
+[Unit]
+Description=Mount backup rootfs
+After=network.target
+After=virtiofs_internal.service
+
+[Service]
+Type=oneshot
+User=root
+Group=root
+ExecStart=/bin/bash -c '[ -e "/dev/vdb" ] && (mkdir -p /mnt/backup; chown 1000:100 /mnt/backup; mount /dev/vdb /mnt/backup) || (rm -rf /mnt/backup)'
+RemainAfterExit=yes
+
+[Install]
+WantedBy=multi-user.target
diff --git a/build/debian/fai_config/scripts/AVF/10-systemd b/build/debian/fai_config/scripts/AVF/10-systemd
index 1605381..a514299 100755
--- a/build/debian/fai_config/scripts/AVF/10-systemd
+++ b/build/debian/fai_config/scripts/AVF/10-systemd
@@ -9,3 +9,4 @@
ln -s /etc/systemd/system/virtiofs.service $target/etc/systemd/system/multi-user.target.wants/virtiofs.service
ln -s /etc/systemd/system/forwarder_guest_launcher.service $target/etc/systemd/system/multi-user.target.wants/forwarder_guest_launcher.service
ln -s /etc/systemd/system/virtiofs_internal.service $target/etc/systemd/system/multi-user.target.wants/virtiofs_internal.service
+ln -s /etc/systemd/system/backup_mount.service $target/etc/systemd/system/multi-user.target.wants/backup_mount.service
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/ConfigJson.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/ConfigJson.java
index a259fe2..f229964 100644
--- a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/ConfigJson.java
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/ConfigJson.java
@@ -94,21 +94,20 @@
: VirtualMachineConfig.DEBUG_LEVEL_NONE;
}
- /** Converts this parsed JSON into VirtualMachieConfig */
- VirtualMachineConfig toConfig(Context context) {
+ /** Converts this parsed JSON into VirtualMachieConfig Builder */
+ VirtualMachineConfig.Builder toConfigBuilder(Context context) {
return new VirtualMachineConfig.Builder(context)
.setProtectedVm(isProtected)
.setMemoryBytes((long) memory_mib * 1024 * 1024)
.setConsoleInputDevice(console_input_device)
.setCpuTopology(getCpuTopology())
- .setCustomImageConfig(toCustomImageConfig(context))
+ .setCustomImageConfig(toCustomImageConfigBuilder(context).build())
.setDebugLevel(getDebugLevel())
.setVmOutputCaptured(console_out)
- .setConnectVmConsole(connect_console)
- .build();
+ .setConnectVmConsole(connect_console);
}
- private VirtualMachineCustomImageConfig toCustomImageConfig(Context context) {
+ VirtualMachineCustomImageConfig.Builder toCustomImageConfigBuilder(Context context) {
VirtualMachineCustomImageConfig.Builder builder =
new VirtualMachineCustomImageConfig.Builder();
@@ -152,7 +151,7 @@
.filter(Objects::nonNull)
.forEach(builder::addSharedPath);
}
- return builder.build();
+ return builder;
}
private static class SharedPathJson {
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/InstallUtils.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/InstallUtils.java
index d55d268..4044fff 100644
--- a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/InstallUtils.java
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/InstallUtils.java
@@ -43,6 +43,7 @@
private static final String VM_CONFIG_FILENAME = "vm_config.json";
private static final String COMPRESSED_PAYLOAD_FILENAME = "images.tar.gz";
private static final String ROOTFS_FILENAME = "root_part";
+ private static final String BACKUP_FILENAME = "root_part_backup";
private static final String INSTALLATION_COMPLETED_FILENAME = "completed";
private static final String PAYLOAD_DIR = "linux";
@@ -54,8 +55,11 @@
return Files.exists(getInstallationCompletedPath(context));
}
- public static void unInstall(Context context) throws IOException {
- Files.delete(getInstallationCompletedPath(context));
+ public static void backupRootFs(Context context) throws IOException {
+ Files.move(
+ getRootfsFile(context).toPath(),
+ getBackupFile(context).toPath(),
+ StandardCopyOption.REPLACE_EXISTING);
}
public static boolean createInstalledMarker(Context context) {
@@ -91,6 +95,10 @@
return new File(context.getFilesDir(), PAYLOAD_DIR);
}
+ public static File getBackupFile(Context context) {
+ return new File(context.getFilesDir(), BACKUP_FILENAME);
+ }
+
private static Path getInstallationCompletedPath(Context context) {
return getInternalStorageDir(context).toPath().resolve(INSTALLATION_COMPLETED_FILENAME);
}
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java
index 846fd26..1eb558e 100644
--- a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java
@@ -24,6 +24,8 @@
import android.os.ResultReceiver;
import android.system.virtualmachine.VirtualMachine;
import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.Disk;
import android.system.virtualmachine.VirtualMachineException;
import android.util.Log;
@@ -85,7 +87,15 @@
mExecutorService = Executors.newCachedThreadPool();
ConfigJson json = ConfigJson.from(InstallUtils.getVmConfigPath(this));
- VirtualMachineConfig config = json.toConfig(this);
+ VirtualMachineConfig.Builder configBuilder = json.toConfigBuilder(this);
+ VirtualMachineCustomImageConfig.Builder customImageConfigBuilder =
+ json.toCustomImageConfigBuilder(this);
+ File backupFile = InstallUtils.getBackupFile(this);
+ if (backupFile.exists()) {
+ customImageConfigBuilder.addDisk(Disk.RWDisk(backupFile.getPath()));
+ configBuilder.setCustomImageConfig(customImageConfigBuilder.build());
+ }
+ VirtualMachineConfig config = configBuilder.build();
Runner runner;
try {