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:
Rob Bradford 2020-10-23 11:20:37 +01:00 committed by Sebastien Boeuf
parent 654846f433
commit 8de3bd728c
5 changed files with 213 additions and 161 deletions

5
Cargo.lock generated
View File

@ -46,6 +46,10 @@ version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1fd36ffbb1fb7c834eac128ea8d0e310c5aeb635548f9d58861e1308d46e71c" checksum = "a1fd36ffbb1fb7c834eac128ea8d0e310c5aeb635548f9d58861e1308d46e71c"
[[package]]
name = "api_client"
version = "0.1.0"
[[package]] [[package]]
name = "arc-swap" name = "arc-swap"
version = "0.4.7" version = "0.4.7"
@ -208,6 +212,7 @@ dependencies = [
name = "cloud-hypervisor" name = "cloud-hypervisor"
version = "0.10.0" version = "0.10.0"
dependencies = [ dependencies = [
"api_client",
"clap", "clap",
"credibility", "credibility",
"dirs", "dirs",

View File

@ -13,6 +13,7 @@ homepage = "https://github.com/cloud-hypervisor/cloud-hypervisor"
lto = true lto = true
[dependencies] [dependencies]
api_client = { path = "api_client" }
clap = { version = "2.33.3", features = ["wrap_help"] } clap = { version = "2.33.3", features = ["wrap_help"] }
hypervisor = { path = "hypervisor" } hypervisor = { path = "hypervisor" }
libc = "0.2.79" libc = "0.2.79"
@ -62,6 +63,7 @@ integration_tests = []
[workspace] [workspace]
members = [ members = [
"acpi_tables", "acpi_tables",
"api_client",
"arch", "arch",
"arch_gen", "arch_gen",
"block_util", "block_util",

5
api_client/Cargo.toml Normal file
View 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
View 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(())
}

View File

@ -5,23 +5,22 @@
#[macro_use(crate_authors)] #[macro_use(crate_authors)]
extern crate clap; extern crate clap;
extern crate api_client;
extern crate serde_json; extern crate serde_json;
extern crate vmm; extern crate vmm;
use api_client::simple_api_command;
use api_client::Error as ApiClientError;
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
use option_parser::{ByteSized, ByteSizedParseError}; use option_parser::{ByteSized, ByteSizedParseError};
use std::fmt; use std::fmt;
use std::io::{Read, Write};
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
use std::process; use std::process;
#[derive(Debug)] #[derive(Debug)]
enum Error { enum Error {
Socket(std::io::Error), Connect(std::io::Error),
StatusCodeParsing(std::num::ParseIntError), ApiClient(ApiClientError),
MissingProtocol,
ContentLengthParsing(std::num::ParseIntError),
ServerResponse(StatusCode, Option<String>),
InvalidCPUCount(std::num::ParseIntError), InvalidCPUCount(std::num::ParseIntError),
InvalidMemorySize(ByteSizedParseError), InvalidMemorySize(ByteSizedParseError),
InvalidBalloonSize(ByteSizedParseError), InvalidBalloonSize(ByteSizedParseError),
@ -38,17 +37,8 @@ impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use Error::*; use Error::*;
match self { match self {
Socket(e) => write!(f, "Error writing to HTTP socket: {}", e), ApiClient(e) => e.fmt(f),
StatusCodeParsing(e) => write!(f, "Error parsing HTTP status code: {}", e), Connect(e) => write!(f, "Error openning HTTP socket: {}", 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)
}
}
InvalidCPUCount(e) => write!(f, "Error parsing CPU count: {}", e), InvalidCPUCount(e) => write!(f, "Error parsing CPU count: {}", e),
InvalidMemorySize(e) => write!(f, "Error parsing memory size: {:?}", e), InvalidMemorySize(e) => write!(f, "Error parsing memory size: {:?}", e),
InvalidBalloonSize(e) => write!(f, "Error parsing balloon 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( fn resize_api_command(
socket: &mut UnixStream, socket: &mut UnixStream,
cpus: Option<&str>, cpus: Option<&str>,
@ -249,6 +99,7 @@ fn resize_api_command(
"resize", "resize",
Some(&serde_json::to_string(&resize).unwrap()), 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> { 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", "resize-zone",
Some(&serde_json::to_string(&resize_zone).unwrap()), Some(&serde_json::to_string(&resize_zone).unwrap()),
) )
.map_err(Error::ApiClient)
} }
fn add_device_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Error> { 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", "add-device",
Some(&serde_json::to_string(&device_config).unwrap()), Some(&serde_json::to_string(&device_config).unwrap()),
) )
.map_err(Error::ApiClient)
} }
fn remove_device_api_command(socket: &mut UnixStream, id: &str) -> Result<(), Error> { 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", "remove-device",
Some(&serde_json::to_string(&remove_device_data).unwrap()), 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> { 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", "add-disk",
Some(&serde_json::to_string(&disk_config).unwrap()), Some(&serde_json::to_string(&disk_config).unwrap()),
) )
.map_err(Error::ApiClient)
} }
fn add_fs_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Error> { 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", "add-fs",
Some(&serde_json::to_string(&fs_config).unwrap()), Some(&serde_json::to_string(&fs_config).unwrap()),
) )
.map_err(Error::ApiClient)
} }
fn add_pmem_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Error> { 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", "add-pmem",
Some(&serde_json::to_string(&pmem_config).unwrap()), Some(&serde_json::to_string(&pmem_config).unwrap()),
) )
.map_err(Error::ApiClient)
} }
fn add_net_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Error> { 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", "add-net",
Some(&serde_json::to_string(&net_config).unwrap()), Some(&serde_json::to_string(&net_config).unwrap()),
) )
.map_err(Error::ApiClient)
} }
fn add_vsock_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Error> { 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", "add-vsock",
Some(&serde_json::to_string(&vsock_config).unwrap()), Some(&serde_json::to_string(&vsock_config).unwrap()),
) )
.map_err(Error::ApiClient)
} }
fn snapshot_api_command(socket: &mut UnixStream, url: &str) -> Result<(), Error> { 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", "snapshot",
Some(&serde_json::to_string(&snapshot_config).unwrap()), Some(&serde_json::to_string(&snapshot_config).unwrap()),
) )
.map_err(Error::ApiClient)
} }
fn restore_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Error> { 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", "restore",
Some(&serde_json::to_string(&restore_config).unwrap()), Some(&serde_json::to_string(&restore_config).unwrap()),
) )
.map_err(Error::ApiClient)
} }
fn do_command(matches: &ArgMatches) -> Result<(), Error> { fn do_command(matches: &ArgMatches) -> Result<(), Error> {
let mut socket = 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() { match matches.subcommand_name() {
Some("info") => simple_api_command(&mut socket, "GET", "info", None), Some("info") => {
Some("counters") => simple_api_command(&mut socket, "GET", "counters", None), 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( Some("resize") => resize_api_command(
&mut socket, &mut socket,
matches matches
@ -476,7 +341,7 @@ fn do_command(matches: &ArgMatches) -> Result<(), Error> {
.value_of("restore_config") .value_of("restore_config")
.unwrap(), .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!(), None => unreachable!(),
} }
} }