mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2024-12-22 05:35:20 +00:00
ch-remote, api_client: Split HTTP/API client code into new crate
Split out the HTTP request handling code from ch-remote into a new crate which can be used in other places where talking to the API server by HTTP is necessary. Signed-off-by: Rob Bradford <robert.bradford@intel.com>
This commit is contained in:
parent
654846f433
commit
8de3bd728c
5
Cargo.lock
generated
5
Cargo.lock
generated
@ -46,6 +46,10 @@ version = "1.0.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1fd36ffbb1fb7c834eac128ea8d0e310c5aeb635548f9d58861e1308d46e71c"
|
||||
|
||||
[[package]]
|
||||
name = "api_client"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "0.4.7"
|
||||
@ -208,6 +212,7 @@ dependencies = [
|
||||
name = "cloud-hypervisor"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"api_client",
|
||||
"clap",
|
||||
"credibility",
|
||||
"dirs",
|
||||
|
@ -13,6 +13,7 @@ homepage = "https://github.com/cloud-hypervisor/cloud-hypervisor"
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
api_client = { path = "api_client" }
|
||||
clap = { version = "2.33.3", features = ["wrap_help"] }
|
||||
hypervisor = { path = "hypervisor" }
|
||||
libc = "0.2.79"
|
||||
@ -62,6 +63,7 @@ integration_tests = []
|
||||
[workspace]
|
||||
members = [
|
||||
"acpi_tables",
|
||||
"api_client",
|
||||
"arch",
|
||||
"arch_gen",
|
||||
"block_util",
|
||||
|
5
api_client/Cargo.toml
Normal file
5
api_client/Cargo.toml
Normal file
@ -0,0 +1,5 @@
|
||||
[package]
|
||||
name = "api_client"
|
||||
version = "0.1.0"
|
||||
authors = ["The Cloud Hypervisor Authors"]
|
||||
edition = "2018"
|
175
api_client/src/lib.rs
Normal file
175
api_client/src/lib.rs
Normal file
@ -0,0 +1,175 @@
|
||||
// Copyright © 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
use std::fmt;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Socket(std::io::Error),
|
||||
StatusCodeParsing(std::num::ParseIntError),
|
||||
MissingProtocol,
|
||||
ContentLengthParsing(std::num::ParseIntError),
|
||||
ServerResponse(StatusCode, Option<String>),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use Error::*;
|
||||
match self {
|
||||
Socket(e) => write!(f, "Error writing to or reading from HTTP socket: {}", e),
|
||||
StatusCodeParsing(e) => write!(f, "Error parsing HTTP status code: {}", e),
|
||||
MissingProtocol => write!(f, "HTTP output is missing protocol statement"),
|
||||
ContentLengthParsing(e) => write!(f, "Error parsing HTTP Content-Length field: {}", e),
|
||||
ServerResponse(s, o) => {
|
||||
if let Some(o) = o {
|
||||
write!(f, "Server responded with an error: {:?}: {}", s, o)
|
||||
} else {
|
||||
write!(f, "Server responded with an error: {:?}", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum StatusCode {
|
||||
Continue,
|
||||
OK,
|
||||
NoContent,
|
||||
BadRequest,
|
||||
NotFound,
|
||||
InternalServerError,
|
||||
NotImplemented,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl StatusCode {
|
||||
fn from_raw(code: usize) -> StatusCode {
|
||||
match code {
|
||||
100 => StatusCode::Continue,
|
||||
200 => StatusCode::OK,
|
||||
204 => StatusCode::NoContent,
|
||||
400 => StatusCode::BadRequest,
|
||||
404 => StatusCode::NotFound,
|
||||
500 => StatusCode::InternalServerError,
|
||||
501 => StatusCode::NotImplemented,
|
||||
_ => StatusCode::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(code: &str) -> Result<StatusCode, Error> {
|
||||
Ok(StatusCode::from_raw(
|
||||
code.trim().parse().map_err(Error::StatusCodeParsing)?,
|
||||
))
|
||||
}
|
||||
|
||||
fn is_server_error(self) -> bool {
|
||||
!matches!(
|
||||
self,
|
||||
StatusCode::OK | StatusCode::Continue | StatusCode::NoContent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_header<'a>(res: &'a str, header: &'a str) -> Option<&'a str> {
|
||||
let header_str = format!("{}: ", header);
|
||||
if let Some(o) = res.find(&header_str) {
|
||||
Some(&res[o + header_str.len()..o + res[o..].find('\r').unwrap()])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_status_code(res: &str) -> Result<StatusCode, Error> {
|
||||
if let Some(o) = res.find("HTTP/1.1") {
|
||||
Ok(StatusCode::parse(
|
||||
&res[o + "HTTP/1.1 ".len()..res[o..].find('\r').unwrap()],
|
||||
)?)
|
||||
} else {
|
||||
Err(Error::MissingProtocol)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_http_response(socket: &mut dyn Read) -> Result<Option<String>, Error> {
|
||||
let mut res = String::new();
|
||||
let mut body_offset = None;
|
||||
let mut content_length: Option<usize> = None;
|
||||
loop {
|
||||
let mut bytes = vec![0; 256];
|
||||
let count = socket.read(&mut bytes).map_err(Error::Socket)?;
|
||||
res.push_str(std::str::from_utf8(&bytes[0..count]).unwrap());
|
||||
|
||||
// End of headers
|
||||
if let Some(o) = res.find("\r\n\r\n") {
|
||||
body_offset = Some(o + "\r\n\r\n".len());
|
||||
|
||||
// With all headers available we can see if there is any body
|
||||
content_length = if let Some(length) = get_header(&res, "Content-Length") {
|
||||
Some(length.trim().parse().map_err(Error::ContentLengthParsing)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if content_length.is_none() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(body_offset) = body_offset {
|
||||
if let Some(content_length) = content_length {
|
||||
if res.len() >= content_length + body_offset {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let body_string = content_length.and(Some(String::from(&res[body_offset.unwrap()..])));
|
||||
let status_code = get_status_code(&res)?;
|
||||
|
||||
if status_code.is_server_error() {
|
||||
Err(Error::ServerResponse(status_code, body_string))
|
||||
} else {
|
||||
Ok(body_string)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn simple_api_command<T: Read + Write>(
|
||||
socket: &mut T,
|
||||
method: &str,
|
||||
c: &str,
|
||||
request_body: Option<&str>,
|
||||
) -> Result<(), Error> {
|
||||
socket
|
||||
.write_all(
|
||||
format!(
|
||||
"{} /api/v1/vm.{} HTTP/1.1\r\nHost: localhost\r\nAccept: */*\r\n",
|
||||
method, c
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.map_err(Error::Socket)?;
|
||||
|
||||
if let Some(request_body) = request_body {
|
||||
socket
|
||||
.write_all(format!("Content-Length: {}\r\n", request_body.len()).as_bytes())
|
||||
.map_err(Error::Socket)?;
|
||||
}
|
||||
|
||||
socket.write_all(b"\r\n").map_err(Error::Socket)?;
|
||||
|
||||
if let Some(request_body) = request_body {
|
||||
socket
|
||||
.write_all(request_body.as_bytes())
|
||||
.map_err(Error::Socket)?;
|
||||
}
|
||||
|
||||
socket.flush().map_err(Error::Socket)?;
|
||||
|
||||
if let Some(body) = parse_http_response(socket)? {
|
||||
println!("{}", body);
|
||||
}
|
||||
Ok(())
|
||||
}
|
@ -5,23 +5,22 @@
|
||||
|
||||
#[macro_use(crate_authors)]
|
||||
extern crate clap;
|
||||
extern crate api_client;
|
||||
extern crate serde_json;
|
||||
extern crate vmm;
|
||||
|
||||
use api_client::simple_api_command;
|
||||
use api_client::Error as ApiClientError;
|
||||
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
|
||||
use option_parser::{ByteSized, ByteSizedParseError};
|
||||
use std::fmt;
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::process;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Error {
|
||||
Socket(std::io::Error),
|
||||
StatusCodeParsing(std::num::ParseIntError),
|
||||
MissingProtocol,
|
||||
ContentLengthParsing(std::num::ParseIntError),
|
||||
ServerResponse(StatusCode, Option<String>),
|
||||
Connect(std::io::Error),
|
||||
ApiClient(ApiClientError),
|
||||
InvalidCPUCount(std::num::ParseIntError),
|
||||
InvalidMemorySize(ByteSizedParseError),
|
||||
InvalidBalloonSize(ByteSizedParseError),
|
||||
@ -38,17 +37,8 @@ impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use Error::*;
|
||||
match self {
|
||||
Socket(e) => write!(f, "Error writing to HTTP socket: {}", e),
|
||||
StatusCodeParsing(e) => write!(f, "Error parsing HTTP status code: {}", e),
|
||||
MissingProtocol => write!(f, "HTTP output is missing protocol statement"),
|
||||
ContentLengthParsing(e) => write!(f, "Error parsing HTTP Content-Length field: {}", e),
|
||||
ServerResponse(s, o) => {
|
||||
if let Some(o) = o {
|
||||
write!(f, "Server responded with an error: {:?}: {}", s, o)
|
||||
} else {
|
||||
write!(f, "Server responded with an error: {:?}", s)
|
||||
}
|
||||
}
|
||||
ApiClient(e) => e.fmt(f),
|
||||
Connect(e) => write!(f, "Error openning HTTP socket: {}", e),
|
||||
InvalidCPUCount(e) => write!(f, "Error parsing CPU count: {}", e),
|
||||
InvalidMemorySize(e) => write!(f, "Error parsing memory size: {:?}", e),
|
||||
InvalidBalloonSize(e) => write!(f, "Error parsing balloon size: {:?}", e),
|
||||
@ -63,146 +53,6 @@ impl fmt::Display for Error {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum StatusCode {
|
||||
Continue,
|
||||
OK,
|
||||
NoContent,
|
||||
BadRequest,
|
||||
NotFound,
|
||||
InternalServerError,
|
||||
NotImplemented,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl StatusCode {
|
||||
fn from_raw(code: usize) -> StatusCode {
|
||||
match code {
|
||||
100 => StatusCode::Continue,
|
||||
200 => StatusCode::OK,
|
||||
204 => StatusCode::NoContent,
|
||||
400 => StatusCode::BadRequest,
|
||||
404 => StatusCode::NotFound,
|
||||
500 => StatusCode::InternalServerError,
|
||||
501 => StatusCode::NotImplemented,
|
||||
_ => StatusCode::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(code: &str) -> Result<StatusCode, Error> {
|
||||
Ok(StatusCode::from_raw(
|
||||
code.trim().parse().map_err(Error::StatusCodeParsing)?,
|
||||
))
|
||||
}
|
||||
|
||||
fn is_server_error(self) -> bool {
|
||||
!matches!(
|
||||
self,
|
||||
StatusCode::OK | StatusCode::Continue | StatusCode::NoContent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_header<'a>(res: &'a str, header: &'a str) -> Option<&'a str> {
|
||||
let header_str = format!("{}: ", header);
|
||||
if let Some(o) = res.find(&header_str) {
|
||||
Some(&res[o + header_str.len()..o + res[o..].find('\r').unwrap()])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_status_code(res: &str) -> Result<StatusCode, Error> {
|
||||
if let Some(o) = res.find("HTTP/1.1") {
|
||||
Ok(StatusCode::parse(
|
||||
&res[o + "HTTP/1.1 ".len()..res[o..].find('\r').unwrap()],
|
||||
)?)
|
||||
} else {
|
||||
Err(Error::MissingProtocol)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_http_response(socket: &mut dyn Read) -> Result<Option<String>, Error> {
|
||||
let mut res = String::new();
|
||||
let mut body_offset = None;
|
||||
let mut content_length: Option<usize> = None;
|
||||
loop {
|
||||
let mut bytes = vec![0; 256];
|
||||
let count = socket.read(&mut bytes).map_err(Error::Socket)?;
|
||||
res.push_str(std::str::from_utf8(&bytes[0..count]).unwrap());
|
||||
|
||||
// End of headers
|
||||
if let Some(o) = res.find("\r\n\r\n") {
|
||||
body_offset = Some(o + "\r\n\r\n".len());
|
||||
|
||||
// With all headers available we can see if there is any body
|
||||
content_length = if let Some(length) = get_header(&res, "Content-Length") {
|
||||
Some(length.trim().parse().map_err(Error::ContentLengthParsing)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if content_length.is_none() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(body_offset) = body_offset {
|
||||
if let Some(content_length) = content_length {
|
||||
if res.len() >= content_length + body_offset {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let body_string = content_length.and(Some(String::from(&res[body_offset.unwrap()..])));
|
||||
let status_code = get_status_code(&res)?;
|
||||
|
||||
if status_code.is_server_error() {
|
||||
Err(Error::ServerResponse(status_code, body_string))
|
||||
} else {
|
||||
Ok(body_string)
|
||||
}
|
||||
}
|
||||
|
||||
fn simple_api_command<T: Read + Write>(
|
||||
socket: &mut T,
|
||||
method: &str,
|
||||
c: &str,
|
||||
request_body: Option<&str>,
|
||||
) -> Result<(), Error> {
|
||||
socket
|
||||
.write_all(
|
||||
format!(
|
||||
"{} /api/v1/vm.{} HTTP/1.1\r\nHost: localhost\r\nAccept: */*\r\n",
|
||||
method, c
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.map_err(Error::Socket)?;
|
||||
|
||||
if let Some(request_body) = request_body {
|
||||
socket
|
||||
.write_all(format!("Content-Length: {}\r\n", request_body.len()).as_bytes())
|
||||
.map_err(Error::Socket)?;
|
||||
}
|
||||
|
||||
socket.write_all(b"\r\n").map_err(Error::Socket)?;
|
||||
|
||||
if let Some(request_body) = request_body {
|
||||
socket
|
||||
.write_all(request_body.as_bytes())
|
||||
.map_err(Error::Socket)?;
|
||||
}
|
||||
|
||||
socket.flush().map_err(Error::Socket)?;
|
||||
|
||||
if let Some(body) = parse_http_response(socket)? {
|
||||
println!("{}", body);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resize_api_command(
|
||||
socket: &mut UnixStream,
|
||||
cpus: Option<&str>,
|
||||
@ -249,6 +99,7 @@ fn resize_api_command(
|
||||
"resize",
|
||||
Some(&serde_json::to_string(&resize).unwrap()),
|
||||
)
|
||||
.map_err(Error::ApiClient)
|
||||
}
|
||||
|
||||
fn resize_zone_api_command(socket: &mut UnixStream, id: &str, size: &str) -> Result<(), Error> {
|
||||
@ -266,6 +117,7 @@ fn resize_zone_api_command(socket: &mut UnixStream, id: &str, size: &str) -> Res
|
||||
"resize-zone",
|
||||
Some(&serde_json::to_string(&resize_zone).unwrap()),
|
||||
)
|
||||
.map_err(Error::ApiClient)
|
||||
}
|
||||
|
||||
fn add_device_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Error> {
|
||||
@ -277,6 +129,7 @@ fn add_device_api_command(socket: &mut UnixStream, config: &str) -> Result<(), E
|
||||
"add-device",
|
||||
Some(&serde_json::to_string(&device_config).unwrap()),
|
||||
)
|
||||
.map_err(Error::ApiClient)
|
||||
}
|
||||
|
||||
fn remove_device_api_command(socket: &mut UnixStream, id: &str) -> Result<(), Error> {
|
||||
@ -288,6 +141,7 @@ fn remove_device_api_command(socket: &mut UnixStream, id: &str) -> Result<(), Er
|
||||
"remove-device",
|
||||
Some(&serde_json::to_string(&remove_device_data).unwrap()),
|
||||
)
|
||||
.map_err(Error::ApiClient)
|
||||
}
|
||||
|
||||
fn add_disk_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Error> {
|
||||
@ -299,6 +153,7 @@ fn add_disk_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Err
|
||||
"add-disk",
|
||||
Some(&serde_json::to_string(&disk_config).unwrap()),
|
||||
)
|
||||
.map_err(Error::ApiClient)
|
||||
}
|
||||
|
||||
fn add_fs_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Error> {
|
||||
@ -310,6 +165,7 @@ fn add_fs_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Error
|
||||
"add-fs",
|
||||
Some(&serde_json::to_string(&fs_config).unwrap()),
|
||||
)
|
||||
.map_err(Error::ApiClient)
|
||||
}
|
||||
|
||||
fn add_pmem_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Error> {
|
||||
@ -321,6 +177,7 @@ fn add_pmem_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Err
|
||||
"add-pmem",
|
||||
Some(&serde_json::to_string(&pmem_config).unwrap()),
|
||||
)
|
||||
.map_err(Error::ApiClient)
|
||||
}
|
||||
|
||||
fn add_net_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Error> {
|
||||
@ -332,6 +189,7 @@ fn add_net_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Erro
|
||||
"add-net",
|
||||
Some(&serde_json::to_string(&net_config).unwrap()),
|
||||
)
|
||||
.map_err(Error::ApiClient)
|
||||
}
|
||||
|
||||
fn add_vsock_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Error> {
|
||||
@ -343,6 +201,7 @@ fn add_vsock_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Er
|
||||
"add-vsock",
|
||||
Some(&serde_json::to_string(&vsock_config).unwrap()),
|
||||
)
|
||||
.map_err(Error::ApiClient)
|
||||
}
|
||||
|
||||
fn snapshot_api_command(socket: &mut UnixStream, url: &str) -> Result<(), Error> {
|
||||
@ -356,6 +215,7 @@ fn snapshot_api_command(socket: &mut UnixStream, url: &str) -> Result<(), Error>
|
||||
"snapshot",
|
||||
Some(&serde_json::to_string(&snapshot_config).unwrap()),
|
||||
)
|
||||
.map_err(Error::ApiClient)
|
||||
}
|
||||
|
||||
fn restore_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Error> {
|
||||
@ -367,15 +227,20 @@ fn restore_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Erro
|
||||
"restore",
|
||||
Some(&serde_json::to_string(&restore_config).unwrap()),
|
||||
)
|
||||
.map_err(Error::ApiClient)
|
||||
}
|
||||
|
||||
fn do_command(matches: &ArgMatches) -> Result<(), Error> {
|
||||
let mut socket =
|
||||
UnixStream::connect(matches.value_of("api-socket").unwrap()).map_err(Error::Socket)?;
|
||||
UnixStream::connect(matches.value_of("api-socket").unwrap()).map_err(Error::Connect)?;
|
||||
|
||||
match matches.subcommand_name() {
|
||||
Some("info") => simple_api_command(&mut socket, "GET", "info", None),
|
||||
Some("counters") => simple_api_command(&mut socket, "GET", "counters", None),
|
||||
Some("info") => {
|
||||
simple_api_command(&mut socket, "GET", "info", None).map_err(Error::ApiClient)
|
||||
}
|
||||
Some("counters") => {
|
||||
simple_api_command(&mut socket, "GET", "counters", None).map_err(Error::ApiClient)
|
||||
}
|
||||
Some("resize") => resize_api_command(
|
||||
&mut socket,
|
||||
matches
|
||||
@ -476,7 +341,7 @@ fn do_command(matches: &ArgMatches) -> Result<(), Error> {
|
||||
.value_of("restore_config")
|
||||
.unwrap(),
|
||||
),
|
||||
Some(c) => simple_api_command(&mut socket, "PUT", c, None),
|
||||
Some(c) => simple_api_command(&mut socket, "PUT", c, None).map_err(Error::ApiClient),
|
||||
None => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user