@@ -310,7 +310,7 @@ class AwsConfig:
310310 boot_image_tar : str = "" # explicit path override
311311
312312 # Data disk
313- data_size : int = 20 # GiB for new empty data volume
313+ data_size : int = 20 # GiB for the volume created from the labeled template
314314 data_snapshot : str = "" # optional existing snapshot for data disk
315315
316316 # Shared-disk device names (dstack host-shared + data)
@@ -324,7 +324,7 @@ class AwsConfig:
324324 security_group_ids : List [str ] = field (default_factory = list )
325325 iam_instance_profile : str = "" # name or ARN
326326
327- # S3 used when importing AMI / shared snapshot from local disk. raw
327+ # S3 used when importing AMI / shared / labeled-data snapshots from raw disks
328328 s3_bucket : str = ""
329329 s3_prefix : str = "dstack-import"
330330 import_role_name : str = "" # optional vmimport role override
@@ -384,6 +384,7 @@ class DeploymentState:
384384 instance_id : str = ""
385385 ami_id : str = ""
386386 shared_snapshot : str = ""
387+ data_snapshot : str = "" # Data template snapshot created by dstack-cloud
387388 external_ip : str = ""
388389 internal_ip : str = ""
389390 status : str = "" # RUNNING, STOPPED, TERMINATED, etc.
@@ -731,25 +732,7 @@ class CloudDeploymentManager:
731732
732733 # Create a minimal raw disk image with GPT partition table
733734 with tempfile .TemporaryDirectory () as tmpdir :
734- raw_file = os .path .join (tmpdir , "disk.raw" )
735-
736- # Create a 10MB sparse file (enough for GPT)
737- disk_size_bytes = 10 * 1024 * 1024
738- with open (raw_file , 'wb' ) as f :
739- f .truncate (disk_size_bytes )
740-
741- # Create GPT partition table with dstack-data partition using sgdisk
742- # -o: clear and create new GPT
743- # -n 1:0:0: create partition 1, start at first available, end at last available
744- # -c 1:dstack-data: set partition 1 name (PARTLABEL) to dstack-data
745- result = subprocess .run (
746- ["sgdisk" , "-o" , "-n" , "1:0:0" , "-c" , "1:dstack-data" , raw_file ],
747- capture_output = True , text = True
748- )
749- if result .returncode != 0 :
750- raise RuntimeError (f"Failed to create GPT partition table: { result .stderr } " )
751-
752- logger .debug ("Created GPT partition table with dstack-data label" )
735+ self ._build_data_raw_disk (Path (tmpdir ))
753736
754737 # Compress to tar.gz for upload
755738 tar_file = os .path .join (tmpdir , "disk.tar.gz" )
@@ -1697,6 +1680,29 @@ class CloudDeploymentManager:
16971680 logger .warning (".user-config not found, skipping" )
16981681 return raw_file
16991682
1683+ def _build_data_raw_disk (self , work_path : Optional [Path ] = None ) -> Path :
1684+ """Build a minimal GPT disk with a PARTLABEL=dstack-data partition."""
1685+ import shutil
1686+
1687+ if not shutil .which ("sgdisk" ):
1688+ raise FileNotFoundError (
1689+ "'sgdisk' command not found. Install the gdisk package."
1690+ )
1691+ if work_path is None :
1692+ work_path = Path (tempfile .mkdtemp (prefix = "dstack-data-" ))
1693+ work_path .mkdir (parents = True , exist_ok = True )
1694+ raw_file = work_path / "disk.raw"
1695+ subprocess .run (["truncate" , "-s" , "10M" , str (raw_file )], check = True )
1696+ subprocess .run (
1697+ [
1698+ "sgdisk" , "-o" , "-n" , "1:0:0" , "-c" , "1:dstack-data" ,
1699+ str (raw_file ),
1700+ ],
1701+ check = True ,
1702+ capture_output = True ,
1703+ )
1704+ return raw_file
1705+
17001706 def _aws_import_snapshot_from_raw (
17011707 self ,
17021708 config : AwsConfig ,
@@ -1931,14 +1937,31 @@ class CloudDeploymentManager:
19311937 pass
19321938
19331939 # Block device mappings: shared + data
1940+ managed_data_snapshot = ""
1941+ if config .data_snapshot :
1942+ data_snapshot = config .data_snapshot
1943+ else :
1944+ logger .info ("building labeled data disk template..." )
1945+ data_raw = self ._build_data_raw_disk ()
1946+ try :
1947+ data_token = f"data-{ config .instance_name } -{ int (time .time ())} " [:64 ]
1948+ data_snapshot = self ._aws_import_snapshot_from_raw (
1949+ config , data_raw , f"dstack-data-{ config .instance_name } " , data_token
1950+ )
1951+ managed_data_snapshot = data_snapshot
1952+ finally :
1953+ try :
1954+ import shutil
1955+ shutil .rmtree (data_raw .parent , ignore_errors = True )
1956+ except Exception :
1957+ pass
1958+
19341959 data_ebs : Dict [str , Any ] = {
19351960 "DeleteOnTermination" : True ,
19361961 "VolumeType" : config .volume_type ,
1962+ "SnapshotId" : data_snapshot ,
1963+ "VolumeSize" : int (config .data_size ),
19371964 }
1938- if config .data_snapshot :
1939- data_ebs ["SnapshotId" ] = config .data_snapshot
1940- else :
1941- data_ebs ["VolumeSize" ] = int (config .data_size )
19421965
19431966 mappings = [
19441967 {
@@ -2019,6 +2042,7 @@ class CloudDeploymentManager:
20192042 instance_id = instance_id ,
20202043 ami_id = ami_id ,
20212044 shared_snapshot = shared_snapshot ,
2045+ data_snapshot = managed_data_snapshot ,
20222046 external_ip = external_ip ,
20232047 internal_ip = internal_ip ,
20242048 status = inst .get ("State" , {}).get ("Name" , "running" ).upper (),
@@ -2032,6 +2056,8 @@ class CloudDeploymentManager:
20322056 logger .info (f"Instance ID: { instance_id } " )
20332057 logger .info (f"AMI: { ami_id } " )
20342058 logger .info (f"Shared snapshot: { shared_snapshot } " )
2059+ if managed_data_snapshot :
2060+ logger .info (f"Data template snapshot: { managed_data_snapshot } " )
20352061 logger .info (f"Public IP: { external_ip or '(none)' } " )
20362062 logger .info (f"Private IP: { internal_ip or '(none)' } " )
20372063 logger .info ("" )
@@ -2758,6 +2784,13 @@ class CloudDeploymentManager:
27582784 "--region" , state .region ,
27592785 "--snapshot-id" , state .shared_snapshot ,
27602786 ], capture = True , check = False )
2787+ if not keep_images and state .data_snapshot :
2788+ logger .info (f"Deleting data template snapshot { state .data_snapshot } ..." )
2789+ self ._run_aws ([
2790+ "ec2" , "delete-snapshot" ,
2791+ "--region" , state .region ,
2792+ "--snapshot-id" , state .data_snapshot ,
2793+ ], capture = True , check = False )
27612794 state_path = self .work_dir / STATE_FILE
27622795 if state_path .exists ():
27632796 state_path .unlink ()
0 commit comments