Initial commit
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/target
|
||||
result
|
||||
result-*
|
||||
|
||||
*.qcow2
|
||||
1220
Cargo.lock
generated
Normal file
1220
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
Cargo.toml
Normal file
45
Cargo.toml
Normal file
@@ -0,0 +1,45 @@
|
||||
[package]
|
||||
name = "donquota"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
resolver = "2"
|
||||
license = "BSD-3-Clause"
|
||||
authors = [
|
||||
"Programvareverkstedet <projects@pvv.ntnu.no>",
|
||||
]
|
||||
description = "Modern remote quotactl(2)"
|
||||
categories = ["command-line-interface", "command-line-utilities"]
|
||||
readme = "README.md"
|
||||
autobins = false
|
||||
autolib = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.100"
|
||||
chrono = { version = "0.4.42", features = ["serde"] }
|
||||
clap = { version = "4.5.53", features = ["derive"] }
|
||||
futures-util = "0.3.31"
|
||||
nix = { version = "0.30.1", features = [] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
tokio = { version = "1.49.0", features = ["macros", "net", "rt-multi-thread", "signal", "sync", "time"] }
|
||||
toml = "0.9.10"
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
||||
sd-notify = "0.4.5"
|
||||
serde_json = "1.0.148"
|
||||
zlink = { version = "0.2.0", features = ["introspection"] }
|
||||
clap_complete = "4.5.65"
|
||||
|
||||
[lib]
|
||||
name = "donquota_lib"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "donquotad"
|
||||
bench = false
|
||||
path = "src/bin/donquotad.rs"
|
||||
|
||||
[profile.releaselto]
|
||||
inherits = "release"
|
||||
strip = true
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
26
LICENSE
Normal file
26
LICENSE
Normal file
@@ -0,0 +1,26 @@
|
||||
Copyright (c) 2026, Programvareverkstedet <projects@pvv.ntnu.no>
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
6
README.md
Normal file
6
README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
[](https://pages.pvv.ntnu.no/Projects/don-quota/main/coverage/)
|
||||
[](https://pages.pvv.ntnu.no/Projects/don-quota/main/docs/don-quota/)
|
||||
|
||||
# DonQuota
|
||||
|
||||
Modern remote [`quotactl(2)`](https://man7.org/linux/man-pages/man2/quotactl.2.html)
|
||||
48
flake.lock
generated
Normal file
48
flake.lock
generated
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1767116409,
|
||||
"narHash": "sha256-5vKw92l1GyTnjoLzEagJy5V5mDFck72LiQWZSOnSicw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "cad22e7d996aea55ecab064e84834289143e44a0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1767322002,
|
||||
"narHash": "sha256-yHKXXw2OWfIFsyTjduB4EyFwR0SYYF0hK8xI9z4NIn0=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "03c6e38661c02a27ca006a284813afdc461e9f7e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
81
flake.nix
Normal file
81
flake.nix
Normal file
@@ -0,0 +1,81 @@
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, rust-overlay}:
|
||||
let
|
||||
inherit (nixpkgs) lib;
|
||||
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
"x86_64-darwin"
|
||||
"aarch64-darwin"
|
||||
"armv7l-linux"
|
||||
];
|
||||
|
||||
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [
|
||||
(import rust-overlay)
|
||||
];
|
||||
};
|
||||
|
||||
rust-bin = rust-overlay.lib.mkRustBin { } pkgs.buildPackages;
|
||||
toolchain = rust-bin.nightly.latest.default.override {
|
||||
extensions = [ "rust-src" ];
|
||||
};
|
||||
in f system pkgs toolchain);
|
||||
in {
|
||||
devShell = forAllSystems (system: pkgs: toolchain: pkgs.mkShell {
|
||||
nativeBuildInputs = with pkgs; [
|
||||
toolchain
|
||||
cargo-edit
|
||||
];
|
||||
|
||||
env = {
|
||||
RUST_SRC_PATH = "${toolchain}/lib/rustlib/src/rust/library";
|
||||
RUSTFLAGS = "-Zhigher-ranked-assumptions";
|
||||
};
|
||||
});
|
||||
|
||||
overlays = {
|
||||
default = self.overlays.donquota;
|
||||
donquota = final: prev: {
|
||||
inherit (self.packages.${prev.stdenv.hostPlatform.system}) donquota;
|
||||
};
|
||||
};
|
||||
|
||||
packages = forAllSystems (system: pkgs: _:
|
||||
let
|
||||
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
|
||||
cargoLock = ./Cargo.lock;
|
||||
src = lib.fileset.toSource {
|
||||
root = ./.;
|
||||
fileset = lib.fileset.unions [
|
||||
./src
|
||||
./Cargo.toml
|
||||
./Cargo.lock
|
||||
];
|
||||
};
|
||||
|
||||
rustPlatform = pkgs.makeRustPlatform {
|
||||
rustc = pkgs.rust-bin.nightly.latest.default;
|
||||
cargo = pkgs.rust-bin.nightly.latest.cargo;
|
||||
};
|
||||
in {
|
||||
default = self.packages.${system}.donquota;
|
||||
|
||||
donquota = pkgs.callPackage ./nix/package.nix { inherit cargoToml cargoLock src rustPlatform; };
|
||||
|
||||
filteredSource = pkgs.runCommandLocal "filtered-source" { } ''
|
||||
ln -s ${src} $out
|
||||
'';
|
||||
});
|
||||
};
|
||||
}
|
||||
42
nix/package.nix
Normal file
42
nix/package.nix
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
lib
|
||||
, rustPlatform
|
||||
, stdenv
|
||||
, buildPackages
|
||||
, installShellFiles
|
||||
|
||||
, cargoToml
|
||||
, cargoLock
|
||||
, src
|
||||
}:
|
||||
rustPlatform.buildRustPackage {
|
||||
pname = "donquota";
|
||||
inherit (cargoToml.package) version;
|
||||
inherit src;
|
||||
|
||||
cargoLock.lockFile = cargoLock;
|
||||
|
||||
buildType = "releaselto";
|
||||
|
||||
RUSTFLAGS = "-Zhigher-ranked-assumptions";
|
||||
|
||||
nativeBuildInputs = [ installShellFiles ];
|
||||
postInstall = let
|
||||
emulator = stdenv.hostPlatform.emulator buildPackages;
|
||||
installShellCompletions = lib.mapCartesianProduct ({ shell, command }: ''
|
||||
(
|
||||
export PATH="$out/bin:$PATH"
|
||||
"${emulator}" "${command}" --completions=${shell} > "$TMP/${command}.${shell}"
|
||||
)
|
||||
installShellCompletion "--${shell}" --cmd "${command}" "$TMP/${command}.${shell}"
|
||||
'') {
|
||||
shell = [ "bash" "zsh" "fish" ];
|
||||
command = [ "uquota" ];
|
||||
};
|
||||
in lib.concatStringsSep "\n" installShellCompletions;
|
||||
|
||||
meta = with lib; {
|
||||
license = licenses.mit;
|
||||
platforms = platforms.linux ++ platforms.darwin;
|
||||
};
|
||||
}
|
||||
91
src/bin/donquotad.rs
Normal file
91
src/bin/donquotad.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
os::fd::{AsRawFd, FromRawFd, OwnedFd},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
use donquota_lib::server::{config::DEFAULT_CONFIG_PATH, varlink_api::varlink_client_server_task};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
author = "Programvareverkstedet <projects@pvv.ntnu.no>",
|
||||
about,
|
||||
version
|
||||
)]
|
||||
struct Args {
|
||||
/// Path to configuration file
|
||||
#[arg(
|
||||
short = 'c',
|
||||
long = "config",
|
||||
default_value = DEFAULT_CONFIG_PATH,
|
||||
value_name = "PATH"
|
||||
)]
|
||||
config_path: PathBuf,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(fmt::layer())
|
||||
.with(EnvFilter::from_default_env())
|
||||
.init();
|
||||
|
||||
let _config = toml::from_str::<donquota_lib::server::config::Config>(
|
||||
&std::fs::read_to_string(&args.config_path).context(format!(
|
||||
"Failed to read configuration file {:?}",
|
||||
args.config_path,
|
||||
))?,
|
||||
)?;
|
||||
|
||||
let fd_map: HashMap<String, OwnedFd> = HashMap::from_iter(
|
||||
sd_notify::listen_fds_with_names(false)?.map(|(fd_num, name)| {
|
||||
(
|
||||
name.clone(),
|
||||
// SAFETY: please don't mess around with file descriptors in random places
|
||||
// around the codebase lol
|
||||
unsafe { std::os::fd::OwnedFd::from_raw_fd(fd_num) },
|
||||
)
|
||||
}),
|
||||
);
|
||||
|
||||
let mut join_set = tokio::task::JoinSet::new();
|
||||
|
||||
join_set.spawn(client_server(
|
||||
fd_map
|
||||
.get("client_socket")
|
||||
.context("DonQuote client-server socket fd not provided by systemd")?
|
||||
.try_clone()
|
||||
.context("Failed to clone DonQuote client-server socket fd")?,
|
||||
));
|
||||
|
||||
join_set.spawn(ctrl_c_handler());
|
||||
|
||||
join_set.join_next().await.unwrap()??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ctrl_c_handler() -> anyhow::Result<()> {
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to listen for Ctrl-C: {}", e))
|
||||
}
|
||||
|
||||
async fn client_server(socket_fd: OwnedFd) -> anyhow::Result<()> {
|
||||
// SAFETY: see above
|
||||
let std_socket =
|
||||
unsafe { std::os::unix::net::UnixListener::from_raw_fd(socket_fd.as_raw_fd()) };
|
||||
std_socket.set_nonblocking(true)?;
|
||||
let zlink_listener = zlink::unix::Listener::try_from(OwnedFd::from(std_socket))?;
|
||||
let client_server_task = varlink_client_server_task(zlink_listener);
|
||||
|
||||
client_server_task.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
0
src/bin/uquota.rs
Normal file
0
src/bin/uquota.rs
Normal file
2
src/lib.rs
Normal file
2
src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod proto;
|
||||
pub mod server;
|
||||
1
src/proto.rs
Normal file
1
src/proto.rs
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
2
src/server.rs
Normal file
2
src/server.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod config;
|
||||
pub mod varlink_api;
|
||||
13
src/server/config.rs
Normal file
13
src/server/config.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const DEFAULT_CONFIG_PATH: &str = "/etc/donquota/config.toml";
|
||||
|
||||
pub const DEFAULT_CLIENT_SOCKET_PATH: &str = "/run/donquota/server_client.sock";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
/// Path to the Unix domain socket for client-server communication.
|
||||
///
|
||||
/// If left as `None`, the server expects to be served a file descriptor to the socket named 'client'.
|
||||
pub client_socket_path: Option<String>,
|
||||
}
|
||||
86
src/server/varlink_api.rs
Normal file
86
src/server/varlink_api.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use anyhow::Context;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zlink::{ReplyError, service::MethodReply};
|
||||
|
||||
#[zlink::proxy("no.ntnu.pvv.donquota")]
|
||||
pub trait DonQuotaClientProxy {
|
||||
async fn uquota(&mut self, all: bool) -> zlink::Result<Result<(), DonQuotaClientError>>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "method", content = "parameters")]
|
||||
pub enum DonQuotaClientRequest {
|
||||
#[serde(rename = "no.ntnu.pvv.donquota.Uquota")]
|
||||
Uquota,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum DonQuotaClientResponse {
|
||||
Uquota(UquotaResponse),
|
||||
}
|
||||
|
||||
pub type UquotaResponse = ();
|
||||
|
||||
#[derive(Debug, ReplyError)]
|
||||
#[zlink(interface = "no.ntnu.pvv.donquota")]
|
||||
pub enum DonQuotaClientError {
|
||||
InvalidRequest,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DonQuotaClientServer;
|
||||
|
||||
impl Default for DonQuotaClientServer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl DonQuotaClientServer {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl DonQuotaClientServer {
|
||||
async fn handle_uquota_request(&self) -> UquotaResponse {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
impl zlink::Service for DonQuotaClientServer {
|
||||
type MethodCall<'de> = DonQuotaClientRequest;
|
||||
type ReplyParams<'se> = DonQuotaClientResponse;
|
||||
type ReplyStreamParams = ();
|
||||
type ReplyStream = futures_util::stream::Empty<zlink::Reply<()>>;
|
||||
type ReplyError<'se> = DonQuotaClientError;
|
||||
|
||||
async fn handle<'ser, 'de: 'ser, Sock: zlink::connection::Socket>(
|
||||
&'ser mut self,
|
||||
call: zlink::Call<Self::MethodCall<'de>>,
|
||||
_conn: &mut zlink::Connection<Sock>,
|
||||
) -> MethodReply<Self::ReplyParams<'ser>, Self::ReplyStream, Self::ReplyError<'ser>> {
|
||||
match call.method() {
|
||||
DonQuotaClientRequest::Uquota => {
|
||||
let response = self.handle_uquota_request().await;
|
||||
MethodReply::Single(Some(DonQuotaClientResponse::Uquota(response)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn varlink_client_server_task(socket: zlink::unix::Listener) -> anyhow::Result<()> {
|
||||
let service = DonQuotaClientServer::new();
|
||||
|
||||
let server = zlink::Server::new(socket, service);
|
||||
|
||||
tracing::info!("Starting DonQuota client API server");
|
||||
|
||||
server
|
||||
.run()
|
||||
.await
|
||||
.context("DonQuota client API server failed")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user