Skip to main content

incus_client/resources/
instances.rs

1//! Instance (container/VM) CRUD, lifecycle, and snapshots.
2//!
3//! Exec, console attach, and file push/pull are deliberately **not**
4//! implemented here: each uses `POST .../exec`-style operations whose
5//! `metadata` carries secrets for separate control/stdin/stdout WebSocket
6//! connections - a materially different protocol from the generic
7//! operations/events model the rest of this crate is built on. That's
8//! follow-up work for whenever a real consumer needs it, not a gap in this
9//! epic.
10
11use serde::{Deserialize, Serialize};
12
13use crate::error::{Error, Result};
14use crate::operations::{operation_from_envelope, Operation};
15use crate::transport::{
16    resource_error_or, sync_metadata, Client, Method, RecursionQuery, WithEtag,
17};
18
19/// A container or virtual machine. `config`/`devices` stay untyped
20/// (`serde_json::Value`) - Incus's instance config schema is large and
21/// mostly free-form key-value pairs, so fully typing it is out of scope for
22/// this crate.
23#[derive(Debug, Clone, Deserialize)]
24pub struct Instance {
25    pub name: String,
26    pub status: String,
27    pub status_code: u16,
28    #[serde(rename = "type")]
29    pub instance_type: String,
30    pub architecture: String,
31    pub created_at: String,
32    pub last_used_at: String,
33    pub location: String,
34    pub project: String,
35    #[serde(default)]
36    pub config: serde_json::Value,
37    #[serde(default)]
38    pub devices: serde_json::Value,
39    #[serde(default)]
40    pub profiles: Vec<String>,
41}
42
43/// Parameters for [`Client::create_instance`]. `source` is the raw Incus
44/// source-object JSON (e.g. `{"type": "image", "fingerprint": "..."}`) -
45/// kept untyped since Incus supports several distinct source shapes
46/// (image, copy, migration, none) that aren't worth fully typing for v1.
47#[derive(Debug, Clone, Serialize)]
48pub struct CreateInstanceParams {
49    pub name: String,
50    #[serde(rename = "type")]
51    pub instance_type: String,
52    pub source: serde_json::Value,
53}
54
55impl Client {
56    /// Lists instances. `recursion = true` fetches every instance's full
57    /// object (config/devices/state) in one call and can be expensive on
58    /// hosts with many instances; `recursion = false` returns lightweight
59    /// name/URL references only.
60    pub async fn list_instances(&self, recursion: bool) -> Result<Vec<serde_json::Value>> {
61        let recursion_query = RecursionQuery::new(recursion);
62        let envelope = self
63            .request(
64                Method::Get,
65                "/1.0/instances",
66                &recursion_query.as_query(),
67                None,
68                None,
69            )
70            .await?;
71        Ok(serde_json::from_value(sync_metadata(envelope, "list")?)?)
72    }
73
74    /// Fetches one instance by name, along with its ETag for use as a later
75    /// `If-Match` precondition.
76    pub async fn get_instance(&self, name: &str) -> Result<WithEtag<Instance>> {
77        let path = format!("/1.0/instances/{name}");
78        let envelope = self
79            .request(Method::Get, &path, &[], None, None)
80            .await
81            .map_err(|err| resource_error_or(err, name))?;
82        match envelope {
83            crate::transport::IncusEnvelope::Sync { metadata, etag } => Ok(WithEtag {
84                value: serde_json::from_value(metadata)?,
85                etag,
86            }),
87            other => Err(Error::InvalidResponse(format!(
88                "expected a sync instance response, got {other:?}"
89            ))),
90        }
91    }
92
93    /// Creates an instance. Always async, per Incus's documented behavior
94    /// for instance creation.
95    pub async fn create_instance(&self, params: &CreateInstanceParams) -> Result<Operation> {
96        let body = serde_json::to_value(params)?;
97        let envelope = self
98            .request(Method::Post, "/1.0/instances", &[], Some(&body), None)
99            .await?;
100        operation_from_envelope(envelope)
101    }
102
103    /// Full replacement update (PUT). `etag`, if provided, is sent as
104    /// `If-Match` for optimistic concurrency; a stale ETag surfaces as
105    /// `Error::PreconditionFailed`, not the generic `Error::Api`.
106    pub async fn update_instance(
107        &self,
108        name: &str,
109        new_definition: &serde_json::Value,
110        etag: Option<&str>,
111    ) -> Result<Operation> {
112        let path = format!("/1.0/instances/{name}");
113        let envelope = self
114            .request(Method::Put, &path, &[], Some(new_definition), etag)
115            .await
116            .map_err(|err| resource_error_or(err, name))?;
117        operation_from_envelope(envelope)
118    }
119
120    /// Same as [`Client::update_instance`], but takes the `WithEtag` from a
121    /// prior [`Client::get_instance`] call directly instead of a bare
122    /// `etag: Option<&str>` - the "pit of success" version of the
123    /// fetch-then-guarded-update workflow, since it makes threading a
124    /// genuinely-fetched ETag the natural path rather than something a
125    /// caller has to remember to do by hand. `update_instance` remains
126    /// available directly for callers with a legitimate reason to supply
127    /// their own ETag (e.g. one persisted from a previous process).
128    pub async fn update_instance_guarded(
129        &self,
130        fetched: &WithEtag<Instance>,
131        new_definition: &serde_json::Value,
132    ) -> Result<Operation> {
133        self.update_instance(&fetched.value().name, new_definition, fetched.etag())
134            .await
135    }
136
137    /// Partial update (PATCH) - use this instead of `update_instance` for
138    /// small config changes, to avoid a GET-then-PUT round trip.
139    pub async fn patch_instance(
140        &self,
141        name: &str,
142        patch: &serde_json::Value,
143        etag: Option<&str>,
144    ) -> Result<Operation> {
145        let path = format!("/1.0/instances/{name}");
146        let envelope = self
147            .request(Method::Patch, &path, &[], Some(patch), etag)
148            .await
149            .map_err(|err| resource_error_or(err, name))?;
150        operation_from_envelope(envelope)
151    }
152
153    /// Same as [`Client::patch_instance`], but takes the `WithEtag` from a
154    /// prior [`Client::get_instance`] call directly - see
155    /// [`Client::update_instance_guarded`]'s doc comment for why this
156    /// exists alongside the raw-`etag` version.
157    pub async fn patch_instance_guarded(
158        &self,
159        fetched: &WithEtag<Instance>,
160        patch: &serde_json::Value,
161    ) -> Result<Operation> {
162        self.patch_instance(&fetched.value().name, patch, fetched.etag())
163            .await
164    }
165
166    pub async fn delete_instance(&self, name: &str) -> Result<Operation> {
167        let path = format!("/1.0/instances/{name}");
168        let envelope = self
169            .request(Method::Delete, &path, &[], None, None)
170            .await
171            .map_err(|err| resource_error_or(err, name))?;
172        operation_from_envelope(envelope)
173    }
174
175    async fn set_state(&self, name: &str, action: &str) -> Result<Operation> {
176        let path = format!("/1.0/instances/{name}/state");
177        let body = serde_json::json!({ "action": action });
178        let envelope = self
179            .request(Method::Put, &path, &[], Some(&body), None)
180            .await?;
181        operation_from_envelope(envelope)
182    }
183
184    pub async fn start_instance(&self, name: &str) -> Result<Operation> {
185        self.set_state(name, "start").await
186    }
187
188    pub async fn stop_instance(&self, name: &str) -> Result<Operation> {
189        self.set_state(name, "stop").await
190    }
191
192    pub async fn restart_instance(&self, name: &str) -> Result<Operation> {
193        self.set_state(name, "restart").await
194    }
195
196    pub async fn pause_instance(&self, name: &str) -> Result<Operation> {
197        self.set_state(name, "freeze").await
198    }
199
200    pub async fn list_snapshots(
201        &self,
202        instance_name: &str,
203        recursion: bool,
204    ) -> Result<Vec<serde_json::Value>> {
205        let recursion_query = RecursionQuery::new(recursion);
206        let path = format!("/1.0/instances/{instance_name}/snapshots");
207        let envelope = self
208            .request(Method::Get, &path, &recursion_query.as_query(), None, None)
209            .await?;
210        Ok(serde_json::from_value(sync_metadata(envelope, "list")?)?)
211    }
212
213    pub async fn create_snapshot(
214        &self,
215        instance_name: &str,
216        snapshot_name: &str,
217    ) -> Result<Operation> {
218        let path = format!("/1.0/instances/{instance_name}/snapshots");
219        let body = serde_json::json!({ "name": snapshot_name });
220        let envelope = self
221            .request(Method::Post, &path, &[], Some(&body), None)
222            .await?;
223        operation_from_envelope(envelope)
224    }
225
226    pub async fn delete_snapshot(
227        &self,
228        instance_name: &str,
229        snapshot_name: &str,
230    ) -> Result<Operation> {
231        let path = format!("/1.0/instances/{instance_name}/snapshots/{snapshot_name}");
232        let envelope = self
233            .request(Method::Delete, &path, &[], None, None)
234            .await
235            .map_err(|err| resource_error_or(err, snapshot_name))?;
236        operation_from_envelope(envelope)
237    }
238}
239
240#[cfg(test)]
241#[path = "instances_tests.rs"]
242mod tests;