diff --git a/REUSE.toml b/REUSE.toml index bf435ec4..8f345c27 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -64,6 +64,7 @@ SPDX-License-Identifier = "Apache-2.0" [[annotations]] path = [ "docs/security/dstack-audit.pdf", + "dstack_Technical_Charter_Final_10-17-2025.pdf", "sdk/simulator/quote.hex", "ra-tls/assets/tdx_quote", "cc-eventlog/samples/ccel.bin", diff --git a/dstack-util/src/system_setup.rs b/dstack-util/src/system_setup.rs index 697f18b2..6e568702 100644 --- a/dstack-util/src/system_setup.rs +++ b/dstack-util/src/system_setup.rs @@ -1124,7 +1124,7 @@ impl Stage1<'_> { } async fn setup(&self) -> Result<()> { - let envs = self.unseal_env_vars()?; + let _envs = self.unseal_env_vars()?; self.link_files()?; self.setup_guest_agent_config()?; self.vmm diff --git a/dstack_Technical_Charter_Final_10-17-2025.pdf b/dstack_Technical_Charter_Final_10-17-2025.pdf new file mode 100644 index 00000000..e73d6e12 Binary files /dev/null and b/dstack_Technical_Charter_Final_10-17-2025.pdf differ diff --git a/gateway/dstack-app/deploy-to-vmm.sh b/gateway/dstack-app/deploy-to-vmm.sh index 1271281b..9262efff 100755 --- a/gateway/dstack-app/deploy-to-vmm.sh +++ b/gateway/dstack-app/deploy-to-vmm.sh @@ -75,7 +75,7 @@ SUBNET_INDEX=0 OS_IMAGE=dstack-0.5.5 # Set defaults for variables that might not be in .env -GATEWAY_IMAGE=dstacktee/gateway@sha256:a7b7e3144371b053ba21d6ac18141afd49e3cd767ca2715599aa0e2703b3a11a +GATEWAY_IMAGE=dstacktee/dstack-gateway@sha256:a7b7e3144371b053ba21d6ac18141afd49e3cd767ca2715599aa0e2703b3a11a # Port configurations GATEWAY_RPC_ADDR=0.0.0.0:9202 diff --git a/kms/README.md b/kms/README.md index 7fa27dd8..19130c92 100644 --- a/kms/README.md +++ b/kms/README.md @@ -150,7 +150,7 @@ The verification process follows these steps: ## The RPC Interface -The KMS RPC interface is defined in [kms.proto](rpc/proto/kms.proto). +The KMS RPC interface is defined in [kms_rpc.proto](rpc/proto/kms_rpc.proto). The core interface serving the dstack app are: - `GetAppKey`: Requests an app key using the app ID and TDX quote @@ -171,7 +171,7 @@ The `GetAppKey` RPC is used by the dstack app to request an app key. In this RPC Note: -There are multiple keys derived for different usage, see [kms.proto](rpc/proto/kms.proto) for more details. +There are multiple keys derived for different usage, see [kms_rpc.proto](rpc/proto/kms_rpc.proto) for more details. The root key is generated by a genesis KMS node in TEE and would be stored in the KMS node's encrypted local disk, replicated to other KMS nodes. The keys are derived with app id which guarantees apps can not get the keys from other apps. diff --git a/kms/auth-eth-bun/package.json b/kms/auth-eth-bun/package.json index febd26eb..3bef0eb8 100644 --- a/kms/auth-eth-bun/package.json +++ b/kms/auth-eth-bun/package.json @@ -15,7 +15,7 @@ "check": "bun run lint && bun run test:run" }, "dependencies": { - "hono": "4.9.7", + "hono": "4.10.3", "@hono/zod-validator": "0.2.2", "zod": "3.25.76", "viem": "2.31.7" diff --git a/kms/auth-mock/package.json b/kms/auth-mock/package.json index a2c38999..3c62e80e 100644 --- a/kms/auth-mock/package.json +++ b/kms/auth-mock/package.json @@ -15,7 +15,7 @@ "check": "bun run lint && bun run test:run" }, "dependencies": { - "hono": "4.9.6", + "hono": "4.10.3", "@hono/zod-validator": "0.2.2", "zod": "3.25.76" }, diff --git a/kms/dstack-app/deploy-to-vmm.sh b/kms/dstack-app/deploy-to-vmm.sh index 9f798cc6..b8f6aeee 100755 --- a/kms/dstack-app/deploy-to-vmm.sh +++ b/kms/dstack-app/deploy-to-vmm.sh @@ -53,7 +53,7 @@ GIT_REV=HEAD OS_IMAGE=dstack-0.5.5 # The dstack KMS image name to use for the KMS app -KMS_IMAGE=dstacktee/kms@sha256:11ac59f524a22462ccd2152219b0bec48a28ceb734e32500152d4abefab7a62a +KMS_IMAGE=dstacktee/dstack-kms@sha256:11ac59f524a22462ccd2152219b0bec48a28ceb734e32500152d4abefab7a62a # The admin token for the KMS app ADMIN_TOKEN=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) diff --git a/vmm/rpc/src/lib.rs b/vmm/rpc/src/lib.rs index 089da7c6..4780ac98 100644 --- a/vmm/rpc/src/lib.rs +++ b/vmm/rpc/src/lib.rs @@ -7,3 +7,12 @@ extern crate alloc; pub use generated::*; mod generated; + +impl GpuConfig { + pub fn is_empty(&self) -> bool { + if self.attach_mode == "all" { + return false; + } + self.gpus.is_empty() + } +} diff --git a/vmm/src/config.rs b/vmm/src/config.rs index 999b4a21..34365226 100644 --- a/vmm/src/config.rs +++ b/vmm/src/config.rs @@ -265,6 +265,10 @@ pub struct Config { /// The URL of the KMS server pub kms_url: String, + /// Node name (optional, used as prefix in UI title) + #[serde(default)] + pub node_name: String, + /// CVM configuration pub cvm: CvmConfig, /// Gateway configuration diff --git a/vmm/src/console.html b/vmm/src/console.html index c07a6f4e..f9d0bd78 100644 --- a/vmm/src/console.html +++ b/vmm/src/console.html @@ -10,7 +10,7 @@ - Codestin Search App + Codestin Search App (ContentType, String) { - (ContentType::HTML, file_or_include_str!("console.html")) +async fn index(app: &State) -> (ContentType, String) { + let html = file_or_include_str!("console.html"); + let title = if app.config.node_name.is_empty() { + "dstack VM Management Console".to_string() + } else { + format!("{} - dstack VM Management Console", app.config.node_name) + }; + let html = html.replace("{{TITLE}}", &title); + (ContentType::HTML, html) } #[get("/res/")] diff --git a/vmm/src/main_service.rs b/vmm/src/main_service.rs index d57fd0eb..a3aa6317 100644 --- a/vmm/src/main_service.rs +++ b/vmm/src/main_service.rs @@ -66,7 +66,7 @@ pub fn resolve_gpus_with_config( gpu_cfg: &rpc::GpuConfig, cvm_config: &crate::config::CvmConfig, ) -> Result { - if !cvm_config.gpu.enabled { + if !cvm_config.gpu.enabled && !gpu_cfg.is_empty() { bail!("GPU is not enabled"); } let gpus = resolve_gpus(gpu_cfg)?; diff --git a/vmm/src/vmm-cli.py b/vmm/src/vmm-cli.py index bbdac681..36e9288f 100755 --- a/vmm/src/vmm-cli.py +++ b/vmm/src/vmm-cli.py @@ -123,7 +123,6 @@ def read_utf8(filepath: str) -> str: with open(filepath, 'rb') as f: return f.read().decode('utf-8') - class UnixSocketHTTPConnection(http.client.HTTPConnection): """HTTPConnection that connects to a Unix domain socket.""" @@ -332,6 +331,33 @@ def remove_vm(self, vm_id: str) -> None: self.rpc_call('RemoveVm', {'id': vm_id}) print(f"Removed VM {vm_id}") + def resize_vm( + self, + vm_id: str, + vcpu: Optional[int] = None, + memory: Optional[int] = None, + disk_size: Optional[int] = None, + image: Optional[str] = None, + ) -> None: + """Resize a VM""" + params = {"id": vm_id} + if vcpu is not None: + params["vcpu"] = vcpu + if memory is not None: + params["memory"] = memory + if disk_size is not None: + params["disk_size"] = disk_size + if image is not None: + params["image"] = image + + if len(params) == 1: + raise Exception( + "at least one parameter must be specified for resize: --vcpu, --memory, --disk, or --image" + ) + + self.rpc_call("ResizeVm", params) + print(f"Resized VM {vm_id}") + def show_logs(self, vm_id: str, lines: int = 20, follow: bool = False) -> None: """Show VM logs""" path = f"/logs?id={vm_id}&follow={str(follow).lower()}&ansi=false&lines={lines}" @@ -609,6 +635,15 @@ def update_vm_app_compose(self, vm_id: str, app_compose: str) -> None: self.rpc_call('UpgradeApp', {'id': vm_id, 'compose_file': app_compose}) print(f"App compose updated for VM {vm_id}") + + def update_vm_ports(self, vm_id: str, ports: List[str]) -> None: + """Update port mapping for a VM""" + port_mappings = [parse_port_mapping(port) for port in ports] + self.rpc_call( + "UpgradeApp", {"id": vm_id, + "update_ports": True, "ports": port_mappings} + ) + print(f"Port mapping updated for VM {vm_id}") def list_gpus(self, json_output: bool = False) -> None: """List all available GPUs""" @@ -884,6 +919,18 @@ def main(): remove_parser = subparsers.add_parser('remove', help='Remove a VM') remove_parser.add_argument('vm_id', help='VM ID to remove') + # Resize command + resize_parser = subparsers.add_parser("resize", help="Resize a VM") + resize_parser.add_argument("vm_id", help="VM ID to resize") + resize_parser.add_argument("--vcpu", type=int, help="Number of vCPUs") + resize_parser.add_argument( + "--memory", type=parse_memory_size, help="Memory size (e.g. 1G, 100M)" + ) + resize_parser.add_argument( + "--disk", type=parse_disk_size, help="Disk size (e.g. 20G, 1T)" + ) + resize_parser.add_argument("--image", type=str, help="Image name") + # Logs command logs_parser = subparsers.add_parser('logs', help='Show VM logs') logs_parser.add_argument('vm_id', help='VM ID to show logs for') @@ -1016,6 +1063,19 @@ def main(): update_user_config_parser.add_argument( 'user_config', help='Path to user config file') + # Update port mapping + update_ports_parser = subparsers.add_parser( + "update-ports", help="Update port mapping for a VM" + ) + update_ports_parser.add_argument("vm_id", help="VM ID to update") + update_ports_parser.add_argument( + "--port", + action="append", + type=str, + required=True, + help="Port mapping in format: protocol[:address]:from:to (can be used multiple times)", + ) + args = parser.parse_args() cli = VmmCLI(args.url, args.auth_user, args.auth_password) @@ -1028,6 +1088,14 @@ def main(): cli.stop_vm(args.vm_id, args.force) elif args.command == 'remove': cli.remove_vm(args.vm_id) + elif args.command == 'resize': + cli.resize_vm( + args.vm_id, + vcpu=args.vcpu, + memory=args.memory, + disk_size=args.disk, + image=args.image, + ) elif args.command == 'logs': cli.show_logs(args.vm_id, args.lines, args.follow) elif args.command == 'compose': @@ -1046,6 +1114,8 @@ def main(): args.vm_id, open(args.user_config, 'r').read()) elif args.command == 'update-app-compose': cli.update_vm_app_compose(args.vm_id, open(args.compose, 'r').read()) + elif args.command == "update-ports": + cli.update_vm_ports(args.vm_id, args.port) elif args.command == 'kms': if not args.kms_action: kms_parser.print_help()