Port incus features to lxc [stage]

This commit is contained in:
crschnick
2024-12-30 02:33:11 +00:00
parent ed6f24a2bf
commit 39d3e81196
11 changed files with 388 additions and 32 deletions
@@ -139,20 +139,20 @@ public class IncusCommandView extends CommandViewBase {
}
}
public ShellControl exec(String container, Integer uid, FilePath dir) {
public ShellControl exec(String container, Integer uid, FilePath dir, ShellDialect shell) {
return shellControl
.subShell(createOpenFunction(container, uid, dir, false), createOpenFunction(container, uid, dir, true))
.subShell(createOpenFunction(container, uid, dir, false, shell), createOpenFunction(container, uid, dir, true, shell))
.withErrorFormatter(IncusCommandView::formatErrorMessage)
.withExceptionConverter(IncusCommandView::convertException)
.elevated(requiresElevation());
}
private ShellOpenFunction createOpenFunction(String containerName, Integer uid, FilePath dir, boolean terminal) {
private ShellOpenFunction createOpenFunction(String containerName, Integer uid, FilePath dir, boolean terminal, ShellDialect shell) {
return new ShellOpenFunction() {
@Override
public CommandBuilder prepareWithoutInitCommand() {
return execCommand(containerName, uid, dir, terminal)
.add(ShellDialects.SH.getLaunchCommand().loginCommand());
.add(shell.getLaunchCommand().loginCommand());
}
@Override
@@ -7,6 +7,8 @@ import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.*;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellDialect;
import io.xpipe.core.process.ShellDialects;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.store.FixedChildStore;
import io.xpipe.core.store.StatefulDataStore;
@@ -77,10 +79,11 @@ public class IncusContainerStore
public ShellControl control(ShellControl parent) throws Exception {
Integer uid = null;
FilePath homeDir = null;
if (identity != null && identity.unwrap().getUsername() != null) {
try (var temp = new IncusCommandView(parent)
.exec(containerName, null, null)
.start()) {
ShellDialect shell;
try (var temp = new IncusCommandView(parent)
.exec(containerName, null, null, ShellDialects.SH)
.start()) {
if (identity != null && identity.unwrap().getUsername() != null) {
var passwd = PasswdFile.parse(temp);
var username = identity.unwrap().getUsername();
uid = passwd.getUidForUserIfPresent(username)
@@ -89,9 +92,10 @@ public class IncusContainerStore
homeDir = temp.command("eval echo ~" +username).readStdoutIfPossible().filter(s -> !s.isBlank())
.map(FilePath::new).orElse(null);
}
shell = CommandSupport.isInPath(temp, "bash") ? ShellDialects.BASH : ShellDialects.SH;
}
var sc = new IncusCommandView(parent).exec(containerName, uid, homeDir);
var sc = new IncusCommandView(parent).exec(containerName, uid, homeDir, shell);
sc.withSourceStore(IncusContainerStore.this);
if (identity != null && identity.unwrap().getPassword() != null) {
sc.setElevationHandler(new BaseElevationHandler(
@@ -7,6 +7,7 @@ import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.CommandViewBase;
import io.xpipe.core.process.*;
import io.xpipe.core.store.FilePath;
import lombok.NonNull;
import java.util.*;
@@ -86,12 +87,40 @@ public class LxdCommandView extends CommandViewBase {
.readStdoutOrThrow();
}
public String queryContainerState(String containerName) throws Exception {
var states = listContainersAndStates();
return states.getOrDefault(containerName, "?");
}
public void start(String containerName) throws Exception {
build(commandBuilder -> commandBuilder.add("start").addQuoted(containerName))
.execute();
}
public void stop(String containerName) throws Exception {
build(commandBuilder -> commandBuilder.add("stop").addQuoted(containerName))
.execute();
}
public void pause(String containerName) throws Exception {
build(commandBuilder -> commandBuilder.add("pause").addQuoted(containerName))
.execute();
}
public CommandControl console(String containerName) throws Exception {
return build(commandBuilder -> commandBuilder.add("console").addQuoted(containerName));
}
public CommandControl configEdit(String containerName) throws Exception {
return build(commandBuilder -> commandBuilder.add("config", "edit").addQuoted(containerName));
}
public List<DataStoreEntryRef<LxdContainerStore>> listContainers(DataStoreEntryRef<LxdCmdStore> store)
throws Exception {
return listContainersAndStates().entrySet().stream()
.map(s -> {
boolean running = s.getValue().toLowerCase(Locale.ROOT).equals("running");
var c = new LxdContainerStore(store, s.getKey());
var c = LxdContainerStore.builder().cmd(store).containerName(s.getKey()).build();
var entry = DataStoreEntry.createNew(c.getContainerName(), c);
entry.setStorePersistentState(ContainerStoreState.builder()
.containerState(s.getValue())
@@ -119,33 +148,37 @@ public class LxdCommandView extends CommandViewBase {
}
}
public ShellControl exec(String container) {
public ShellControl exec(String container, Integer uid, FilePath dir, ShellDialect shell) {
return shellControl
.subShell(createOpenFunction(container, false), createOpenFunction(container, true))
.subShell(createOpenFunction(container, uid, dir, shell, false), createOpenFunction(container, uid,dir, shell, true))
.withErrorFormatter(LxdCommandView::formatErrorMessage)
.withExceptionConverter(LxdCommandView::convertException)
.elevated(requiresElevation());
}
private ShellOpenFunction createOpenFunction(String containerName, boolean terminal) {
private ShellOpenFunction createOpenFunction(String containerName, Integer uid, FilePath dir, ShellDialect shell, boolean terminal) {
return new ShellOpenFunction() {
@Override
public CommandBuilder prepareWithoutInitCommand() {
return execCommand(containerName, terminal)
.add(ShellDialects.SH.getLaunchCommand().loginCommand());
return execCommand(containerName, uid, dir, terminal)
.add(shell.getLaunchCommand().loginCommand());
}
@Override
public CommandBuilder prepareWithInitCommand(@NonNull String command) {
return execCommand(containerName, terminal).add(command);
return execCommand(containerName, uid, dir , terminal).add(command);
}
};
}
public CommandBuilder execCommand(String containerName, boolean terminal) {
return CommandBuilder.of()
.add("lxc", "exec", terminal ? "-t" : "-T")
.addQuoted(containerName)
.add("--");
public CommandBuilder execCommand(String containerName, Integer uid, FilePath dir, boolean terminal) {
var c = CommandBuilder.of().add("lxc", "exec", terminal ? "-t" : "-T");
if (uid != null) {
c.add("--user").add(uid.toString());
}
if (dir != null) {
c.add("--cwd").addFile(dir);
}
return c.addQuoted(containerName).add("--");
}
}
@@ -0,0 +1,53 @@
package io.xpipe.ext.system.lxd;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.ext.base.store.StorePauseAction;
import io.xpipe.ext.base.store.StoreRestartAction;
import io.xpipe.ext.base.store.StoreStartAction;
import io.xpipe.ext.base.store.StoreStopAction;
import javafx.beans.value.ObservableValue;
import java.util.List;
public class LxdContainerActionMenu implements ActionProvider {
@Override
public BranchDataStoreCallSite<?> getBranchDataStoreCallSite() {
return new BranchDataStoreCallSite<LxdContainerStore>() {
@Override
public Class<LxdContainerStore> getApplicableClass() {
return LxdContainerStore.class;
}
@Override
public boolean isMajor(DataStoreEntryRef<LxdContainerStore> o) {
return true;
}
@Override
public ObservableValue<String> getName(DataStoreEntryRef<LxdContainerStore> store) {
return AppI18n.observable("containerActions");
}
@Override
public String getIcon(DataStoreEntryRef<LxdContainerStore> store) {
return "mdi2p-package-variant-closed";
}
@Override
public List<ActionProvider> getChildren(DataStoreEntryRef<LxdContainerStore> store) {
return List.of(
new StoreStartAction(),
new StoreStopAction(),
new StorePauseAction(),
new StoreRestartAction(),
new LxdContainerConsoleAction(),
new LxdContainerEditConfigAction(),
new LxdContainerEditRunConfigAction());
}
};
}
}
@@ -0,0 +1,57 @@
package io.xpipe.ext.system.lxd;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.terminal.TerminalLauncher;
import io.xpipe.ext.system.incus.IncusCommandView;
import javafx.beans.value.ObservableValue;
import lombok.Value;
public class LxdContainerConsoleAction implements ActionProvider {
@Override
public LeafDataStoreCallSite<?> getLeafDataStoreCallSite() {
return new LeafDataStoreCallSite<LxdContainerStore>() {
@Override
public ActionProvider.Action createAction(DataStoreEntryRef<LxdContainerStore> store) {
return new Action(store.get());
}
@Override
public Class<LxdContainerStore> getApplicableClass() {
return LxdContainerStore.class;
}
@Override
public ObservableValue<String> getName(DataStoreEntryRef<LxdContainerStore> store) {
return AppI18n.observable("serialConsole");
}
@Override
public String getIcon(DataStoreEntryRef<LxdContainerStore> store) {
return "mdi2c-console";
}
@Override
public boolean requiresValidStore() {
return false;
}
};
}
@Value
static class Action implements ActionProvider.Action {
DataStoreEntry store;
@Override
public void execute() throws Exception {
var d = (LxdContainerStore) store.getStore();
var view = new IncusCommandView(d.getCmd().getStore().getHost().getStore().getOrStartSession());
TerminalLauncher.open(store.getName(), view.console(d.getContainerName()));
}
}
}
@@ -0,0 +1,58 @@
package io.xpipe.ext.system.lxd;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.terminal.TerminalLauncher;
import io.xpipe.ext.system.incus.IncusCommandView;
import javafx.beans.value.ObservableValue;
import lombok.Value;
public class LxdContainerEditConfigAction implements ActionProvider {
@Override
public LeafDataStoreCallSite<?> getLeafDataStoreCallSite() {
return new LeafDataStoreCallSite<LxdContainerStore>() {
@Override
public ActionProvider.Action createAction(DataStoreEntryRef<LxdContainerStore> store) {
return new Action(store.get());
}
@Override
public Class<LxdContainerStore> getApplicableClass() {
return LxdContainerStore.class;
}
@Override
public ObservableValue<String> getName(DataStoreEntryRef<LxdContainerStore> store) {
return AppI18n.observable("editConfiguration");
}
@Override
public String getIcon(DataStoreEntryRef<LxdContainerStore> store) {
return "mdi2f-file-document-edit";
}
@Override
public boolean requiresValidStore() {
return false;
}
};
}
@Value
static class Action implements ActionProvider.Action {
DataStoreEntry store;
@Override
public void execute() throws Exception {
var d = (LxdContainerStore) store.getStore();
var view = new IncusCommandView(
d.getCmd().getStore().getHost().getStore().getOrStartSession());
TerminalLauncher.open(store.getName(), view.configEdit(d.getContainerName()));
}
}
}
@@ -0,0 +1,69 @@
package io.xpipe.ext.system.lxd;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.browser.file.BrowserFileOpener;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.store.FilePath;
import javafx.beans.value.ObservableValue;
import lombok.Value;
public class LxdContainerEditRunConfigAction implements ActionProvider {
@Override
public LeafDataStoreCallSite<?> getLeafDataStoreCallSite() {
return new LeafDataStoreCallSite<LxdContainerStore>() {
@Override
public ActionProvider.Action createAction(DataStoreEntryRef<LxdContainerStore> store) {
return new Action(store.get());
}
@Override
public Class<LxdContainerStore> getApplicableClass() {
return LxdContainerStore.class;
}
@Override
public ObservableValue<String> getName(DataStoreEntryRef<LxdContainerStore> store) {
return AppI18n.observable("editRunConfiguration");
}
@Override
public String getIcon(DataStoreEntryRef<LxdContainerStore> store) {
return "mdi2m-movie-edit";
}
@Override
public boolean requiresValidStore() {
return false;
}
};
}
@Value
static class Action implements ActionProvider.Action {
DataStoreEntry store;
@Override
public void execute() throws Exception {
var d = (LxdContainerStore) store.getStore();
var elevatedRef = ProcessControlProvider.get()
.elevated(d.getCmd().getStore().getHost().get().ref());
var file = new FilePath("/run/lxd/" + d.getContainerName() + "/lxc.conf");
var model = BrowserFullSessionModel.DEFAULT.openFileSystemSync(
elevatedRef, m -> file.getParent().toString(), null, true);
var found = model.findFile(file.toString());
if (found.isEmpty()) {
return;
}
AppLayoutModel.get().selectBrowser();
BrowserFileOpener.openInTextEditor(model, found.get());
}
}
}
@@ -5,13 +5,19 @@ import io.xpipe.app.ext.ShellControlFunction;
import io.xpipe.app.ext.ShellControlParentStoreFunction;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.LicenseRequiredException;
import io.xpipe.app.util.Validators;
import io.xpipe.app.util.*;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellDialect;
import io.xpipe.core.process.ShellDialects;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.store.FixedChildStore;
import io.xpipe.core.store.StatefulDataStore;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.ext.base.identity.IdentityValue;
import io.xpipe.ext.base.store.PauseableStore;
import io.xpipe.ext.base.store.StartableStore;
import io.xpipe.ext.base.store.StoppableStore;
import lombok.AllArgsConstructor;
import lombok.Value;
import lombok.experimental.SuperBuilder;
@@ -25,10 +31,11 @@ import java.util.OptionalInt;
@Jacksonized
@Value
@AllArgsConstructor
public class LxdContainerStore implements ShellStore, FixedChildStore, StatefulDataStore<ContainerStoreState> {
public class LxdContainerStore implements ShellStore, FixedChildStore, StatefulDataStore<ContainerStoreState>, StartableStore, StoppableStore, PauseableStore {
private final DataStoreEntryRef<LxdCmdStore> cmd;
private final String containerName;
IdentityValue identity;
@Override
public Class<ContainerStoreState> getStateClass() {
@@ -41,6 +48,9 @@ public class LxdContainerStore implements ShellStore, FixedChildStore, StatefulD
Validators.isType(cmd, LxdCmdStore.class);
cmd.checkComplete();
Validators.nonNull(containerName);
if (identity != null) {
identity.checkComplete(false, false, false);
}
}
@Override
@@ -58,8 +68,30 @@ public class LxdContainerStore implements ShellStore, FixedChildStore, StatefulD
}
@Override
public ShellControl control(ShellControl parent) {
var base = new LxdCommandView(parent).exec(containerName);
public ShellControl control(ShellControl parent) throws Exception {
Integer uid = null;
FilePath homeDir = null;
ShellDialect shell;
try (var temp = new LxdCommandView(parent)
.exec(containerName, null, null, ShellDialects.SH)
.start()) {
if (identity != null && identity.unwrap().getUsername() != null) {
var passwd = PasswdFile.parse(temp);
var username = identity.unwrap().getUsername();
uid = passwd.getUidForUserIfPresent(username)
.orElseThrow(() -> new IllegalArgumentException("User " + username + " not found"));
homeDir = temp.command("eval echo ~" +username).readStdoutIfPossible().filter(s -> !s.isBlank())
.map(FilePath::new).orElse(null);
}
shell = CommandSupport.isInPath(temp, "bash") ? ShellDialects.BASH : ShellDialects.SH;
}
var base = new LxdCommandView(parent).exec(containerName, uid, homeDir, shell);
if (identity != null && identity.unwrap().getPassword() != null) {
base.setElevationHandler(new BaseElevationHandler(
LxdContainerStore.this, identity.unwrap().getPassword())
.orElse(base.getElevationHandler()));
}
return base.withSourceStore(LxdContainerStore.this)
.onInit(shellControl -> {
var s = getState().toBuilder()
@@ -85,4 +117,39 @@ public class LxdContainerStore implements ShellStore, FixedChildStore, StatefulD
}
};
}
private void refreshContainerState(ShellControl sc) throws Exception {
var state = getState();
var view = new LxdCommandView(sc);
var displayState = view.queryContainerState(containerName);
var running = "RUNNING".equals(displayState);
var newState =
state.toBuilder().containerState(displayState).running(running).build();
setState(newState);
}
@Override
public void start() throws Exception {
var sc = getCmd().getStore().getHost().getStore().getOrStartSession();
var view = new LxdCommandView(sc);
view.start(containerName);
refreshContainerState(sc);
}
@Override
public void stop() throws Exception {
var sc = getCmd().getStore().getHost().getStore().getOrStartSession();
var view = new LxdCommandView(sc);
view.stop(containerName);
refreshContainerState(sc);
}
@Override
public void pause() throws Exception {
var sc = getCmd().getStore().getHost().getStore().getOrStartSession();
var view = new LxdCommandView(sc);
view.pause(containerName);
refreshContainerState(sc);
}
}
@@ -12,8 +12,10 @@ import io.xpipe.app.util.OptionsBuilder;
import io.xpipe.app.util.ShellStoreFormat;
import io.xpipe.app.util.SimpleValidator;
import io.xpipe.core.store.DataStore;
import io.xpipe.ext.base.identity.IdentityChoice;
import io.xpipe.ext.base.store.ShellStoreProvider;
import io.xpipe.ext.system.incus.IncusContainerStore;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
@@ -46,7 +48,7 @@ public class LxdContainerStoreProvider implements ShellStoreProvider {
in a host shell of `%s` to open a shell into the container.
""",
new LxdCommandView(null)
.execCommand(lxd.getContainerName(), true)
.execCommand(lxd.getContainerName(), null, null,true)
.buildSimple(),
lxd.getCmd().getStore().getHost().get().getName());
}
@@ -59,8 +61,8 @@ public class LxdContainerStoreProvider implements ShellStoreProvider {
@Override
public GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {
var val = new SimpleValidator();
LxdContainerStore st = (LxdContainerStore) store.getValue();
var identity = new SimpleObjectProperty<>(st.getIdentity());
var q = new OptionsBuilder()
.name("host")
@@ -73,8 +75,18 @@ public class LxdContainerStoreProvider implements ShellStoreProvider {
.description("lxdContainerDescription")
.addString(new SimpleObjectProperty<>(st.getContainerName()), false)
.disable()
.buildComp();
return new GuiDialog(q, val);
.sub(IdentityChoice.container(identity), identity)
.bind(
() -> {
return LxdContainerStore.builder()
.containerName(st.getContainerName())
.cmd(st.getCmd())
.identity(identity.getValue())
.build();
},
store)
.buildDialog();
return q;
}
@Override
@@ -86,7 +98,8 @@ public class LxdContainerStoreProvider implements ShellStoreProvider {
@Override
public ObservableValue<String> informationString(StoreSection section) {
return ShellStoreFormat.shellStore(section, (ContainerStoreState s) -> s.getContainerState());
return ShellStoreFormat.shellStore(
section, (ContainerStoreState s) -> DataStoreFormatter.capitalize(s.getContainerState()));
}
@Override
@@ -6,6 +6,7 @@ import io.xpipe.ext.system.incus.IncusContainerStoreProvider;
import io.xpipe.ext.system.incus.IncusInstallStoreProvider;
import io.xpipe.ext.system.incus.IncusScanProvider;
import io.xpipe.ext.system.lxd.LxdCmdStoreProvider;
import io.xpipe.ext.system.lxd.LxdContainerActionMenu;
import io.xpipe.ext.system.lxd.LxdContainerStoreProvider;
import io.xpipe.ext.system.lxd.LxdScanProvider;
import io.xpipe.ext.system.podman.PodmanCmdStoreProvider;
@@ -36,5 +37,6 @@ open module io.xpipe.ext.system {
PodmanCmdStoreProvider;
provides ActionProvider with
IncusContainerActionMenu,
LxdContainerActionMenu,
PodmanContainerActionMenu;
}
+1 -1
View File
@@ -1 +1 @@
14.0-31
14.0-32