oxide_api/
lib.rs

1//! A fully generated, opinionated API client library for Oxide.
2//!
3//! [![docs.rs](https://docs.rs/oxide-api/badge.svg)](https://docs.rs/oxide-api)
4//!
5//! ## API Details
6//!
7//! API for interacting with the Oxide control plane
8//!
9//!
10//!
11//! ### Contact
12//!
13//!
14//! | url | email |
15//! |----|----|
16//! | <https://oxide.computer> | api@oxide.computer |
17//!
18//!
19//!
20//! ## Client Details
21//!
22//! This client is generated from the [Oxide OpenAPI
23//! specs](https://github.com/oxidecomputer/omicron) based on API spec version `0.0.1`. This way it will remain
24//! up to date as features are added. The documentation for the crate is generated
25//! along with the code to make this library easy to use.
26//!
27//!
28//! To install the library, add the following to your `Cargo.toml` file.
29//!
30//! ```toml
31//! [dependencies]
32//! oxide-api = "0.1.0-rc.41"
33//! ```
34//!
35//! ## Basic example
36//!
37//! Typical use will require intializing a `Client`. This requires
38//! a user agent string and set of credentials.
39//!
40//! ```
41//! use oxide_api::Client;
42//!
43//! let oxide = Client::new(String::from("api-key"), String::from("host"));
44//! ```
45//!
46//! Alternatively, the library can search for most of the variables required for
47//! the client in the environment:
48//!
49//! - `OXIDE_TOKEN`
50//! - `OXIDE_HOST`
51//!
52//! And then you can create a client from the environment.
53//!
54//! ```
55//! use oxide_api::Client;
56//!
57//! let oxide = Client::new_from_env();
58//! ```
59#![feature(derive_default_enum)]
60#![allow(clippy::too_many_arguments)]
61#![allow(clippy::nonstandard_macro_braces)]
62#![allow(clippy::large_enum_variant)]
63#![allow(clippy::tabs_in_doc_comments)]
64#![allow(missing_docs)]
65#![cfg_attr(docsrs, feature(doc_cfg))]
66
67/// Virtual disks are used to store instance-local data which includes the operating system.
68///
69///FROM: http://oxide.computer/docs/#xxx
70pub mod disks;
71/// TODO operations that will not ship to customers.
72///
73///FROM: http://oxide.computer/docs/#xxx
74pub mod hidden;
75/// Images are read-only Virtual Disks that may be used to boot Virtual Machines.
76///
77///FROM: http://oxide.computer/docs/#xxx
78pub mod images;
79/// Images are read-only Virtual Disks that may be used to boot Virtual Machines. These images are scoped globally.
80///
81///FROM: http://oxide.computer/docs/#xxx
82pub mod images_global;
83/// Virtual machine instances are the basic unit of computation. These operations are used for provisioning, controlling, and destroying instances.
84///
85///FROM: http://oxide.computer/docs/#xxx
86pub mod instances;
87/// IP Pools contain external IP addresses that can be assigned to virtual machine Instances.
88///
89///FROM: http://oxide.computer/docs/#xxx
90pub mod ip_pools;
91/// Authentication endpoints.
92///
93///FROM: http://oxide.computer/docs/#xxx
94pub mod login;
95/// Metrics provide insight into the operation of the Oxide deployment. These include telemetry on hardware and software components that can be used to understand the current state as well as to diagnose issues.
96///
97///FROM: http://oxide.computer/docs/#xxx
98pub mod metrics;
99/// Organizations represent a subset of users and projects in an Oxide deployment.
100///
101///FROM: http://oxide.computer/docs/#xxx
102pub mod organizations;
103/// System-wide IAM policy.
104///
105///FROM: http://oxide.computer/docs/#xxx
106pub mod policy;
107/// Projects are a grouping of associated resources such as instances and disks within an organization for purposes of billing and access control.
108///
109///FROM: http://oxide.computer/docs/#xxx
110pub mod projects;
111/// These operations pertain to hardware inventory and management. Racks are the unit of expansion of an Oxide deployment. Racks are in turn composed of sleds, switches, power supplies, and a cabled backplane.
112///
113///FROM: http://oxide.computer/docs/#xxx
114pub mod racks;
115/// Roles are a component of Identity and Access Management (IAM) that allow a user or agent account access to additional permissions.
116///
117///FROM: http://oxide.computer/docs/#xxx
118pub mod roles;
119/// Routers direct the flow of network traffic into, out of, and within a VPC via routes.
120///
121///FROM: http://oxide.computer/docs/#xxx
122pub mod routers;
123/// Routes define router policy.
124///
125///FROM: http://oxide.computer/docs/#xxx
126pub mod routes;
127/// Sagas are the abstraction used to represent multi-step operations within the Oxide deployment. These operations can be used to query saga status and report errors.
128///
129///FROM: http://oxide.computer/docs/#xxx
130pub mod sagas;
131/// Silos represent a logical partition of users and resources.
132///
133///FROM: http://oxide.computer/docs/#xxx
134pub mod silos;
135/// This tag should be moved into hardware.
136///
137///FROM: http://oxide.computer/docs/#xxx
138pub mod sleds;
139/// Snapshots of Virtual Disks at a particular point in time.
140///
141///FROM: http://oxide.computer/docs/#xxx
142pub mod snapshots;
143/// Public SSH keys for an individual user.
144///
145///FROM: http://oxide.computer/docs/#xxx
146pub mod sshkeys;
147/// This tag should be moved into a generic network tag.
148///
149///FROM: http://oxide.computer/docs/#xxx
150pub mod subnets;
151/// Internal system information.
152///
153///FROM: http://oxide.computer/docs/#xxx
154pub mod system;
155#[cfg(test)]
156mod tests;
157pub mod types;
158/// This tag should be moved into a operations tag.
159///
160///FROM: http://oxide.computer/docs/#xxx
161pub mod updates;
162#[doc(hidden)]
163pub mod utils;
164/// A Virtual Private Cloud (VPC) is an isolated network environment that should probaby be moved into a more generic networking tag.
165///
166///FROM: http://oxide.computer/docs/#xxx
167pub mod vpcs;
168
169use anyhow::{anyhow, Error, Result};
170
171mod progenitor_support {
172    use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
173
174    const PATH_SET: &AsciiSet = &CONTROLS
175        .add(b' ')
176        .add(b'"')
177        .add(b'#')
178        .add(b'<')
179        .add(b'>')
180        .add(b'?')
181        .add(b'`')
182        .add(b'{')
183        .add(b'}');
184
185    #[allow(dead_code)]
186    pub(crate) fn encode_path(pc: &str) -> String {
187        utf8_percent_encode(pc, PATH_SET).to_string()
188    }
189}
190
191use std::env;
192
193/// Entrypoint for interacting with the API client.
194#[derive(Clone)]
195pub struct Client {
196    host: String,
197    token: String,
198
199    client: reqwest::Client,
200}
201
202impl Client {
203    /// Create a new Client struct. It takes a type that can convert into
204    /// an &str (`String` or `Vec<u8>` for example). As long as the function is
205    /// given a valid API key your requests will work.
206    pub fn new<T, H>(token: T, host: H) -> Self
207    where
208        T: ToString,
209        H: ToString,
210    {
211        let client = reqwest::Client::builder().build();
212        match client {
213            Ok(c) => Client {
214                host: host.to_string(),
215                token: token.to_string(),
216
217                client: c,
218            },
219            Err(e) => panic!("creating reqwest client failed: {:?}", e),
220        }
221    }
222
223    /// Create a new Client struct from environment variables: OXIDE_TOKEN and OXIDE_HOST.
224    pub fn new_from_env() -> Self {
225        let token = env::var("OXIDE_TOKEN").expect("must set OXIDE_TOKEN");
226        let host = env::var("OXIDE_HOST").expect("must set OXIDE_HOST");
227
228        Client::new(token, host)
229    }
230
231    async fn url_and_auth(&self, uri: &str) -> Result<(reqwest::Url, Option<String>)> {
232        let parsed_url = uri.parse::<reqwest::Url>();
233
234        let auth = format!("Bearer {}", self.token);
235        parsed_url.map(|u| (u, Some(auth))).map_err(Error::from)
236    }
237
238    pub async fn request_raw(
239        &self,
240        method: reqwest::Method,
241        uri: &str,
242        body: Option<reqwest::Body>,
243    ) -> Result<reqwest::RequestBuilder> {
244        let u = if uri.starts_with("https://") || uri.starts_with("http://") {
245            uri.to_string()
246        } else {
247            (self.host.clone() + uri).to_string()
248        };
249        let (url, auth) = self.url_and_auth(&u).await?;
250
251        let instance = <&Client>::clone(&self);
252
253        let mut req = instance.client.request(method.clone(), url);
254
255        // Set the default headers.
256        req = req.header(
257            reqwest::header::ACCEPT,
258            reqwest::header::HeaderValue::from_static("application/json"),
259        );
260        req = req.header(
261            reqwest::header::CONTENT_TYPE,
262            reqwest::header::HeaderValue::from_static("application/json"),
263        );
264
265        if let Some(auth_str) = auth {
266            req = req.header(http::header::AUTHORIZATION, &*auth_str);
267        }
268
269        if let Some(body) = body {
270            log::debug!(
271                "body: {:?}",
272                String::from_utf8(body.as_bytes().unwrap().to_vec()).unwrap()
273            );
274            req = req.body(body);
275        }
276        log::debug!("request: {:?}", &req);
277        Ok(req)
278    }
279
280    pub async fn response_raw(
281        &self,
282        method: reqwest::Method,
283        uri: &str,
284        body: Option<reqwest::Body>,
285    ) -> Result<reqwest::Response> {
286        let req = self.request_raw(method, uri, body).await?;
287        Ok(req.send().await?)
288    }
289
290    async fn request<Out>(
291        &self,
292        method: reqwest::Method,
293        uri: &str,
294        body: Option<reqwest::Body>,
295    ) -> Result<Out>
296    where
297        Out: serde::de::DeserializeOwned + 'static + Send,
298    {
299        let response = self.response_raw(method, uri, body).await?;
300
301        let status = response.status();
302
303        let response_body = response.bytes().await?;
304
305        if status.is_success() {
306            log::debug!(
307                "response payload {}",
308                String::from_utf8_lossy(&response_body)
309            );
310            let parsed_response = if status == http::StatusCode::NO_CONTENT
311                || std::any::TypeId::of::<Out>() == std::any::TypeId::of::<()>()
312            {
313                serde_json::from_str("null")
314            } else {
315                serde_json::from_slice::<Out>(&response_body)
316            };
317            parsed_response.map_err(Error::from)
318        } else {
319            let error: anyhow::Error = if response_body.is_empty() {
320                anyhow!("code: {}, empty response", status)
321            } else {
322                // Parse the error as the error type.
323                match serde_json::from_slice::<crate::types::ErrorResponse>(&response_body) {
324                    Ok(resp) => {
325                        let e: crate::types::Error = resp.into();
326                        e.into()
327                    }
328                    Err(_) => {
329                        anyhow!(
330                            "code: {}, error: {:?}",
331                            status,
332                            String::from_utf8_lossy(&response_body),
333                        )
334                    }
335                }
336            };
337
338            Err(error)
339        }
340    }
341
342    async fn request_entity<D>(
343        &self,
344        method: http::Method,
345        uri: &str,
346        body: Option<reqwest::Body>,
347    ) -> Result<D>
348    where
349        D: serde::de::DeserializeOwned + 'static + Send,
350    {
351        let r = self.request(method, uri, body).await?;
352        Ok(r)
353    }
354
355    async fn get<D>(&self, uri: &str, message: Option<reqwest::Body>) -> Result<D>
356    where
357        D: serde::de::DeserializeOwned + 'static + Send,
358    {
359        self.request_entity(http::Method::GET, &(self.host.to_string() + uri), message)
360            .await
361    }
362
363    async fn post<D>(&self, uri: &str, message: Option<reqwest::Body>) -> Result<D>
364    where
365        D: serde::de::DeserializeOwned + 'static + Send,
366    {
367        self.request_entity(http::Method::POST, &(self.host.to_string() + uri), message)
368            .await
369    }
370
371    #[allow(dead_code)]
372    async fn patch<D>(&self, uri: &str, message: Option<reqwest::Body>) -> Result<D>
373    where
374        D: serde::de::DeserializeOwned + 'static + Send,
375    {
376        self.request_entity(http::Method::PATCH, &(self.host.to_string() + uri), message)
377            .await
378    }
379
380    async fn put<D>(&self, uri: &str, message: Option<reqwest::Body>) -> Result<D>
381    where
382        D: serde::de::DeserializeOwned + 'static + Send,
383    {
384        self.request_entity(http::Method::PUT, &(self.host.to_string() + uri), message)
385            .await
386    }
387
388    async fn delete<D>(&self, uri: &str, message: Option<reqwest::Body>) -> Result<D>
389    where
390        D: serde::de::DeserializeOwned + 'static + Send,
391    {
392        self.request_entity(
393            http::Method::DELETE,
394            &(self.host.to_string() + uri),
395            message,
396        )
397        .await
398    }
399
400    /// Virtual disks are used to store instance-local data which includes the operating system.
401    ///
402    ///FROM: http://oxide.computer/docs/#xxx
403    pub fn disks(&self) -> disks::Disks {
404        disks::Disks::new(self.clone())
405    }
406
407    /// TODO operations that will not ship to customers.
408    ///
409    ///FROM: http://oxide.computer/docs/#xxx
410    pub fn hidden(&self) -> hidden::Hidden {
411        hidden::Hidden::new(self.clone())
412    }
413
414    /// Images are read-only Virtual Disks that may be used to boot Virtual Machines.
415    ///
416    ///FROM: http://oxide.computer/docs/#xxx
417    pub fn images(&self) -> images::Images {
418        images::Images::new(self.clone())
419    }
420
421    /// Images are read-only Virtual Disks that may be used to boot Virtual Machines. These images are scoped globally.
422    ///
423    ///FROM: http://oxide.computer/docs/#xxx
424    pub fn images_global(&self) -> images_global::ImagesGlobal {
425        images_global::ImagesGlobal::new(self.clone())
426    }
427
428    /// Virtual machine instances are the basic unit of computation. These operations are used for provisioning, controlling, and destroying instances.
429    ///
430    ///FROM: http://oxide.computer/docs/#xxx
431    pub fn instances(&self) -> instances::Instances {
432        instances::Instances::new(self.clone())
433    }
434
435    /// IP Pools contain external IP addresses that can be assigned to virtual machine Instances.
436    ///
437    ///FROM: http://oxide.computer/docs/#xxx
438    pub fn ip_pools(&self) -> ip_pools::IpPools {
439        ip_pools::IpPools::new(self.clone())
440    }
441
442    /// Authentication endpoints.
443    ///
444    ///FROM: http://oxide.computer/docs/#xxx
445    pub fn login(&self) -> login::Login {
446        login::Login::new(self.clone())
447    }
448
449    /// Metrics provide insight into the operation of the Oxide deployment. These include telemetry on hardware and software components that can be used to understand the current state as well as to diagnose issues.
450    ///
451    ///FROM: http://oxide.computer/docs/#xxx
452    pub fn metrics(&self) -> metrics::Metrics {
453        metrics::Metrics::new(self.clone())
454    }
455
456    /// Organizations represent a subset of users and projects in an Oxide deployment.
457    ///
458    ///FROM: http://oxide.computer/docs/#xxx
459    pub fn organizations(&self) -> organizations::Organizations {
460        organizations::Organizations::new(self.clone())
461    }
462
463    /// System-wide IAM policy.
464    ///
465    ///FROM: http://oxide.computer/docs/#xxx
466    pub fn policy(&self) -> policy::Policy {
467        policy::Policy::new(self.clone())
468    }
469
470    /// Projects are a grouping of associated resources such as instances and disks within an organization for purposes of billing and access control.
471    ///
472    ///FROM: http://oxide.computer/docs/#xxx
473    pub fn projects(&self) -> projects::Projects {
474        projects::Projects::new(self.clone())
475    }
476
477    /// These operations pertain to hardware inventory and management. Racks are the unit of expansion of an Oxide deployment. Racks are in turn composed of sleds, switches, power supplies, and a cabled backplane.
478    ///
479    ///FROM: http://oxide.computer/docs/#xxx
480    pub fn racks(&self) -> racks::Racks {
481        racks::Racks::new(self.clone())
482    }
483
484    /// Roles are a component of Identity and Access Management (IAM) that allow a user or agent account access to additional permissions.
485    ///
486    ///FROM: http://oxide.computer/docs/#xxx
487    pub fn roles(&self) -> roles::Roles {
488        roles::Roles::new(self.clone())
489    }
490
491    /// Routers direct the flow of network traffic into, out of, and within a VPC via routes.
492    ///
493    ///FROM: http://oxide.computer/docs/#xxx
494    pub fn routers(&self) -> routers::Routers {
495        routers::Routers::new(self.clone())
496    }
497
498    /// Routes define router policy.
499    ///
500    ///FROM: http://oxide.computer/docs/#xxx
501    pub fn routes(&self) -> routes::Routes {
502        routes::Routes::new(self.clone())
503    }
504
505    /// Sagas are the abstraction used to represent multi-step operations within the Oxide deployment. These operations can be used to query saga status and report errors.
506    ///
507    ///FROM: http://oxide.computer/docs/#xxx
508    pub fn sagas(&self) -> sagas::Sagas {
509        sagas::Sagas::new(self.clone())
510    }
511
512    /// This tag should be moved into hardware.
513    ///
514    ///FROM: http://oxide.computer/docs/#xxx
515    pub fn sleds(&self) -> sleds::Sleds {
516        sleds::Sleds::new(self.clone())
517    }
518
519    /// Silos represent a logical partition of users and resources.
520    ///
521    ///FROM: http://oxide.computer/docs/#xxx
522    pub fn silos(&self) -> silos::Silos {
523        silos::Silos::new(self.clone())
524    }
525
526    /// Snapshots of Virtual Disks at a particular point in time.
527    ///
528    ///FROM: http://oxide.computer/docs/#xxx
529    pub fn snapshots(&self) -> snapshots::Snapshots {
530        snapshots::Snapshots::new(self.clone())
531    }
532
533    /// Public SSH keys for an individual user.
534    ///
535    ///FROM: http://oxide.computer/docs/#xxx
536    pub fn sshkeys(&self) -> sshkeys::Sshkeys {
537        sshkeys::Sshkeys::new(self.clone())
538    }
539
540    /// This tag should be moved into a generic network tag.
541    ///
542    ///FROM: http://oxide.computer/docs/#xxx
543    pub fn subnets(&self) -> subnets::Subnets {
544        subnets::Subnets::new(self.clone())
545    }
546
547    /// Internal system information.
548    ///
549    ///FROM: http://oxide.computer/docs/#xxx
550    pub fn system(&self) -> system::System {
551        system::System::new(self.clone())
552    }
553
554    /// This tag should be moved into a operations tag.
555    ///
556    ///FROM: http://oxide.computer/docs/#xxx
557    pub fn updates(&self) -> updates::Updates {
558        updates::Updates::new(self.clone())
559    }
560
561    /// A Virtual Private Cloud (VPC) is an isolated network environment that should probaby be moved into a more generic networking tag.
562    ///
563    ///FROM: http://oxide.computer/docs/#xxx
564    pub fn vpcs(&self) -> vpcs::Vpcs {
565        vpcs::Vpcs::new(self.clone())
566    }
567}