minccino/src/yui/routes.rs
snow flurry cecc8e7b2c Limit data sent on /_yui/instances
Don't send ssh key and user-data when listing all hosts, to save on data
2022-09-24 20:03:20 -07:00

215 lines
6.3 KiB
Rust

use std::convert::TryInto;
use crate::entity::*;
use crate::macros::api_err;
use crate::types::MacAddr;
use crate::types::*;
use crate::yui::auth::*;
use crate::yui::*;
use crate::Instances;
use rocket::serde::json::Json;
use rocket_db_pools::Connection;
use sea_orm::ActiveValue::{NotSet, Set};
use sea_orm::QuerySelect;
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, ModelTrait, PaginatorTrait, QueryFilter,
};
/// Create a new instance
#[post("/instances", format = "application/json", data = "<iinfo>")]
pub async fn new_instance(
db: Connection<Instances>,
iinfo: Json<InstanceReq>,
_api: APIUser,
) -> Result<Json<InstanceResponse>, APIError> {
let iinfo: InstanceReq = iinfo.into_inner();
let ii_name = iinfo.name.clone();
// loosely validate ssh keys
if let Some(ssh_keys) = &iinfo.ssh_keys {
for (i, key) in ssh_keys.iter().enumerate() {
if key.contains('\n') {
return Err(api_err!(
BadRequest,
"SSH Key {} contains newlines. Ensure SSH keys are split as an array",
i
));
}
}
}
// validation: confirm hostname isn't in use
if instance_info::Entity::find()
.filter(instance_info::Column::Hostname.eq(ii_name))
.count(&*db)
.await
.map_err(|e| api_err!(InternalServerError, "Database access failed: {:?}", e))?
!= 0
{
return Err(api_err!(BadRequest, "Hostname already exists"));
}
// validation: confirm mac address isn't in use
let inst_mac = MacAddr::try_from(iinfo.mac_address)
.map_err(|_| api_err!(BadRequest, "MAC address is invalid"))?;
if instance_info::Entity::find()
.filter(instance_info::Column::MacAddress.eq(inst_mac.to_string()))
.count(&*db)
.await
.map_err(|e| api_err!(InternalServerError, "Database access failed: {:?}", e))?
!= 0
{
return Err(api_err!(Conflict, "MAC Address is in use"));
}
// create and commit the instance info
let newid: u32 = (inst_mac[3] as u32) << 24
| (inst_mac[4] as u32) << 16
| (inst_mac[5] as u32) << 8
| (rand::random::<u8>() as u32);
let iisql = instance_info::ActiveModel {
id: Set(newid),
hostname: Set(iinfo.name.clone()),
mac_address: Set(inst_mac.to_string()),
ssh_keys: match iinfo.ssh_keys {
Some(keys) => Set(Some(keys.join("\n"))),
None => NotSet,
},
user_data: Set(iinfo
.user_data
.map(base64::decode)
.map_or(Ok(None), |v| v.map(Some))
.map_err(|e| api_err!(BadRequest, "Error decoding user-data: {:?}", e))?),
};
match iisql.insert(&*db).await {
Ok(v) => Ok(Json(InstanceResponse::new(vec![v]))),
Err(err) => Err(api_err!(
InternalServerError,
"Failed to add record: {:?}",
err
)),
}
}
/// List all instances in the database
#[get("/instances")]
pub async fn get_instances(
db: Connection<Instances>,
_api: APIUser,
) -> Result<Json<InstanceResponse>, APIError> {
match instance_info::Entity::find()
.column(instance_info::Column::Id)
.column(instance_info::Column::MacAddress)
.column(instance_info::Column::Hostname)
.all(&*db)
.await
{
Ok(v) => Ok(Json(InstanceResponse::new(v))),
Err(err) => Err(api_err!(
InternalServerError,
"Failed to get records: {:?}",
err
)),
}
}
/// Get a specified instance
#[get("/instances/<id>")]
pub async fn get_instance(
db: Connection<Instances>,
id: u32,
_api: APIUser,
) -> Result<Json<InstanceResponse>, APIError> {
match instance_info::Entity::find_by_id(id)
.one(&*db)
.await
.map_err(|e| api_err!(InternalServerError, "Database error: {:?}", e))?
{
Some(v) => Ok(Json(InstanceResponse::new(vec![v]))),
None => Err(api_err!(NotFound, "Instance not found")),
}
}
/// Delete an instance from the database
#[delete("/instances/<id>")]
pub async fn delete_instance(
db: Connection<Instances>,
id: u32,
_api: APIUser,
) -> Result<(), APIError> {
match instance_info::Entity::find_by_id(id)
.one(&*db)
.await
.map_err(|e| api_err!(InternalServerError, "Error deleting instance: {:?}", e))?
{
Some(ent) => {
ent.delete(&*db)
.await
.map_err(|e| api_err!(InternalServerError, "Error deleting instance: {:?}", e))?;
Ok(())
}
None => Err(api_err!(NotFound, "Instance not found")),
}
}
// Set a default (primarily, ssh-keys)
#[post("/config/<key>", format = "plain", data = "<val>")]
async fn set_defaults(
db: Connection<Instances>,
key: String,
val: String,
_api: APIUser,
) -> Result<(), APIError> {
let key: DefaultKey = key
.try_into()
.map_err(|_| api_err!(BadRequest, "Invalid default key"))?;
match defaults::Entity::find_by_id(key)
.one(&*db)
.await
.map_err(|e| api_err!(InternalServerError, "Error getting default: {:?}", e))?
{
Some(def) => {
let mut model: defaults::ActiveModel = def.into();
model.value = Set(val);
model.update(&*db).await.map(|_| ())
}
None => {
let model = defaults::ActiveModel {
key: Set(key),
value: Set(val),
};
model.insert(&*db).await.map(|_| ())
}
}
.map_err(|e| api_err!(InternalServerError, "Error setting `{}`: {:?}", key, e))
}
// get a default setting
#[get("/config/<key>")]
async fn get_defaults(
db: Connection<Instances>,
key: String,
_api: APIUser,
) -> Result<String, APIError> {
let key: DefaultKey = key
.try_into()
.map_err(|_| api_err!(BadRequest, "Invalid default key"))?;
defaults::Entity::find_by_id(key)
.one(&*db)
.await
.map_err(|e| api_err!(InternalServerError, "Error getting default: {:?}", e))?
.map_or(Err(api_err!(NotFound, "Key not yet set")), |v| Ok(v.value))
}
pub fn all() -> Vec<rocket::Route> {
routes![
new_instance,
get_instances,
get_instance,
delete_instance,
set_defaults,
get_defaults,
]
}