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"
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",

View File

@ -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
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)]
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!(),
}
}