kratart/
launch.rs

1use std::collections::HashMap;
2use std::fs;
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use advmac::MacAddr6;
7use anyhow::{anyhow, Result};
8use tokio::sync::Semaphore;
9use uuid::Uuid;
10
11use krata::launchcfg::{
12    LaunchInfo, LaunchNetwork, LaunchNetworkIpv4, LaunchNetworkIpv6, LaunchNetworkResolver,
13    LaunchPackedFormat, LaunchRoot,
14};
15use krataoci::packer::OciPackedImage;
16pub use xenclient::{
17    pci::PciBdf, DomainPciDevice as PciDevice, DomainPciRdmReservePolicy as PciRdmReservePolicy,
18};
19use xenclient::{DomainChannel, DomainConfig, DomainDisk, DomainNetworkInterface};
20use xenplatform::domain::BaseDomainConfig;
21
22use crate::cfgblk::ConfigBlock;
23use crate::RuntimeContext;
24
25use super::{ZoneInfo, ZoneState};
26
27pub struct ZoneLaunchRequest {
28    pub format: LaunchPackedFormat,
29    pub kernel: Vec<u8>,
30    pub initrd: Vec<u8>,
31    pub uuid: Option<Uuid>,
32    pub name: Option<String>,
33    pub target_cpus: u32,
34    pub max_cpus: u32,
35    pub target_memory: u64,
36    pub max_memory: u64,
37    pub env: HashMap<String, String>,
38    pub run: Option<Vec<String>>,
39    pub pcis: Vec<PciDevice>,
40    pub kernel_verbose: bool,
41    pub kernel_cmdline_append: String,
42    pub image: OciPackedImage,
43    pub addons_image: Option<PathBuf>,
44    pub network: ZoneLaunchNetwork,
45}
46
47pub struct ZoneLaunchNetwork {
48    pub ipv4: String,
49    pub ipv4_prefix: u8,
50    pub ipv6: String,
51    pub ipv6_prefix: u8,
52    pub gateway_ipv4: String,
53    pub gateway_ipv6: String,
54    pub zone_mac: MacAddr6,
55    pub nameservers: Vec<String>,
56}
57
58pub struct ZoneLauncher {
59    pub launch_semaphore: Arc<Semaphore>,
60}
61
62impl ZoneLauncher {
63    pub fn new(launch_semaphore: Arc<Semaphore>) -> Result<Self> {
64        Ok(Self { launch_semaphore })
65    }
66
67    pub async fn launch(
68        &mut self,
69        context: &RuntimeContext,
70        request: ZoneLaunchRequest,
71    ) -> Result<ZoneInfo> {
72        let uuid = request.uuid.unwrap_or_else(Uuid::new_v4);
73        let xen_name = format!("krata-{uuid}");
74        let _launch_permit = self.launch_semaphore.acquire().await?;
75        let launch_config = LaunchInfo {
76            root: LaunchRoot {
77                format: request.format.clone(),
78            },
79            hostname: Some(
80                request
81                    .name
82                    .as_ref()
83                    .map(|x| x.to_string())
84                    .unwrap_or_else(|| format!("krata-{}", uuid)),
85            ),
86            network: Some(LaunchNetwork {
87                link: "eth0".to_string(),
88                ipv4: LaunchNetworkIpv4 {
89                    address: format!("{}/{}", request.network.ipv4, request.network.ipv4_prefix),
90                    gateway: request.network.gateway_ipv4,
91                },
92                ipv6: LaunchNetworkIpv6 {
93                    address: format!("{}/{}", request.network.ipv6, request.network.ipv6_prefix),
94                    gateway: request.network.gateway_ipv6.to_string(),
95                },
96                resolver: LaunchNetworkResolver {
97                    nameservers: request.network.nameservers,
98                },
99            }),
100            env: request.env,
101            run: request.run,
102        };
103
104        let cfgblk = ConfigBlock::new(&uuid, request.image.clone())?;
105        let cfgblk_file = cfgblk.file.clone();
106        let cfgblk_dir = cfgblk.dir.clone();
107        tokio::task::spawn_blocking(move || cfgblk.build(&launch_config)).await??;
108
109        let image_squashfs_path = request
110            .image
111            .path
112            .to_str()
113            .ok_or_else(|| anyhow!("failed to convert image path to string"))?;
114
115        let cfgblk_dir_path = cfgblk_dir
116            .to_str()
117            .ok_or_else(|| anyhow!("failed to convert cfgblk directory path to string"))?;
118        let cfgblk_squashfs_path = cfgblk_file
119            .to_str()
120            .ok_or_else(|| anyhow!("failed to convert cfgblk squashfs path to string"))?;
121        let addons_squashfs_path = request
122            .addons_image
123            .map(|x| x.to_str().map(|x| x.to_string()))
124            .map(|x| {
125                Some(x.ok_or_else(|| anyhow!("failed to convert addons squashfs path to string")))
126            })
127            .unwrap_or(None);
128
129        let addons_squashfs_path = if let Some(path) = addons_squashfs_path {
130            Some(path?)
131        } else {
132            None
133        };
134
135        let image_squashfs_loop = context.autoloop.loopify(image_squashfs_path)?;
136        let cfgblk_squashfs_loop = context.autoloop.loopify(cfgblk_squashfs_path)?;
137        let addons_squashfs_loop = if let Some(ref addons_squashfs_path) = addons_squashfs_path {
138            Some(context.autoloop.loopify(addons_squashfs_path)?)
139        } else {
140            None
141        };
142        let mut cmdline_options = ["console=hvc0"].to_vec();
143        if !request.kernel_verbose {
144            cmdline_options.push("quiet");
145        }
146
147        if !request.kernel_cmdline_append.is_empty() {
148            cmdline_options.push(&request.kernel_cmdline_append);
149        }
150
151        let cmdline = cmdline_options.join(" ");
152
153        let zone_mac_string = request.network.zone_mac.to_string().replace('-', ":");
154
155        let mut disks = vec![
156            DomainDisk {
157                vdev: "xvda".to_string(),
158                block: image_squashfs_loop.clone(),
159                writable: false,
160            },
161            DomainDisk {
162                vdev: "xvdb".to_string(),
163                block: cfgblk_squashfs_loop.clone(),
164                writable: false,
165            },
166        ];
167
168        if let Some(ref addons) = addons_squashfs_loop {
169            disks.push(DomainDisk {
170                vdev: "xvdc".to_string(),
171                block: addons.clone(),
172                writable: false,
173            });
174        }
175
176        let mut loops = vec![
177            format!("{}:{}:none", image_squashfs_loop.path, image_squashfs_path),
178            format!(
179                "{}:{}:{}",
180                cfgblk_squashfs_loop.path, cfgblk_squashfs_path, cfgblk_dir_path
181            ),
182        ];
183
184        if let Some(ref addons) = addons_squashfs_loop {
185            loops.push(format!(
186                "{}:{}:none",
187                addons.path,
188                addons_squashfs_path
189                    .clone()
190                    .ok_or_else(|| anyhow!("addons squashfs path missing"))?
191            ));
192        }
193
194        let mut extra_keys = vec![
195            ("krata/uuid".to_string(), uuid.to_string()),
196            ("krata/loops".to_string(), loops.join(",")),
197        ];
198
199        if let Some(name) = request.name.as_ref() {
200            extra_keys.push(("krata/name".to_string(), name.clone()));
201        }
202
203        let config = DomainConfig {
204            base: BaseDomainConfig {
205                max_vcpus: request.max_cpus,
206                target_vcpus: request.target_cpus,
207                max_mem_mb: request.max_memory,
208                target_mem_mb: request.target_memory,
209                kernel: request.kernel,
210                initrd: request.initrd,
211                cmdline,
212                uuid,
213                owner_domid: 0,
214                enable_iommu: !request.pcis.is_empty(),
215            },
216            backend_domid: 0,
217            name: xen_name,
218            swap_console_backend: Some("krata-console".to_string()),
219            disks,
220            channels: vec![DomainChannel {
221                typ: "krata-channel".to_string(),
222                initialized: false,
223            }],
224            vifs: vec![DomainNetworkInterface {
225                mac: zone_mac_string.clone(),
226                mtu: 1500,
227                bridge: None,
228                script: None,
229            }],
230            pcis: request.pcis.clone(),
231            filesystems: vec![],
232            extra_keys,
233            extra_rw_paths: vec!["krata/zone".to_string()],
234        };
235        match context.xen.create(&config).await {
236            Ok(created) => Ok(ZoneInfo {
237                name: request.name.as_ref().map(|x| x.to_string()),
238                uuid,
239                domid: created.domid,
240                image: request.image.digest,
241                loops: vec![],
242                state: ZoneState { exit_code: None },
243            }),
244            Err(error) => {
245                let _ = context.autoloop.unloop(&image_squashfs_loop.path).await;
246                let _ = context.autoloop.unloop(&cfgblk_squashfs_loop.path).await;
247                let _ = fs::remove_dir(&cfgblk_dir);
248                Err(error.into())
249            }
250        }
251    }
252}