From a511e5b87e31e121445d58062f9daae2bc3b18b3 Mon Sep 17 00:00:00 2001 From: Klaus Ma Date: Sat, 31 Jan 2026 08:19:25 +0000 Subject: [PATCH 1/2] chore: add three model for flmadm install Signed-off-by: Klaus Ma --- Cargo.lock | 12 +- common/src/lib.rs | 15 +- docker/Dockerfile.cache | 19 -- docker/Dockerfile.console | 37 ++-- docker/Dockerfile.fem | 32 +-- docker/Dockerfile.fsm | 23 +- docs/designs/RFE318-cache/STATUS.md | 2 +- e2e/tests/test_agent.py | 2 +- e2e/tests/test_core.py | 10 +- e2e/tests/test_flmrun.py | 3 +- executor_manager/Cargo.toml | 1 + executor_manager/src/shims/host_shim.rs | 19 +- flmadm/README.md | 121 ++++++++++- flmadm/src/commands/install.rs | 278 ++++++++++++++++++++---- flmadm/src/commands/uninstall.rs | 15 +- flmadm/src/main.rs | 51 ++++- flmadm/src/managers/installation.rs | 209 ++++++++++++++---- flmadm/src/managers/systemd.rs | 73 ++++--- flmadm/src/managers/user.rs | 131 ----------- flmadm/src/types.rs | 39 +++- flmexec/src/script/lang/python.rs | 19 +- object_cache/README.md | 12 +- 22 files changed, 770 insertions(+), 353 deletions(-) delete mode 100644 docker/Dockerfile.cache diff --git a/Cargo.lock b/Cargo.lock index e20633cb..517d6423 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1561,6 +1561,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "shellexpand 3.1.1", "stdng", "tokio", "tokio-stream", @@ -3891,6 +3892,15 @@ dependencies = [ "dirs", ] +[[package]] +name = "shellexpand" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" +dependencies = [ + "dirs", +] + [[package]] name = "shlex" version = "1.3.0" @@ -5524,7 +5534,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "shellexpand", + "shellexpand 2.1.2", "syn 2.0.114", "witx", ] diff --git a/common/src/lib.rs b/common/src/lib.rs index 242cfdc1..e6207120 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -181,12 +181,13 @@ pub fn default_applications() -> HashMap { "description": "The output of the script in UTF-8." }); - // Get FLAME_HOME from environment or use default - let flame_home = std::env::var("FLAME_HOME").unwrap_or_else(|_| "/usr/local/flame".to_string()); - let flmexec_cmd = format!("{}/bin/flmexec-service", flame_home); - let flmping_cmd = format!("{}/bin/flmping-service", flame_home); - let flmping_url = format!("file://{}/bin/flmping-service", flame_home); - let flamepy_sdk_path = format!("file://{}/sdk/python", flame_home); + // Use ${FLAME_HOME} variable substitution syntax + // This will be expanded at runtime by the executor to the actual FLAME_HOME path + let flmexec_cmd = "${FLAME_HOME}/bin/flmexec-service".to_string(); + let flmping_cmd = "${FLAME_HOME}/bin/flmping-service".to_string(); + let uv_cmd = "${FLAME_HOME}/bin/uv".to_string(); + let flmping_url = "file://${FLAME_HOME}/bin/flmping-service".to_string(); + let flamepy_sdk_path = "file://${FLAME_HOME}/sdk/python".to_string(); HashMap::from([ ( @@ -222,7 +223,7 @@ pub fn default_applications() -> HashMap { "The Flame Runner application for executing customized Python applications." .to_string(), ), - command: Some("/usr/bin/uv".to_string()), + command: Some(uv_cmd), arguments: vec![ "run".to_string(), "--with".to_string(), diff --git a/docker/Dockerfile.cache b/docker/Dockerfile.cache deleted file mode 100644 index 76299fe8..00000000 --- a/docker/Dockerfile.cache +++ /dev/null @@ -1,19 +0,0 @@ -FROM rust:1.88 AS builder - -WORKDIR /usr/src/flame -COPY . . - -RUN apt-get update && apt-get install -y --no-install-recommends protobuf-compiler pkg-config libssl-dev && rm -rf /var/lib/apt/lists/* -RUN cargo install --path ./object_cache - -FROM ubuntu:24.04 - -RUN mkdir -p /usr/local/flame/bin -RUN mkdir -p /var/lib/flame/cache -WORKDIR /usr/local/flame - -COPY --from=builder /usr/local/cargo/bin/flame-cache /usr/local/flame/bin/flame-cache - -RUN chmod +x /usr/local/flame/bin/* - -ENTRYPOINT ["/usr/local/flame/bin/flame-cache"] diff --git a/docker/Dockerfile.console b/docker/Dockerfile.console index 3bee2d15..c90685c2 100644 --- a/docker/Dockerfile.console +++ b/docker/Dockerfile.console @@ -2,25 +2,36 @@ FROM rust:1.88 AS builder WORKDIR /usr/src/flame COPY . . -RUN apt-get update && apt-get install -y protobuf-compiler pkg-config libssl-dev -RUN cargo install --path ./flmctl -RUN cargo install --path ./flmping -RUN cargo install --path ./flmexec -FROM ubuntu:24.04 +RUN apt-get update && apt-get install -y protobuf-compiler pkg-config libssl-dev +RUN cargo build --release -RUN apt-get update && apt-get install -y wget vim iputils-ping ssh curl python3 python3-pip -COPY --from=builder /usr/local/cargo/bin/flmping /usr/local/bin/flmping -COPY --from=builder /usr/local/cargo/bin/flmctl /usr/local/bin/flmctl -COPY --from=builder /usr/local/cargo/bin/flmexec /usr/local/bin/flmexec +# Build flmadm for installation +RUN cargo install --path ./flmadm +# Copy uv before flmadm install (flmadm will detect and copy it to FLAME_HOME/bin) COPY --from=ghcr.io/astral-sh/uv:0.9.26 /uv /uvx /usr/bin/ -RUN chmod +x /usr/local/bin/* +# Install using flmadm with client profile +RUN /usr/local/cargo/bin/flmadm install \ + --src-dir /usr/src/flame \ + --prefix /usr/local/flame \ + --client \ + --skip-build \ + --force + +FROM ubuntu:24.04 + +RUN apt-get update && apt-get install -y wget vim iputils-ping ssh curl python3 python3-pip && rm -rf /var/lib/apt/lists/* + +# Copy the entire installation from builder (including uv in bin/) +COPY --from=builder /usr/local/flame /usr/local/flame -COPY ./sdk/python /usr/local/flame/sdk/python +# Add flame binaries to PATH (includes uv) +ENV PATH="/usr/local/flame/bin:${PATH}" +ENV FLAME_HOME=/usr/local/flame -# Install flamepy and pytest for test client using uv -RUN uv pip install --system --break-system-packages --no-cache /usr/local/flame/sdk/python pytest +# Install flamepy and pytest for test client using uv from FLAME_HOME +RUN /usr/local/flame/bin/uv pip install --system --break-system-packages --no-cache /usr/local/flame/sdk/python pytest CMD ["service", "ssh", "start", "-D"] diff --git a/docker/Dockerfile.fem b/docker/Dockerfile.fem index 068edbac..fe152603 100644 --- a/docker/Dockerfile.fem +++ b/docker/Dockerfile.fem @@ -4,27 +4,31 @@ WORKDIR /usr/src/flame COPY . . RUN apt-get update && apt-get install -y protobuf-compiler pkg-config libssl-dev -RUN cargo install --path ./executor_manager -RUN cargo install --path ./flmping -RUN cargo install --path ./flmexec +RUN cargo build --release -FROM ubuntu:24.04 - -RUN apt-get update && apt-get install -y python3-pip +# Build flmadm for installation +RUN cargo install --path ./flmadm -RUN mkdir -p /usr/local/flame/bin /usr/local/flame/work/tmp /usr/local/flame/sdk +# Copy uv before flmadm install (flmadm will detect and copy it to FLAME_HOME/bin) +COPY --from=ghcr.io/astral-sh/uv:0.9.26 /uv /uvx /usr/bin/ -WORKDIR /usr/local/flame/work +# Install using flmadm with worker profile +RUN /usr/local/cargo/bin/flmadm install \ + --src-dir /usr/src/flame \ + --prefix /usr/local/flame \ + --worker \ + --no-systemd \ + --skip-build \ + --force -COPY --from=ghcr.io/astral-sh/uv:0.9.26 /uv /uvx /usr/bin/ +FROM ubuntu:24.04 -COPY --from=builder /usr/local/cargo/bin/flame-executor-manager /usr/local/flame/bin/flame-executor-manager -COPY --from=builder /usr/local/cargo/bin/flmping-service /usr/local/flame/bin/flmping-service -COPY --from=builder /usr/local/cargo/bin/flmexec-service /usr/local/flame/bin/flmexec-service +RUN apt-get update && apt-get install -y python3-pip && rm -rf /var/lib/apt/lists/* -RUN chmod +x /usr/local/flame/bin/* +WORKDIR /usr/local/flame/work -COPY ./sdk/python /usr/local/flame/sdk/python +# Copy the entire installation from builder (including uv in bin/) +COPY --from=builder /usr/local/flame /usr/local/flame ENV FLAME_HOME=/usr/local/flame diff --git a/docker/Dockerfile.fsm b/docker/Dockerfile.fsm index 9efa2384..4d0d57cd 100644 --- a/docker/Dockerfile.fsm +++ b/docker/Dockerfile.fsm @@ -4,19 +4,26 @@ WORKDIR /usr/src/flame COPY . . RUN apt-get update && apt-get install -y protobuf-compiler pkg-config libssl-dev -RUN cargo install --path ./session_manager +RUN cargo build --release + +# Build flmadm for installation +RUN cargo install --path ./flmadm + +# Install using flmadm with control-plane profile +RUN /usr/local/cargo/bin/flmadm install \ + --src-dir /usr/src/flame \ + --prefix /usr/local/flame \ + --control-plane \ + --no-systemd \ + --skip-build \ + --force FROM ubuntu:24.04 -RUN mkdir -p /usr/local/flame/bin -RUN mkdir -p /usr/local/flame/work -RUN mkdir -p /usr/local/flame/migrations WORKDIR /usr/local/flame -COPY session_manager/migrations /usr/local/flame/migrations -COPY --from=builder /usr/local/cargo/bin/flame-session-manager /usr/local/flame/bin/flame-session-manager - -RUN chmod +x /usr/local/flame/bin/* +# Copy the entire installation from builder +COPY --from=builder /usr/local/flame /usr/local/flame ENV FLAME_HOME=/usr/local/flame diff --git a/docs/designs/RFE318-cache/STATUS.md b/docs/designs/RFE318-cache/STATUS.md index 559cf930..02351030 100644 --- a/docs/designs/RFE318-cache/STATUS.md +++ b/docs/designs/RFE318-cache/STATUS.md @@ -19,7 +19,7 @@ The object cache is now provided as an embedded library within the `flame-execut - ✅ In-memory index with HashMap - ✅ Object loading from disk on startup - ✅ Configuration support (flame-cluster.yaml with storage path) -- ✅ Docker integration (Dockerfile.cache, compose.yaml, Makefile) +- ✅ Docker integration (embedded in executor-manager, compose.yaml, Makefile) ### API Operations - ✅ `get_flight_info`: Returns flight metadata for objects diff --git a/e2e/tests/test_agent.py b/e2e/tests/test_agent.py index ff2d1681..2c3fc86a 100644 --- a/e2e/tests/test_agent.py +++ b/e2e/tests/test_agent.py @@ -29,7 +29,7 @@ def setup_test_env(): FLM_TEST_APP, flamepy.ApplicationAttributes( shim=flamepy.Shim.Host, - command="uv", + command="${FLAME_HOME}/bin/uv", working_directory="/opt/e2e", environments={"FLAME_LOG_LEVEL": "DEBUG"}, arguments=["run", "src/e2e/instance_svc.py", "src/e2e/api.py"], diff --git a/e2e/tests/test_core.py b/e2e/tests/test_core.py index 71c3c862..acb30e32 100644 --- a/e2e/tests/test_core.py +++ b/e2e/tests/test_core.py @@ -34,7 +34,7 @@ def setup_test_env(): FLM_TEST_SVC_APP, flamepy.ApplicationAttributes( shim=flamepy.Shim.Host, - command="uv", + command="${FLAME_HOME}/bin/uv", working_directory="/opt/e2e", environments={"FLAME_LOG_LEVEL": "DEBUG"}, arguments=["run", "src/e2e/basic_svc.py", "src/e2e/api.py"], @@ -137,7 +137,8 @@ def test_application_context_info(): "Host" in response.application_context.shim or "host" in response.application_context.shim.lower() ) - assert response.application_context.command == "uv" + # Command should use FLAME_HOME environment variable + assert response.application_context.command == "${FLAME_HOME}/bin/uv" assert response.application_context.working_directory == "/opt/e2e" assert response.application_context.url == FLM_TEST_SVC_APP_URL @@ -177,7 +178,8 @@ def test_all_context_info(): # Check application context details assert response.application_context.name == FLM_TEST_SVC_APP - assert response.application_context.command == "uv" + # Command should use FLAME_HOME environment variable + assert response.application_context.command == "${FLAME_HOME}/bin/uv" assert response.application_context.working_directory == "/opt/e2e" assert response.application_context.url == FLM_TEST_SVC_APP_URL @@ -373,7 +375,7 @@ def test_task_invoke_exception_handling(): FLM_ERROR_SVC_APP, flamepy.ApplicationAttributes( shim=flamepy.Shim.Host, - command="uv", + command="${FLAME_HOME}/bin/uv", working_directory="/opt/e2e", environments={"FLAME_LOG_LEVEL": "DEBUG"}, arguments=["run", "src/e2e/error_svc.py"], diff --git a/e2e/tests/test_flmrun.py b/e2e/tests/test_flmrun.py index 2d0c225a..b31f8c99 100644 --- a/e2e/tests/test_flmrun.py +++ b/e2e/tests/test_flmrun.py @@ -79,7 +79,8 @@ def test_flmrun_application_registered(): assert flmrun.name == FLMRUN_E2E_APP assert flmrun.shim == flamepy.Shim.Host assert flmrun.state == flamepy.ApplicationState.ENABLED - assert flmrun.command == "/usr/bin/uv" + # Command should use FLAME_HOME environment variable + assert flmrun.command == "${FLAME_HOME}/bin/uv" def test_flmrun_sum_function(): diff --git a/executor_manager/Cargo.toml b/executor_manager/Cargo.toml index 332480f0..631f725b 100644 --- a/executor_manager/Cargo.toml +++ b/executor_manager/Cargo.toml @@ -35,6 +35,7 @@ wasmtime = "16" wasmtime-wasi = "16" anyhow = "1" tokio-stream = { workspace = true } +shellexpand = "3.1" # Dependencies for embedded object cache arrow = "53" diff --git a/executor_manager/src/shims/host_shim.rs b/executor_manager/src/shims/host_shim.rs index 9c681c08..81f449d4 100644 --- a/executor_manager/src/shims/host_shim.rs +++ b/executor_manager/src/shims/host_shim.rs @@ -197,6 +197,14 @@ impl HostShim { Ok(envs) } + /// Expand environment variables in a string + /// Supports both ${VAR} and $VAR syntax + fn expand_env_vars(s: &str) -> String { + shellexpand::env(s) + .unwrap_or(std::borrow::Cow::Borrowed(s)) + .into_owned() + } + fn launch_instance( app: &ApplicationContext, executor: &Executor, @@ -204,8 +212,17 @@ impl HostShim { ) -> Result { trace_fn!("HostShim::launch_instance"); + // Expand environment variables in command and arguments let command = app.command.clone().unwrap_or_default(); - let args = app.arguments.clone(); + let command = Self::expand_env_vars(&command); + + let args: Vec = app + .arguments + .clone() + .iter() + .map(|arg| Self::expand_env_vars(arg)) + .collect(); + let log_level = env::var(RUST_LOG).unwrap_or(String::from(DEFAULT_SVC_LOG_LEVEL)); let mut envs = app.environments.clone(); diff --git a/flmadm/README.md b/flmadm/README.md index 4af5feb8..f7cb50fe 100644 --- a/flmadm/README.md +++ b/flmadm/README.md @@ -14,10 +14,10 @@ Flame provides two separate CLI tools: - **Source Building**: Builds Flame binaries from source (Rust) - **Python SDK Installation**: Automatically installs the Flame Python SDK - **Systemd Integration**: Generates and manages systemd service files -- **User Management**: Creates and manages the `flame` user for service isolation - **Configuration Generation**: Creates default configuration files - **Clean Uninstallation**: Safely removes Flame with backup support -- **Flexible Installation**: Supports both system-wide and user-local installations +- **Flexible Installation**: Supports both system-wide (root) and user-local installations +- **Installation Profiles**: Install specific components (control-plane, worker, client) ## Installation @@ -74,6 +74,30 @@ sudo flmadm install --clean --enable sudo flmadm install --verbose ``` +**Install specific profiles:** +```bash +# Control plane only (session manager, flmctl, flmadm) +sudo flmadm install --control-plane + +# Worker only (executor manager, services, SDK) +sudo flmadm install --worker + +# Client only (flmping, flmexec, SDK) +sudo flmadm install --client + +# Combined installation (control plane + worker on same node) +sudo flmadm install --control-plane --worker --enable + +# Force overwrite existing components +sudo flmadm install --worker --force + +# Client installation (no systemd needed) +flmadm install --client --prefix ~/flame + +# ERROR: Cannot use --enable with client-only +# flmadm install --client --enable # This will fail with an error +``` + ### Uninstall Flame **Basic uninstall (with backup):** @@ -93,7 +117,7 @@ sudo flmadm uninstall --prefix /opt/flame **Complete removal (no backup):** ```bash -sudo flmadm uninstall --no-backup --remove-user --force +sudo flmadm uninstall --no-backup --force ``` **Custom backup location:** @@ -105,12 +129,75 @@ sudo flmadm uninstall --backup-dir /backups/flame-backup-2026-01-28 - `--src-dir `: Source code directory for building Flame (default: clone from GitHub) - `--prefix `: Target installation directory (default: `/usr/local/flame`) +- `--control-plane`: Install control plane components only (flame-session-manager, flmctl, flmadm) +- `--worker`: Install worker components only (flame-executor-manager, flmping-service, flmexec-service, flamepy) +- `--client`: Install client components only (flmping, flmexec, flamepy) - `--no-systemd`: Skip systemd service generation (for user-local installs) - `--enable`: Enable and start systemd services after installation - `--skip-build`: Skip building from source (use pre-built binaries) - `--clean`: Remove existing installation before installing (creates backup) +- `--force`: Force overwrite existing components without prompting - `--verbose`: Show detailed build output (useful for debugging build issues) +**Note:** If no profile flags (`--control-plane`, `--worker`, `--client`) are specified, all components will be installed by default. + +## Installation Profiles + +Flame supports three installation profiles to allow flexible deployment architectures: + +### Control Plane Profile (`--control-plane`) + +Installs components required for cluster management: +- `flame-session-manager`: Main control plane service +- `flmctl`: CLI for job submission and management +- `flmadm`: Administration CLI + +**Use case:** Deploy on dedicated control plane nodes that manage the cluster but don't execute workloads. + +### Worker Profile (`--worker`) + +Installs components required for executing workloads: +- `flame-executor-manager`: Worker node service +- `flmping-service`: Health check service +- `flmexec-service`: Execution service +- `flamepy`: Python SDK + +**Use case:** Deploy on worker nodes that execute user workloads. + +### Client Profile (`--client`) + +Installs client tools for submitting jobs: +- `flmctl`: CLI for job submission and session management +- `flmping`: CLI for health checks +- `flmexec`: CLI for job execution +- `flamepy`: Python SDK + +**Use case:** Deploy on client machines or jump hosts for users to submit jobs without running services. + +**Note:** The client profile doesn't install any systemd services. The `--enable` flag is not applicable when only `--client` is specified. The `--no-systemd` flag is automatically implied for client-only installations. + +### Combined Deployments + +Profiles can be combined in a single installation: + +```bash +# Single-node deployment (all-in-one) +sudo flmadm install --control-plane --worker --enable + +# Control plane + client tools +sudo flmadm install --control-plane --client + +# Worker + client tools +sudo flmadm install --worker --client +``` + +### Component Overwrite Behavior + +When installing over an existing installation: +- Without `--force`: Prompts for confirmation before overwriting each existing component +- With `--force`: Automatically overwrites all components without prompting +- With `--clean`: Backs up and removes the entire installation before installing + ## Uninstall Options - `--prefix `: Installation directory to uninstall (default: `/usr/local/flame`) @@ -120,7 +207,6 @@ sudo flmadm uninstall --backup-dir /backups/flame-backup-2026-01-28 - `--backup-dir `: Custom backup directory - `--no-backup`: Do not create backup (PERMANENTLY DELETE - use with caution!) - `--force`: Skip confirmation prompts -- `--remove-user`: Remove the flame user and group ## Directory Structure @@ -200,9 +286,18 @@ flmadm install --src-dir . --prefix ~/flame --skip-build --no-systemd --clean ### Production Deployment ```bash -# Initial installation +# Single-node deployment (all components) sudo flmadm install --enable +# Multi-node deployment: Control plane node +sudo flmadm install --control-plane --enable + +# Multi-node deployment: Worker nodes +sudo flmadm install --worker --enable + +# Client machine (no services) +flmadm install --client --prefix ~/flame --no-systemd + # Verify installation sudo systemctl status flame-session-manager flame-executor-manager /usr/local/flame/bin/flmctl --version @@ -213,6 +308,22 @@ sudo flmadm install --clean sudo systemctl start flame-session-manager flame-executor-manager ``` +### Distributed Cluster Setup + +```bash +# On control plane node (control01) +sudo flmadm install --control-plane --enable + +# On worker nodes (worker01, worker02, ...) +sudo flmadm install --worker --enable + +# On client/jump host (for users) +flmadm install --client --prefix ~/flame --no-systemd + +# Update only worker nodes +sudo flmadm install --worker --force --skip-build +``` + ### Testing and Cleanup ```bash diff --git a/flmadm/src/commands/install.rs b/flmadm/src/commands/install.rs index 3f3b0f0f..f833362d 100644 --- a/flmadm/src/commands/install.rs +++ b/flmadm/src/commands/install.rs @@ -9,6 +9,15 @@ use anyhow::Result; pub fn run(config: InstallConfig) -> Result<()> { println!("🚀 Flame Installation"); println!(" Target: {}", config.prefix.display()); + println!(" Profiles:"); + for profile in &config.profiles { + let profile_name = match profile { + crate::types::InstallProfile::ControlPlane => "Control Plane", + crate::types::InstallProfile::Worker => "Worker", + crate::types::InstallProfile::Client => "Client", + }; + println!(" • {}", profile_name); + } println!(); // Phase 1: Validation @@ -46,12 +55,22 @@ pub fn run(config: InstallConfig) -> Result<()> { install_components(&artifacts, &src_dir, &paths, &config)?; } - // Phase 5: Systemd Setup (if requested) - if config.systemd { + // Phase 5: Systemd Setup (if requested and needed) + let has_control_plane = config + .profiles + .contains(&crate::types::InstallProfile::ControlPlane); + let has_worker = config + .profiles + .contains(&crate::types::InstallProfile::Worker); + let needs_systemd = has_control_plane || has_worker; + + if config.systemd && needs_systemd { println!("\n═══ Phase 5: Systemd Setup ═══"); setup_systemd(&paths, &config)?; - } else { + } else if !config.systemd { println!("\n═══ Phase 5: Skipping Systemd (--no-systemd) ═══"); + } else { + println!("\n═══ Phase 5: Skipping Systemd (no services to install) ═══"); } // Phase 6: Summary @@ -67,9 +86,36 @@ fn validate_config(config: &InstallConfig) -> Result<()> { anyhow::bail!("Installation prefix must be an absolute path"); } - // Check if we need root privileges + // Check if profiles require systemd services + let has_control_plane = config + .profiles + .contains(&crate::types::InstallProfile::ControlPlane); + let has_worker = config + .profiles + .contains(&crate::types::InstallProfile::Worker); + let has_client_only = config.profiles.len() == 1 + && config + .profiles + .contains(&crate::types::InstallProfile::Client); + + // Check if client-only installation is combined with systemd flags + if has_client_only && config.systemd { + println!( + "ℹ️ Note: Client profile doesn't install services. Ignoring systemd configuration." + ); + } + + if has_client_only && config.enable { + anyhow::bail!( + "Cannot use --enable with --client profile only.\n \ + The client profile doesn't install any services.\n \ + Use --control-plane and/or --worker to install services." + ); + } + + // Check if we need root privileges for systemd let user_manager = UserManager::new(); - if config.systemd && !user_manager.is_root() { + if config.systemd && (has_control_plane || has_worker) && !user_manager.is_root() { anyhow::bail!( "Root privileges required for system-wide installation with systemd.\n Run with sudo or use --no-systemd for user-local installation." ); @@ -80,21 +126,69 @@ fn validate_config(config: &InstallConfig) -> Result<()> { anyhow::bail!("Cannot use --enable without systemd (--no-systemd conflicts with --enable)"); } - // Check if uv is available at /usr/bin/uv (required by flmrun) - let uv_path = std::path::Path::new("/usr/bin/uv"); - if !uv_path.exists() { - anyhow::bail!( - "uv is not installed at /usr/bin/uv (required by flmrun service)\n\ - Please install uv using one of these methods:\n\ - 1. curl -LsSf https://astral.sh/uv/install.sh | sh && sudo cp ~/.local/bin/uv /usr/bin/uv\n\ - 2. Or install uv via your package manager and ensure it's at /usr/bin/uv" - ); + // Check if uv is available (required by worker and client profiles) + let needs_uv = has_worker + || config + .profiles + .contains(&crate::types::InstallProfile::Client); + if needs_uv { + match find_uv_executable() { + Some(uv_path) => { + println!("✓ Found uv at: {}", uv_path.display()); + } + None => { + anyhow::bail!( + "uv is not found in PATH (required by worker and client profiles)\n\ + Please install uv using one of these methods:\n\ + 1. curl -LsSf https://astral.sh/uv/install.sh | sh\n\ + 2. Or install uv via your package manager" + ); + } + } } println!("✓ Configuration validated"); Ok(()) } +/// Find uv executable in the system PATH +fn find_uv_executable() -> Option { + use std::process::Command; + + // Try to find uv using 'which' command + if let Ok(output) = Command::new("which").arg("uv").output() { + if output.status.success() { + let path_str = String::from_utf8_lossy(&output.stdout); + let path = path_str.trim(); + if !path.is_empty() { + return Some(std::path::PathBuf::from(path)); + } + } + } + + // Fallback: check common locations + for common_path in [ + "/usr/bin/uv", + "/usr/local/bin/uv", + "/opt/homebrew/bin/uv", // macOS Homebrew + ] { + let path = std::path::Path::new(common_path); + if path.exists() { + return Some(path.to_path_buf()); + } + } + + // Try to find in $HOME/.local/bin (common user install location) + if let Ok(home) = std::env::var("HOME") { + let user_uv = std::path::PathBuf::from(home).join(".local/bin/uv"); + if user_uv.exists() { + return Some(user_uv); + } + } + + None +} + fn handle_clean_install(paths: &InstallationPaths) -> Result<()> { println!("🧹 Clean installation requested"); @@ -122,24 +216,31 @@ fn install_components( paths: &InstallationPaths, config: &InstallConfig, ) -> Result<()> { - // Create flame user (for system-wide installation) - must be done before creating directories - let user_manager = UserManager::new(); - if config.systemd && user_manager.is_root() { - user_manager.create_user()?; - } - // Create directories let installation_manager = InstallationManager::new(); installation_manager.create_directories(paths)?; // Install binaries - installation_manager.install_binaries(artifacts, paths)?; + installation_manager.install_binaries( + artifacts, + paths, + &config.profiles, + config.force_overwrite, + )?; + + // Install uv (for worker and client profiles) + installation_manager.install_uv(paths, &config.profiles)?; // Install Python SDK - installation_manager.install_python_sdk(src_dir, paths)?; + installation_manager.install_python_sdk( + src_dir, + paths, + &config.profiles, + config.force_overwrite, + )?; // Install database migrations - installation_manager.install_migrations(src_dir, paths)?; + installation_manager.install_migrations(src_dir, paths, &config.profiles)?; // Generate configuration let config_generator = ConfigGenerator::new(); @@ -152,14 +253,27 @@ fn setup_systemd(paths: &InstallationPaths, config: &InstallConfig) -> Result<() let systemd_manager = SystemdManager::new(); // Install service files - systemd_manager.install_services(&paths.prefix)?; + systemd_manager.install_services(&paths.prefix, &config.profiles)?; // Enable and start services if requested if config.enable { - systemd_manager.enable_and_start_services()?; + systemd_manager.enable_and_start_services(&config.profiles)?; } else { + let has_control_plane = config + .profiles + .contains(&crate::types::InstallProfile::ControlPlane); + let has_worker = config + .profiles + .contains(&crate::types::InstallProfile::Worker); + println!("ℹ️ Services installed but not enabled. To start services:"); - println!(" sudo systemctl enable --now flame-session-manager flame-executor-manager"); + if has_control_plane && has_worker { + println!(" sudo systemctl enable --now flame-session-manager flame-executor-manager"); + } else if has_control_plane { + println!(" sudo systemctl enable --now flame-session-manager"); + } else if has_worker { + println!(" sudo systemctl enable --now flame-executor-manager"); + } } Ok(()) @@ -170,41 +284,99 @@ fn print_summary(paths: &InstallationPaths, config: &InstallConfig) { println!(); println!("Installation Details:"); println!(" • Installation prefix: {}", paths.prefix.display()); + println!(" • Installation profiles:"); + + // Show which profiles were installed + for profile in &config.profiles { + let profile_name = match profile { + crate::types::InstallProfile::ControlPlane => "Control Plane", + crate::types::InstallProfile::Worker => "Worker", + crate::types::InstallProfile::Client => "Client", + }; + println!( + " - {}: {}", + profile_name, + profile.components().join(", ") + ); + } + + println!(); println!(" • Binaries: {}", paths.bin.display()); println!( " • Configuration: {}", paths.conf.join("flame-cluster.yaml").display() ); - println!(" • Python SDK: {}", paths.sdk_python.display()); + + // Only show SDK path if it was installed + let has_flamepy = config + .profiles + .iter() + .any(|p| p.includes_component("flamepy")); + if has_flamepy { + println!(" • Python SDK: {}", paths.sdk_python.display()); + } println!(); - if config.systemd { + let has_control_plane = config + .profiles + .contains(&crate::types::InstallProfile::ControlPlane); + let has_worker = config + .profiles + .contains(&crate::types::InstallProfile::Worker); + + if config.systemd && (has_control_plane || has_worker) { println!("Systemd Services:"); if config.enable { - println!(" • flame-session-manager: enabled and running"); - println!(" • flame-executor-manager: enabled and running"); + if has_control_plane { + println!(" • flame-session-manager: enabled and running"); + } + if has_worker { + println!(" • flame-executor-manager: enabled and running"); + } println!(); println!("To check service status:"); - println!(" sudo systemctl status flame-session-manager"); - println!(" sudo systemctl status flame-executor-manager"); + if has_control_plane { + println!(" sudo systemctl status flame-session-manager"); + } + if has_worker { + println!(" sudo systemctl status flame-executor-manager"); + } } else { - println!(" • flame-session-manager: installed (not enabled)"); - println!(" • flame-executor-manager: installed (not enabled)"); + if has_control_plane { + println!(" • flame-session-manager: installed (not enabled)"); + } + if has_worker { + println!(" • flame-executor-manager: installed (not enabled)"); + } println!(); println!("To start services:"); - println!(" sudo systemctl enable --now flame-session-manager"); - println!(" sudo systemctl enable --now flame-executor-manager"); + if has_control_plane { + println!(" sudo systemctl enable --now flame-session-manager"); + } + if has_worker { + println!(" sudo systemctl enable --now flame-executor-manager"); + } } println!(); println!("To view logs:"); - println!(" sudo journalctl -u flame-session-manager -f"); - println!(" tail -f {}/logs/fsm.log", paths.prefix.display()); - } else { + if has_control_plane { + println!(" sudo journalctl -u flame-session-manager -f"); + println!(" tail -f {}/logs/fsm.log", paths.prefix.display()); + } + if has_worker { + println!(" sudo journalctl -u flame-executor-manager -f"); + println!(" tail -f {}/logs/fem.log", paths.prefix.display()); + } + } else if !config.systemd && (has_control_plane || has_worker) { println!("Manual Service Management:"); - println!(" • Start session manager: {}/bin/flame-session-manager --config {}/conf/flame-cluster.yaml", - paths.prefix.display(), paths.prefix.display()); - println!(" • Start executor manager: {}/bin/flame-executor-manager --config {}/conf/flame-cluster.yaml", - paths.prefix.display(), paths.prefix.display()); + if has_control_plane { + println!(" • Start session manager: {}/bin/flame-session-manager --config {}/conf/flame-cluster.yaml", + paths.prefix.display(), paths.prefix.display()); + } + if has_worker { + println!(" • Start executor manager: {}/bin/flame-executor-manager --config {}/conf/flame-cluster.yaml", + paths.prefix.display(), paths.prefix.display()); + } } println!(); @@ -214,9 +386,21 @@ fn print_summary(paths: &InstallationPaths, config: &InstallConfig) { paths.prefix.display() ); println!(" 2. Add {}/bin to your PATH", paths.bin.display()); - println!( - " 3. Test the installation: {}/bin/flmctl --version", - paths.bin.display() - ); + + // Provide relevant test command based on what was installed + if has_control_plane { + println!( + " 3. Test the installation: {}/bin/flmctl --version", + paths.bin.display() + ); + } else if config + .profiles + .contains(&crate::types::InstallProfile::Client) + { + println!( + " 3. Test the installation: {}/bin/flmping --version", + paths.bin.display() + ); + } println!(); } diff --git a/flmadm/src/commands/uninstall.rs b/flmadm/src/commands/uninstall.rs index 9b0ae918..ea7f7228 100644 --- a/flmadm/src/commands/uninstall.rs +++ b/flmadm/src/commands/uninstall.rs @@ -1,6 +1,5 @@ use crate::managers::{ backup::BackupManager, installation::InstallationManager, systemd::SystemdManager, - user::UserManager, }; use crate::types::{InstallationPaths, UninstallConfig}; use anyhow::Result; @@ -32,13 +31,7 @@ pub fn run(config: UninstallConfig) -> Result<()> { println!("\n═══ Phase 4: Remove Installation ═══"); remove_installation(&paths, &config)?; - // Phase 5: Remove User (if requested) - if config.remove_user { - println!("\n═══ Phase 5: Remove User ═══"); - remove_user()?; - } - - // Phase 6: Summary + // Phase 5: Summary println!("\n═══ Uninstallation Complete ═══"); print_summary(&paths, &config, backup_dir); @@ -217,12 +210,6 @@ fn remove_installation(paths: &InstallationPaths, config: &UninstallConfig) -> R ) } -fn remove_user() -> Result<()> { - let user_manager = UserManager::new(); - user_manager.remove_user(false)?; - Ok(()) -} - fn print_summary( paths: &InstallationPaths, config: &UninstallConfig, diff --git a/flmadm/src/main.rs b/flmadm/src/main.rs index 93bbd03d..d12211b8 100644 --- a/flmadm/src/main.rs +++ b/flmadm/src/main.rs @@ -28,6 +28,18 @@ enum Commands { #[arg(long, default_value = "/usr/local/flame", value_name = "PATH")] prefix: PathBuf, + /// Install control plane components (flame-session-manager, flmctl, flmadm) + #[arg(long)] + control_plane: bool, + + /// Install worker components (flame-executor-manager, flmping-service, flmexec-service, flamepy) + #[arg(long)] + worker: bool, + + /// Install client components (flmping, flmexec, flamepy) + #[arg(long)] + client: bool, + /// Skip systemd service generation #[arg(long)] no_systemd: bool, @@ -44,6 +56,10 @@ enum Commands { #[arg(long)] clean: bool, + /// Force overwrite existing components without prompting + #[arg(long)] + force: bool, + /// Show detailed build output #[arg(long)] verbose: bool, @@ -78,10 +94,6 @@ enum Commands { /// Skip confirmation prompts #[arg(long)] force: bool, - - /// Remove the flame user and group - #[arg(long)] - remove_user: bool, }, } @@ -98,12 +110,39 @@ fn main() { Commands::Install { src_dir, prefix, + control_plane, + worker, + client, no_systemd, enable, skip_build, clean, + force, verbose, } => { + // Determine which profiles to install + let profiles = if control_plane || worker || client { + // If any profile flag is specified, only install those profiles + let mut profiles = Vec::new(); + if control_plane { + profiles.push(types::InstallProfile::ControlPlane); + } + if worker { + profiles.push(types::InstallProfile::Worker); + } + if client { + profiles.push(types::InstallProfile::Client); + } + profiles + } else { + // If no profile flags are specified, install all profiles (default behavior) + vec![ + types::InstallProfile::ControlPlane, + types::InstallProfile::Worker, + types::InstallProfile::Client, + ] + }; + let config = types::InstallConfig { src_dir, prefix, @@ -112,6 +151,8 @@ fn main() { skip_build, clean, verbose, + profiles, + force_overwrite: force, }; commands::install::run(config) } @@ -123,7 +164,6 @@ fn main() { backup_dir, no_backup, force, - remove_user, } => { let config = types::UninstallConfig { prefix, @@ -133,7 +173,6 @@ fn main() { backup_dir, no_backup, force, - remove_user, }; commands::uninstall::run(config) } diff --git a/flmadm/src/managers/installation.rs b/flmadm/src/managers/installation.rs index 97eb44eb..6780f2ce 100644 --- a/flmadm/src/managers/installation.rs +++ b/flmadm/src/managers/installation.rs @@ -1,18 +1,15 @@ -use crate::types::{BuildArtifacts, InstallationPaths}; +use crate::types::{BuildArtifacts, InstallProfile, InstallationPaths}; use anyhow::{Context, Result}; use std::fs; +use std::io::{self, Write}; use std::os::unix::fs::PermissionsExt; use std::path::Path; -pub struct InstallationManager { - user_manager: super::user::UserManager, -} +pub struct InstallationManager; impl InstallationManager { pub fn new() -> Self { - Self { - user_manager: super::user::UserManager::new(), - } + Self } /// Create all required directories @@ -55,25 +52,6 @@ impl InstallationManager { fs::set_permissions(&paths.data, data_perms) .context("Failed to set data directory permissions")?; - // Set ownership if running as root - if self.user_manager.is_root() { - for path in [ - &paths.work, - &paths.logs, - &paths.data, - &paths.conf, - &paths.migrations, - ] { - self.user_manager.set_ownership(path)?; - } - - // Set ownership for SDK directory parent (sdk/python directory will be set later) - let sdk_parent = paths.sdk_python.parent().unwrap().parent().unwrap(); // ${PREFIX}/sdk - if sdk_parent.exists() { - self.user_manager.set_ownership(sdk_parent)?; - } - } - Ok(()) } @@ -82,10 +60,15 @@ impl InstallationManager { &self, artifacts: &BuildArtifacts, paths: &InstallationPaths, + profiles: &[InstallProfile], + force_overwrite: bool, ) -> Result<()> { println!("📦 Installing binaries..."); - for (name, src, dst) in [ + // Check which components should be installed based on profiles + let components_to_install = self.get_components_to_install(profiles); + + let all_binaries = [ ( "flame-session-manager", &artifacts.session_manager, @@ -110,7 +93,21 @@ impl InstallationManager { &artifacts.flmexec_service, paths.bin.join("flmexec-service"), ), - ] { + ]; + + for (name, src, dst) in all_binaries { + // Skip components that are not in any of the selected profiles + if !components_to_install.iter().any(|c| c == name) { + println!(" ⊘ Skipped {} (not in selected profiles)", name); + continue; + } + + // Check if the file already exists + if dst.exists() && !force_overwrite && !self.prompt_overwrite(name)? { + println!(" ⊘ Skipped {} (already exists)", name); + continue; + } + fs::copy(src, &dst).context(format!("Failed to copy {} binary", name))?; // Set executable permissions @@ -121,16 +118,50 @@ impl InstallationManager { println!(" ✓ Installed {}", name); } - // Set ownership if running as root - if self.user_manager.is_root() { - self.user_manager.set_ownership(&paths.bin)?; + Ok(()) + } + + /// Get all components that should be installed based on the profiles + fn get_components_to_install(&self, profiles: &[InstallProfile]) -> Vec { + let mut components = Vec::new(); + for profile in profiles { + for component in profile.components() { + let component_str = component.to_string(); + if !components.contains(&component_str) { + components.push(component_str); + } + } } + components + } - Ok(()) + /// Prompt the user whether to overwrite an existing file + fn prompt_overwrite(&self, component: &str) -> Result { + print!(" ⚠️ {} already exists. Overwrite? [y/N]: ", component); + io::stdout().flush()?; + + let mut input = String::new(); + io::stdin().read_line(&mut input)?; + + let response = input.trim().to_lowercase(); + Ok(response == "y" || response == "yes") } /// Install Python SDK - pub fn install_python_sdk(&self, src_dir: &Path, paths: &InstallationPaths) -> Result<()> { + pub fn install_python_sdk( + &self, + src_dir: &Path, + paths: &InstallationPaths, + profiles: &[InstallProfile], + force_overwrite: bool, + ) -> Result<()> { + // Check if any profile requires flamepy + let components_to_install = self.get_components_to_install(profiles); + if !components_to_install.iter().any(|c| c == "flamepy") { + println!("⊘ Skipped Python SDK (not in selected profiles)"); + return Ok(()); + } + println!("🐍 Installing Python SDK..."); let sdk_src = src_dir.join("sdk/python"); @@ -138,16 +169,34 @@ impl InstallationManager { anyhow::bail!("Python SDK source not found at: {:?}", sdk_src); } + // Check if SDK already exists + if paths.sdk_python.exists() && !force_overwrite { + print!( + " ⚠️ Python SDK already exists at {}. Overwrite? [y/N]: ", + paths.sdk_python.display() + ); + io::stdout().flush()?; + + let mut input = String::new(); + io::stdin().read_line(&mut input)?; + + let response = input.trim().to_lowercase(); + if response != "y" && response != "yes" { + println!(" ⊘ Skipped Python SDK (already exists)"); + return Ok(()); + } + + // Remove existing SDK before copying + if paths.sdk_python.exists() { + fs::remove_dir_all(&paths.sdk_python).context("Failed to remove existing SDK")?; + } + } + // Copy SDK source to the installation directory, excluding development artifacts // uv will use this directly with --with "flamepy @ file://..." self.copy_sdk_excluding_artifacts(&sdk_src, &paths.sdk_python) .context("Failed to copy SDK to installation directory")?; - // Set ownership if running as root - if self.user_manager.is_root() { - self.user_manager.set_ownership(&paths.sdk_python)?; - } - println!("✓ Copied Python SDK to: {}", paths.sdk_python.display()); // Create a note in the sdk_python directory for reference @@ -223,7 +272,18 @@ impl InstallationManager { } /// Install database migrations - pub fn install_migrations(&self, src_dir: &Path, paths: &InstallationPaths) -> Result<()> { + pub fn install_migrations( + &self, + src_dir: &Path, + paths: &InstallationPaths, + profiles: &[InstallProfile], + ) -> Result<()> { + // Migrations are only needed for control plane + if !profiles.contains(&InstallProfile::ControlPlane) { + println!("⊘ Skipped database migrations (not in selected profiles)"); + return Ok(()); + } + println!("🗄️ Installing database migrations..."); let migrations_src = src_dir.join("session_manager/migrations/sqlite"); @@ -248,6 +308,77 @@ impl InstallationManager { Ok(()) } + /// Install uv tool + pub fn install_uv(&self, paths: &InstallationPaths, profiles: &[InstallProfile]) -> Result<()> { + // UV is only needed for worker and client profiles + let needs_uv = profiles.contains(&InstallProfile::Worker) + || profiles.contains(&InstallProfile::Client); + + if !needs_uv { + println!("⊘ Skipped uv installation (not in selected profiles)"); + return Ok(()); + } + + println!("🔧 Installing uv..."); + + // Find uv in the system + let uv_src = self.find_uv_executable().context( + "uv not found in system. Please install uv first:\n\ + 1. curl -LsSf https://astral.sh/uv/install.sh | sh\n\ + 2. Or install via your package manager", + )?; + + let uv_dst = paths.bin.join("uv"); + + // Copy uv to installation directory + fs::copy(&uv_src, &uv_dst).context("Failed to copy uv binary")?; + + // Set executable permissions + let perms = fs::Permissions::from_mode(0o755); + fs::set_permissions(&uv_dst, perms).context("Failed to set permissions on uv")?; + + println!(" ✓ Installed uv from {}", uv_src.display()); + Ok(()) + } + + /// Find uv executable in the system + fn find_uv_executable(&self) -> Result { + use std::process::Command; + + // Try to find uv using 'which' command + if let Ok(output) = Command::new("which").arg("uv").output() { + if output.status.success() { + let path_str = String::from_utf8_lossy(&output.stdout); + let path = path_str.trim(); + if !path.is_empty() { + return Ok(std::path::PathBuf::from(path)); + } + } + } + + // Fallback: check common locations + for common_path in [ + "/usr/bin/uv", + "/usr/local/bin/uv", + "/opt/homebrew/bin/uv", // macOS Homebrew + ] { + let path = std::path::Path::new(common_path); + if path.exists() { + return Ok(path.to_path_buf()); + } + } + + // Try to find in $HOME/.local/bin (common user install location) + if let Ok(home) = std::env::var("HOME") { + let user_uv = std::path::PathBuf::from(home).join(".local/bin/uv"); + if user_uv.exists() { + return Ok(user_uv); + } + } + + anyhow::bail!("uv executable not found in system") + } + /// Remove the installation directory pub fn remove_installation( &self, diff --git a/flmadm/src/managers/systemd.rs b/flmadm/src/managers/systemd.rs index da1a76aa..9b4ba205 100644 --- a/flmadm/src/managers/systemd.rs +++ b/flmadm/src/managers/systemd.rs @@ -1,3 +1,4 @@ +use crate::types::InstallProfile; use anyhow::{Context, Result}; use std::fs; use std::path::{Path, PathBuf}; @@ -11,26 +12,39 @@ impl SystemdManager { } /// Generate and install systemd service files - pub fn install_services(&self, prefix: &Path) -> Result<()> { + pub fn install_services(&self, prefix: &Path, profiles: &[InstallProfile]) -> Result<()> { println!("⚙️ Installing systemd service files..."); let prefix_str = prefix.to_str().unwrap(); - // Generate service files - let fsm_service = self.generate_session_manager_service(prefix_str); - let fem_service = self.generate_executor_manager_service(prefix_str); + let has_control_plane = profiles.contains(&InstallProfile::ControlPlane); + let has_worker = profiles.contains(&InstallProfile::Worker); // Write to /etc/systemd/system/ - let fsm_path = PathBuf::from("/etc/systemd/system/flame-session-manager.service"); - let fem_path = PathBuf::from("/etc/systemd/system/flame-executor-manager.service"); + if has_control_plane { + let fsm_service = self.generate_session_manager_service(prefix_str); + let fsm_path = PathBuf::from("/etc/systemd/system/flame-session-manager.service"); + fs::write(&fsm_path, fsm_service) + .context("Failed to write flame-session-manager.service")?; + println!(" ✓ Installed flame-session-manager.service"); + } else { + println!(" ⊘ Skipped flame-session-manager.service (control plane not selected)"); + } - fs::write(&fsm_path, fsm_service) - .context("Failed to write flame-session-manager.service")?; - fs::write(&fem_path, fem_service) - .context("Failed to write flame-executor-manager.service")?; + if has_worker { + let fem_service = self.generate_executor_manager_service(prefix_str); + let fem_path = PathBuf::from("/etc/systemd/system/flame-executor-manager.service"); + fs::write(&fem_path, fem_service) + .context("Failed to write flame-executor-manager.service")?; + println!(" ✓ Installed flame-executor-manager.service"); + } else { + println!(" ⊘ Skipped flame-executor-manager.service (worker not selected)"); + } - // Reload systemd daemon - self.daemon_reload()?; + // Only reload if we installed at least one service + if has_control_plane || has_worker { + self.daemon_reload()?; + } println!("✓ Installed systemd service files"); Ok(()) @@ -68,24 +82,25 @@ impl SystemdManager { } /// Enable and start systemd services - pub fn enable_and_start_services(&self) -> Result<()> { + pub fn enable_and_start_services(&self, profiles: &[InstallProfile]) -> Result<()> { println!("🚀 Enabling and starting Flame services..."); - // Enable services - self.enable_service("flame-session-manager")?; - self.enable_service("flame-executor-manager")?; - - // Start session manager first - self.start_service("flame-session-manager")?; + let has_control_plane = profiles.contains(&InstallProfile::ControlPlane); + let has_worker = profiles.contains(&InstallProfile::Worker); - // Wait for session manager to be ready (with retry) - self.wait_for_service_active("flame-session-manager", 15)?; - - // Start executor manager - self.start_service("flame-executor-manager")?; + if has_control_plane { + // Enable and start session manager + self.enable_service("flame-session-manager")?; + self.start_service("flame-session-manager")?; + self.wait_for_service_active("flame-session-manager", 15)?; + } - // Wait for executor manager to be ready (with retry) - self.wait_for_service_active("flame-executor-manager", 15)?; + if has_worker { + // Enable and start executor manager + self.enable_service("flame-executor-manager")?; + self.start_service("flame-executor-manager")?; + self.wait_for_service_active("flame-executor-manager", 15)?; + } println!("✓ Services are running"); Ok(()) @@ -245,10 +260,7 @@ Wants=network-online.target [Service] Type=simple -User=flame -Group=flame Environment="RUST_LOG=info" -Environment="HOME=/var/lib/flame" Environment="FLAME_HOME={prefix}" WorkingDirectory={prefix} ExecStart={prefix}/bin/flame-session-manager --config {prefix}/conf/flame-cluster.yaml @@ -276,10 +288,7 @@ Requires=flame-session-manager.service [Service] Type=simple -User=flame -Group=flame Environment="RUST_LOG=info" -Environment="HOME=/var/lib/flame" Environment="FLAME_HOME={prefix}" WorkingDirectory={prefix}/work ExecStart={prefix}/bin/flame-executor-manager --config {prefix}/conf/flame-cluster.yaml diff --git a/flmadm/src/managers/user.rs b/flmadm/src/managers/user.rs index 694b7f2a..46df5cb4 100644 --- a/flmadm/src/managers/user.rs +++ b/flmadm/src/managers/user.rs @@ -1,6 +1,3 @@ -use anyhow::{Context, Result}; -use std::process::Command; - pub struct UserManager; impl UserManager { @@ -8,136 +5,8 @@ impl UserManager { Self } - /// Check if the flame user exists - pub fn user_exists(&self) -> Result { - let output = Command::new("id") - .arg("flame") - .output() - .context("Failed to check if flame user exists")?; - - Ok(output.status.success()) - } - - /// Create the flame user and group for system installation - pub fn create_user(&self) -> Result<()> { - if self.user_exists()? { - println!("✓ Flame user already exists"); - return Ok(()); - } - - println!("👤 Creating flame user and group..."); - - // Create flame group - let output = Command::new("groupadd") - .args(["--system", "flame"]) - .output() - .context("Failed to create flame group")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - // Group might already exist, check if that's the error - if !stderr.contains("already exists") { - anyhow::bail!("Failed to create flame group: {}", stderr); - } - } - - // Create flame user with home directory for Python packages - let output = Command::new("useradd") - .args([ - "--system", - "--create-home", - "--home-dir", - "/var/lib/flame", - "--gid", - "flame", - "--shell", - "/bin/bash", - "flame", - ]) - .output() - .context("Failed to create flame user")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - anyhow::bail!("Failed to create flame user: {}", stderr); - } - - println!("✓ Created flame user and group with home directory"); - Ok(()) - } - - /// Remove the flame user and group - pub fn remove_user(&self, force: bool) -> Result<()> { - if !self.user_exists()? { - return Ok(()); - } - - // Check for running processes - if !force { - let output = Command::new("ps") - .args(["-U", "flame", "-o", "pid,cmd", "--no-headers"]) - .output() - .context("Failed to check for flame user processes")?; - - let processes = String::from_utf8_lossy(&output.stdout); - if !processes.trim().is_empty() { - println!("⚠️ Warning: flame user has running processes:"); - println!("{}", processes); - println!(" User was not removed. Stop these processes first."); - return Ok(()); - } - } - - println!("🗑️ Removing flame user and group..."); - - // Remove user - let output = Command::new("userdel") - .arg("flame") - .output() - .context("Failed to remove flame user")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - println!("⚠️ Warning: Failed to remove flame user: {}", stderr); - } - - // Remove group - let output = Command::new("groupdel") - .arg("flame") - .output() - .context("Failed to remove flame group")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - println!("⚠️ Warning: Failed to remove flame group: {}", stderr); - } else { - println!("✓ Removed flame user and group"); - } - - Ok(()) - } - /// Check if we're running as root pub fn is_root(&self) -> bool { users::get_current_uid() == 0 } - - /// Set ownership of a path to flame:flame - pub fn set_ownership(&self, path: &std::path::Path) -> Result<()> { - if !self.is_root() { - return Ok(()); // Skip if not root - } - - let output = Command::new("chown") - .args(["-R", "flame:flame", path.to_str().unwrap()]) - .output() - .context("Failed to set ownership")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - anyhow::bail!("Failed to set ownership: {}", stderr); - } - - Ok(()) - } } diff --git a/flmadm/src/types.rs b/flmadm/src/types.rs index 693baf01..dc068f93 100644 --- a/flmadm/src/types.rs +++ b/flmadm/src/types.rs @@ -1,5 +1,34 @@ use std::path::{Path, PathBuf}; +/// Installation profiles for different deployment scenarios +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InstallProfile { + ControlPlane, + Worker, + Client, +} + +impl InstallProfile { + /// Get the components that should be installed for this profile + pub fn components(&self) -> &[&str] { + match self { + InstallProfile::ControlPlane => &["flame-session-manager", "flmctl", "flmadm"], + InstallProfile::Worker => &[ + "flame-executor-manager", + "flmping-service", + "flmexec-service", + "flamepy", + ], + InstallProfile::Client => &["flmctl", "flmping", "flmexec", "flamepy"], + } + } + + /// Check if a component should be installed for this profile + pub fn includes_component(&self, component: &str) -> bool { + self.components().contains(&component) + } +} + /// Configuration for the install command #[derive(Debug, Clone)] pub struct InstallConfig { @@ -10,6 +39,8 @@ pub struct InstallConfig { pub skip_build: bool, pub clean: bool, pub verbose: bool, + pub profiles: Vec, + pub force_overwrite: bool, } impl Default for InstallConfig { @@ -22,6 +53,12 @@ impl Default for InstallConfig { skip_build: false, clean: false, verbose: false, + profiles: vec![ + InstallProfile::ControlPlane, + InstallProfile::Worker, + InstallProfile::Client, + ], + force_overwrite: false, } } } @@ -36,7 +73,6 @@ pub struct UninstallConfig { pub backup_dir: Option, pub no_backup: bool, pub force: bool, - pub remove_user: bool, } impl Default for UninstallConfig { @@ -49,7 +85,6 @@ impl Default for UninstallConfig { backup_dir: None, no_backup: false, force: false, - remove_user: false, } } } diff --git a/flmexec/src/script/lang/python.rs b/flmexec/src/script/lang/python.rs index c5bb702b..d47383d3 100644 --- a/flmexec/src/script/lang/python.rs +++ b/flmexec/src/script/lang/python.rs @@ -29,7 +29,19 @@ use crate::api::{Script, ScriptRuntime}; use crate::script::ScriptEngine; const DEFAULT_ENTRYPOINT: &str = "main.py"; -const UV_CMD: &str = "/usr/bin/uv"; + +/// Get the uv command path from FLAME_HOME or fallback to system uv +fn get_uv_cmd() -> String { + let flame_home = std::env::var("FLAME_HOME").unwrap_or_else(|_| "/usr/local/flame".to_string()); + let uv_path = format!("{}/bin/uv", flame_home); + + // Check if uv exists in FLAME_HOME, otherwise fallback to system uv + if std::path::Path::new(&uv_path).exists() { + uv_path + } else { + "/usr/bin/uv".to_string() + } +} pub struct PythonScript { runtime: ScriptRuntime, @@ -73,7 +85,10 @@ impl ScriptEngine for PythonScript { tracing::debug!("Running script: {}", self.runtime.entrypoint); tracing::debug!("Work directory: {}", self.runtime.work_dir); - let mut child = Command::new(UV_CMD) + let uv_cmd = get_uv_cmd(); + tracing::debug!("Using uv from: {}", uv_cmd); + + let mut child = Command::new(uv_cmd) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .current_dir(&self.runtime.work_dir) diff --git a/object_cache/README.md b/object_cache/README.md index 9253539f..d8e31423 100644 --- a/object_cache/README.md +++ b/object_cache/README.md @@ -87,18 +87,20 @@ The cache server implements the Arrow Flight protocol: ## Building +The cache is built as part of the executor-manager: + ```bash -# Build the cache service -cargo build --package flame-cache --release +# Build the cache library (part of executor-manager build) +cargo build --package object_cache --release -# Build Docker image -docker build -t xflops/flame-object-cache:latest -f docker/Dockerfile.cache . +# Or build the full executor-manager +cargo build --package executor_manager --release ``` ## Running with Docker Compose ```bash -# Start all services (cache runs in executor-manager) +# Start all services (cache runs embedded in executor-manager) docker compose up -d # View cache logs (part of executor-manager logs) From cb9a7c100de11f9b35510149e918fd2e87d88320 Mon Sep 17 00:00:00 2001 From: Klaus Ma Date: Sat, 31 Jan 2026 09:15:22 +0000 Subject: [PATCH 2/2] remove prompt when install all. Signed-off-by: Klaus Ma --- .github/workflows/e2e-bm.yaml | 2 +- flmadm/README.md | 22 ++++++++++---- flmadm/src/main.rs | 45 +++++++++++++++++++++++------ flmadm/src/managers/installation.rs | 2 +- 4 files changed, 54 insertions(+), 17 deletions(-) diff --git a/.github/workflows/e2e-bm.yaml b/.github/workflows/e2e-bm.yaml index dff6cadf..de9a5786 100644 --- a/.github/workflows/e2e-bm.yaml +++ b/.github/workflows/e2e-bm.yaml @@ -63,7 +63,7 @@ jobs: - name: Install Flame with flmadm and systemd run: | - sudo ./target/release/flmadm install --src-dir . --skip-build --prefix $INSTALL_PREFIX --enable + sudo ./target/release/flmadm install --all --src-dir . --skip-build --prefix $INSTALL_PREFIX --enable echo "$INSTALL_PREFIX/bin" >> $GITHUB_PATH - name: Setup local configuration and packages directory diff --git a/flmadm/README.md b/flmadm/README.md index f7cb50fe..a1eb2ac1 100644 --- a/flmadm/README.md +++ b/flmadm/README.md @@ -39,24 +39,24 @@ sudo flmadm install ### Install Flame -**Basic installation (from GitHub):** +**Basic installation (all components):** ```bash -sudo flmadm install +sudo flmadm install --all ``` **Install from local source:** ```bash -sudo flmadm install --src-dir /path/to/flame +sudo flmadm install --all --src-dir /path/to/flame ``` **Custom installation directory:** ```bash -sudo flmadm install --prefix /opt/flame +sudo flmadm install --all --prefix /opt/flame ``` **Install and start services:** ```bash -sudo flmadm install --enable +sudo flmadm install --all --enable ``` **User-local installation (no systemd):** @@ -76,6 +76,9 @@ sudo flmadm install --verbose **Install specific profiles:** ```bash +# Install all components explicitly (same as default) +sudo flmadm install --all + # Control plane only (session manager, flmctl, flmadm) sudo flmadm install --control-plane @@ -96,6 +99,9 @@ flmadm install --client --prefix ~/flame # ERROR: Cannot use --enable with client-only # flmadm install --client --enable # This will fail with an error + +# ERROR: Cannot use --all with specific profiles +# sudo flmadm install --all --control-plane # This will fail with an error ``` ### Uninstall Flame @@ -129,6 +135,7 @@ sudo flmadm uninstall --backup-dir /backups/flame-backup-2026-01-28 - `--src-dir `: Source code directory for building Flame (default: clone from GitHub) - `--prefix `: Target installation directory (default: `/usr/local/flame`) +- `--all`: Explicitly install all components (control plane + worker + client) - `--control-plane`: Install control plane components only (flame-session-manager, flmctl, flmadm) - `--worker`: Install worker components only (flame-executor-manager, flmping-service, flmexec-service, flamepy) - `--client`: Install client components only (flmping, flmexec, flamepy) @@ -139,7 +146,10 @@ sudo flmadm uninstall --backup-dir /backups/flame-backup-2026-01-28 - `--force`: Force overwrite existing components without prompting - `--verbose`: Show detailed build output (useful for debugging build issues) -**Note:** If no profile flags (`--control-plane`, `--worker`, `--client`) are specified, all components will be installed by default. +**Note:** +- You **must** specify at least one profile flag (`--all`, `--control-plane`, `--worker`, or `--client`) +- The `--all` flag cannot be combined with `--control-plane`, `--worker`, or `--client` +- Profile flags can be combined (e.g., `--control-plane --worker` for a combined node) ## Installation Profiles diff --git a/flmadm/src/main.rs b/flmadm/src/main.rs index d12211b8..27d14c3d 100644 --- a/flmadm/src/main.rs +++ b/flmadm/src/main.rs @@ -40,6 +40,10 @@ enum Commands { #[arg(long)] client: bool, + /// Install all components (control plane + worker + client) + #[arg(long)] + all: bool, + /// Skip systemd service generation #[arg(long)] no_systemd: bool, @@ -113,6 +117,7 @@ fn main() { control_plane, worker, client, + all, no_systemd, enable, skip_build, @@ -120,9 +125,38 @@ fn main() { force, verbose, } => { + // Validate profile flags + if all && (control_plane || worker || client) { + eprintln!( + "Error: --all cannot be used with --control-plane, --worker, or --client" + ); + std::process::exit(types::exit_codes::INSTALL_FAILURE); + } + + // Require explicit profile selection + if !all && !control_plane && !worker && !client { + eprintln!("Error: You must specify which components to install:"); + eprintln!( + " --all Install all components (control plane + worker + client)" + ); + eprintln!(" --control-plane Install control plane components only"); + eprintln!(" --worker Install worker components only"); + eprintln!(" --client Install client components only"); + eprintln!("\nYou can also combine profiles, for example:"); + eprintln!(" --control-plane --worker Install control plane and worker"); + std::process::exit(types::exit_codes::INSTALL_FAILURE); + } + // Determine which profiles to install - let profiles = if control_plane || worker || client { - // If any profile flag is specified, only install those profiles + let profiles = if all { + // Explicit --all flag: install all profiles + vec![ + types::InstallProfile::ControlPlane, + types::InstallProfile::Worker, + types::InstallProfile::Client, + ] + } else { + // Specific profile flags specified let mut profiles = Vec::new(); if control_plane { profiles.push(types::InstallProfile::ControlPlane); @@ -134,13 +168,6 @@ fn main() { profiles.push(types::InstallProfile::Client); } profiles - } else { - // If no profile flags are specified, install all profiles (default behavior) - vec![ - types::InstallProfile::ControlPlane, - types::InstallProfile::Worker, - types::InstallProfile::Client, - ] }; let config = types::InstallConfig { diff --git a/flmadm/src/managers/installation.rs b/flmadm/src/managers/installation.rs index 6780f2ce..9482f8c1 100644 --- a/flmadm/src/managers/installation.rs +++ b/flmadm/src/managers/installation.rs @@ -18,7 +18,7 @@ impl InstallationManager { for (name, path) in [ ("bin", &paths.bin), - ("sdk/python", &paths.sdk_python), + // Note: sdk/python is created by install_python_sdk() to allow existence check ("work", &paths.work), ("work/sessions", &paths.work.join("sessions")), ("work/executors", &paths.work.join("executors")),