openstack_cli/api/
mod.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License at
4//
5//     http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS,
9// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10// See the License for the specific language governing permissions and
11// limitations under the License.
12//
13// SPDX-License-Identifier: Apache-2.0
14
15//! Direct API command implementation
16
17use clap::{Parser, ValueEnum};
18use http::Uri;
19use serde_json::Value;
20use std::io::{self, Write};
21use tracing::info;
22use url::Url;
23
24use openstack_sdk::{
25    AsyncOpenStack,
26    api::{AsyncClient, RestClient},
27    types::ServiceType,
28};
29
30use crate::Cli;
31use crate::OpenStackCliError;
32use crate::common::parse_key_val;
33use crate::output::OutputProcessor;
34
35fn url_to_http_uri(url: Url) -> Uri {
36    url.as_str()
37        .parse::<Uri>()
38        .expect("failed to parse a url::Url as an http::Uri")
39}
40
41/// Supported http methods
42#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, ValueEnum)]
43pub enum Method {
44    /// HEAD
45    Head,
46    /// GET
47    Get,
48    /// PATCH
49    Patch,
50    /// PUT
51    Put,
52    /// POST
53    Post,
54    /// DELETE
55    Delete,
56}
57
58impl From<Method> for http::Method {
59    fn from(item: Method) -> Self {
60        match item {
61            Method::Head => http::Method::HEAD,
62            Method::Get => http::Method::GET,
63            Method::Patch => http::Method::PATCH,
64            Method::Put => http::Method::PUT,
65            Method::Post => http::Method::POST,
66            Method::Delete => http::Method::DELETE,
67        }
68    }
69}
70
71/// Perform direct REST API requests with authorization
72///
73/// This command enables direct REST API call with the authorization and
74/// version discovery handled transparently. This may be used when required
75/// operation is not implemented by the `osc` or some of the parameters
76/// require special handling.
77///
78/// Example:
79///
80/// ```console
81/// osc --os-cloud devstack api compute flavors/detail | jq
82/// ```
83#[derive(Debug, Parser)]
84pub struct ApiCommand {
85    /// Service type as used in the service catalog
86    #[arg()]
87    service_type: String,
88
89    /// Rest URL (relative to the endpoint information
90    /// from the service catalog). Do not start URL with
91    /// the "/" to respect endpoint version information.
92    #[arg()]
93    url: String,
94
95    /// HTTP Method
96    #[arg(short, long, value_enum, default_value_t=Method::Get)]
97    method: Method,
98
99    /// Additional headers
100    #[arg(long, short='H', value_name="key=value", value_parser = parse_key_val::<String, String>)]
101    header: Vec<(String, String)>,
102
103    /// Request body to be used
104    #[arg(long)]
105    body: Option<String>,
106}
107
108impl ApiCommand {
109    /// Perform command action
110    pub async fn take_action(
111        &self,
112        parsed_args: &Cli,
113        client: &mut AsyncOpenStack,
114    ) -> Result<(), OpenStackCliError> {
115        info!("Perform REST API call {:?}", self);
116
117        let op = OutputProcessor::from_args(parsed_args);
118        op.validate_args(parsed_args)?;
119
120        let service_type = ServiceType::from(self.service_type.as_str());
121
122        client.discover_service_endpoint(&service_type).await?;
123
124        let service_endpoint = client.get_service_endpoint(&service_type, None)?;
125
126        let endpoint = service_endpoint.build_request_url(&self.url)?;
127
128        let mut req = http::Request::builder()
129            .method::<http::Method>(self.method.clone().into())
130            .uri(url_to_http_uri(endpoint))
131            .header(
132                http::header::ACCEPT,
133                http::HeaderValue::from_static("application/json"),
134            );
135
136        let headers = req.headers_mut().unwrap();
137        for (name, val) in &self.header {
138            headers.insert(
139                http::HeaderName::from_lowercase(name.to_lowercase().as_bytes()).unwrap(),
140                http::HeaderValue::from_str(val.as_str()).unwrap(),
141            );
142        }
143
144        let rsp = client
145            .rest_async(req, self.body.clone().unwrap_or_default().into_bytes())
146            .await?;
147
148        info!("Response = {:?}", rsp);
149        if let Some(content_type) = rsp.headers().get("content-type") {
150            if content_type == "application/json" {
151                if !rsp.body().is_empty() {
152                    let data: Value = serde_json::from_slice(rsp.body())?;
153                    op.output_machine(data)?;
154                }
155            } else {
156                io::stdout().write_all(rsp.body())?;
157            }
158        }
159
160        Ok(())
161    }
162}