mirror of
https://github.com/kata-containers/kata-containers.git
synced 2025-08-10 04:18:26 +00:00
Merge pull request #9749 from mkulke/mkulke/configure-guest-components-spawning
CoCo: introduce config for guest-components procs
This commit is contained in:
commit
34d45f0868
@ -126,7 +126,8 @@ The kata agent has the ability to configure agent options in guest kernel comman
|
|||||||
| `agent.debug_console_vport` | Debug console port | Allow to specify the `vsock` port to connect the debugging console | integer | `0` |
|
| `agent.debug_console_vport` | Debug console port | Allow to specify the `vsock` port to connect the debugging console | integer | `0` |
|
||||||
| `agent.devmode` | Developer mode | Allow the agent process to coredump | boolean | `false` |
|
| `agent.devmode` | Developer mode | Allow the agent process to coredump | boolean | `false` |
|
||||||
| `agent.hotplug_timeout` | Hotplug timeout | Allow to configure hotplug timeout(seconds) of block devices | integer | `3` |
|
| `agent.hotplug_timeout` | Hotplug timeout | Allow to configure hotplug timeout(seconds) of block devices | integer | `3` |
|
||||||
| `agent.guest_components_rest_api` | `api-server-rest` configuration | Select the features that the API Server Rest attestation component will run with. Valid values are `all`, `attestation`, `resource`, or `none` to not launch the `api-server-rest` component | string | `resource` |
|
| `agent.guest_components_rest_api` | `api-server-rest` configuration | Select the features that the API Server Rest attestation component will run with. Valid values are `all`, `attestation`, `resource` | string | `resource` |
|
||||||
|
| `agent.guest_components_procs` | guest-components processes | Attestation-related processes that should be spawned as children of the guest. Valid values are `none`, `attestation-agent`, `confidential-data-hub` (implies `attestation-agent`), `api-server-rest` (implies `attestation-agent` and `confidential-data-hub`) | string | `api-server-rest` |
|
||||||
| `agent.https_proxy` | HTTPS proxy | Allow to configure `https_proxy` in the guest | string | `""` |
|
| `agent.https_proxy` | HTTPS proxy | Allow to configure `https_proxy` in the guest | string | `""` |
|
||||||
| `agent.log` | Log level | Allow the agent log level to be changed (produces more or less output) | string | `"info"` |
|
| `agent.log` | Log level | Allow the agent log level to be changed (produces more or less output) | string | `"info"` |
|
||||||
| `agent.log_vport` | Log port | Allow to specify the `vsock` port to read logs | integer | `0` |
|
| `agent.log_vport` | Log port | Allow to specify the `vsock` port to read logs | integer | `0` |
|
||||||
|
@ -28,6 +28,7 @@ const CONTAINER_PIPE_SIZE_OPTION: &str = "agent.container_pipe_size";
|
|||||||
const UNIFIED_CGROUP_HIERARCHY_OPTION: &str = "systemd.unified_cgroup_hierarchy";
|
const UNIFIED_CGROUP_HIERARCHY_OPTION: &str = "systemd.unified_cgroup_hierarchy";
|
||||||
const CONFIG_FILE: &str = "agent.config_file";
|
const CONFIG_FILE: &str = "agent.config_file";
|
||||||
const GUEST_COMPONENTS_REST_API_OPTION: &str = "agent.guest_components_rest_api";
|
const GUEST_COMPONENTS_REST_API_OPTION: &str = "agent.guest_components_rest_api";
|
||||||
|
const GUEST_COMPONENTS_PROCS_OPTION: &str = "agent.guest_components_procs";
|
||||||
|
|
||||||
// Configure the proxy settings for HTTPS requests in the guest,
|
// Configure the proxy settings for HTTPS requests in the guest,
|
||||||
// to solve the problem of not being able to access the specified image in some cases.
|
// to solve the problem of not being able to access the specified image in some cases.
|
||||||
@ -59,7 +60,8 @@ const ERR_INVALID_CONTAINER_PIPE_SIZE_PARAM: &str = "unable to parse container p
|
|||||||
const ERR_INVALID_CONTAINER_PIPE_SIZE_KEY: &str = "invalid container pipe size key name";
|
const ERR_INVALID_CONTAINER_PIPE_SIZE_KEY: &str = "invalid container pipe size key name";
|
||||||
const ERR_INVALID_CONTAINER_PIPE_NEGATIVE: &str = "container pipe size should not be negative";
|
const ERR_INVALID_CONTAINER_PIPE_NEGATIVE: &str = "container pipe size should not be negative";
|
||||||
|
|
||||||
const ERR_INVALID_GUEST_COMPONENTS_REST_API_VALUE: &str = "invalid guest components rest api feature given. Valid values are `all`, `attestation`, `resource`, or `none`";
|
const ERR_INVALID_GUEST_COMPONENTS_REST_API_VALUE: &str = "invalid guest components rest api feature given. Valid values are `all`, `attestation`, `resource`";
|
||||||
|
const ERR_INVALID_GUEST_COMPONENTS_PROCS_VALUE: &str = "invalid guest components process param given. Valid values are `attestation-agent`, `confidential-data-hub`, `api-server-rest`, or `none`";
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Display, Deserialize, EnumString, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, Display, Deserialize, EnumString, PartialEq)]
|
||||||
// Features seem to typically be in kebab-case format, but we only have single words at the moment
|
// Features seem to typically be in kebab-case format, but we only have single words at the moment
|
||||||
@ -67,11 +69,23 @@ const ERR_INVALID_GUEST_COMPONENTS_REST_API_VALUE: &str = "invalid guest compone
|
|||||||
pub enum GuestComponentsFeatures {
|
pub enum GuestComponentsFeatures {
|
||||||
All,
|
All,
|
||||||
Attestation,
|
Attestation,
|
||||||
None,
|
|
||||||
#[default]
|
#[default]
|
||||||
Resource,
|
Resource,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Display, Deserialize, EnumString, PartialEq)]
|
||||||
|
/// Attestation-related processes that we want to spawn as children of the agent
|
||||||
|
#[strum(serialize_all = "kebab-case")]
|
||||||
|
pub enum GuestComponentsProcs {
|
||||||
|
None,
|
||||||
|
/// ApiServerRest implies ConfidentialDataHub and AttestationAgent
|
||||||
|
#[default]
|
||||||
|
ApiServerRest,
|
||||||
|
AttestationAgent,
|
||||||
|
/// ConfidentialDataHub implies AttestationAgent
|
||||||
|
ConfidentialDataHub,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct AgentConfig {
|
pub struct AgentConfig {
|
||||||
pub debug_console: bool,
|
pub debug_console: bool,
|
||||||
@ -89,6 +103,7 @@ pub struct AgentConfig {
|
|||||||
pub https_proxy: String,
|
pub https_proxy: String,
|
||||||
pub no_proxy: String,
|
pub no_proxy: String,
|
||||||
pub guest_components_rest_api: GuestComponentsFeatures,
|
pub guest_components_rest_api: GuestComponentsFeatures,
|
||||||
|
pub guest_components_procs: GuestComponentsProcs,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@ -107,6 +122,7 @@ pub struct AgentConfigBuilder {
|
|||||||
pub https_proxy: Option<String>,
|
pub https_proxy: Option<String>,
|
||||||
pub no_proxy: Option<String>,
|
pub no_proxy: Option<String>,
|
||||||
pub guest_components_rest_api: Option<GuestComponentsFeatures>,
|
pub guest_components_rest_api: Option<GuestComponentsFeatures>,
|
||||||
|
pub guest_components_procs: Option<GuestComponentsProcs>,
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! config_override {
|
macro_rules! config_override {
|
||||||
@ -171,6 +187,7 @@ impl Default for AgentConfig {
|
|||||||
https_proxy: String::from(""),
|
https_proxy: String::from(""),
|
||||||
no_proxy: String::from(""),
|
no_proxy: String::from(""),
|
||||||
guest_components_rest_api: GuestComponentsFeatures::default(),
|
guest_components_rest_api: GuestComponentsFeatures::default(),
|
||||||
|
guest_components_procs: GuestComponentsProcs::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -314,6 +331,12 @@ impl AgentConfig {
|
|||||||
config.guest_components_rest_api,
|
config.guest_components_rest_api,
|
||||||
get_guest_components_features_value
|
get_guest_components_features_value
|
||||||
);
|
);
|
||||||
|
parse_cmdline_param!(
|
||||||
|
param,
|
||||||
|
GUEST_COMPONENTS_PROCS_OPTION,
|
||||||
|
config.guest_components_procs,
|
||||||
|
get_guest_components_procs_value
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(addr) = env::var(SERVER_ADDR_ENV_VAR) {
|
if let Ok(addr) = env::var(SERVER_ADDR_ENV_VAR) {
|
||||||
@ -480,6 +503,19 @@ fn get_guest_components_features_value(param: &str) -> Result<GuestComponentsFea
|
|||||||
.map_err(|_| anyhow!(ERR_INVALID_GUEST_COMPONENTS_REST_API_VALUE))
|
.map_err(|_| anyhow!(ERR_INVALID_GUEST_COMPONENTS_REST_API_VALUE))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
fn get_guest_components_procs_value(param: &str) -> Result<GuestComponentsProcs> {
|
||||||
|
let fields: Vec<&str> = param.split('=').collect();
|
||||||
|
ensure!(fields.len() >= 2, ERR_INVALID_GET_VALUE_PARAM);
|
||||||
|
|
||||||
|
// We need name (but the value can be blank)
|
||||||
|
ensure!(!fields[0].is_empty(), ERR_INVALID_GET_VALUE_NO_NAME);
|
||||||
|
|
||||||
|
let value = fields[1..].join("=");
|
||||||
|
GuestComponentsProcs::from_str(&value)
|
||||||
|
.map_err(|_| anyhow!(ERR_INVALID_GUEST_COMPONENTS_PROCS_VALUE))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use test_utils::assert_result;
|
use test_utils::assert_result;
|
||||||
@ -519,6 +555,7 @@ mod tests {
|
|||||||
https_proxy: &'a str,
|
https_proxy: &'a str,
|
||||||
no_proxy: &'a str,
|
no_proxy: &'a str,
|
||||||
guest_components_rest_api: GuestComponentsFeatures,
|
guest_components_rest_api: GuestComponentsFeatures,
|
||||||
|
guest_components_procs: GuestComponentsProcs,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TestData<'_> {
|
impl Default for TestData<'_> {
|
||||||
@ -537,6 +574,7 @@ mod tests {
|
|||||||
https_proxy: "",
|
https_proxy: "",
|
||||||
no_proxy: "",
|
no_proxy: "",
|
||||||
guest_components_rest_api: GuestComponentsFeatures::default(),
|
guest_components_rest_api: GuestComponentsFeatures::default(),
|
||||||
|
guest_components_procs: GuestComponentsProcs::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -942,8 +980,23 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
TestData {
|
TestData {
|
||||||
contents: "agent.guest_components_rest_api=none",
|
contents: "agent.guest_components_procs=api-server-rest",
|
||||||
guest_components_rest_api: GuestComponentsFeatures::None,
|
guest_components_procs: GuestComponentsProcs::ApiServerRest,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
TestData {
|
||||||
|
contents: "agent.guest_components_procs=confidential-data-hub",
|
||||||
|
guest_components_procs: GuestComponentsProcs::ConfidentialDataHub,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
TestData {
|
||||||
|
contents: "agent.guest_components_procs=attestation-agent",
|
||||||
|
guest_components_procs: GuestComponentsProcs::AttestationAgent,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
TestData {
|
||||||
|
contents: "agent.guest_components_procs=none",
|
||||||
|
guest_components_procs: GuestComponentsProcs::None,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -1000,6 +1053,11 @@ mod tests {
|
|||||||
"{}",
|
"{}",
|
||||||
msg
|
msg
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
d.guest_components_procs, config.guest_components_procs,
|
||||||
|
"{}",
|
||||||
|
msg
|
||||||
|
);
|
||||||
|
|
||||||
for v in vars_to_unset {
|
for v in vars_to_unset {
|
||||||
env::remove_var(v);
|
env::remove_var(v);
|
||||||
@ -1500,10 +1558,6 @@ Caused by:
|
|||||||
param: "x=attestation",
|
param: "x=attestation",
|
||||||
result: Ok(GuestComponentsFeatures::Attestation),
|
result: Ok(GuestComponentsFeatures::Attestation),
|
||||||
},
|
},
|
||||||
TestData {
|
|
||||||
param: "x=none",
|
|
||||||
result: Ok(GuestComponentsFeatures::None),
|
|
||||||
},
|
|
||||||
TestData {
|
TestData {
|
||||||
param: "x=resource",
|
param: "x=resource",
|
||||||
result: Ok(GuestComponentsFeatures::Resource),
|
result: Ok(GuestComponentsFeatures::Resource),
|
||||||
@ -1533,6 +1587,68 @@ Caused by:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_guest_components_procs_value() {
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct TestData<'a> {
|
||||||
|
param: &'a str,
|
||||||
|
result: Result<GuestComponentsProcs>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let tests = &[
|
||||||
|
TestData {
|
||||||
|
param: "",
|
||||||
|
result: Err(anyhow!(ERR_INVALID_GET_VALUE_PARAM)),
|
||||||
|
},
|
||||||
|
TestData {
|
||||||
|
param: "=",
|
||||||
|
result: Err(anyhow!(ERR_INVALID_GET_VALUE_NO_NAME)),
|
||||||
|
},
|
||||||
|
TestData {
|
||||||
|
param: "==",
|
||||||
|
result: Err(anyhow!(ERR_INVALID_GET_VALUE_NO_NAME)),
|
||||||
|
},
|
||||||
|
TestData {
|
||||||
|
param: "x=attestation-agent",
|
||||||
|
result: Ok(GuestComponentsProcs::AttestationAgent),
|
||||||
|
},
|
||||||
|
TestData {
|
||||||
|
param: "x=confidential-data-hub",
|
||||||
|
result: Ok(GuestComponentsProcs::ConfidentialDataHub),
|
||||||
|
},
|
||||||
|
TestData {
|
||||||
|
param: "x=none",
|
||||||
|
result: Ok(GuestComponentsProcs::None),
|
||||||
|
},
|
||||||
|
TestData {
|
||||||
|
param: "x=api-server-rest",
|
||||||
|
result: Ok(GuestComponentsProcs::ApiServerRest),
|
||||||
|
},
|
||||||
|
TestData {
|
||||||
|
param: "x===",
|
||||||
|
result: Err(anyhow!(ERR_INVALID_GUEST_COMPONENTS_PROCS_VALUE)),
|
||||||
|
},
|
||||||
|
TestData {
|
||||||
|
param: "x==x",
|
||||||
|
result: Err(anyhow!(ERR_INVALID_GUEST_COMPONENTS_PROCS_VALUE)),
|
||||||
|
},
|
||||||
|
TestData {
|
||||||
|
param: "x=x",
|
||||||
|
result: Err(anyhow!(ERR_INVALID_GUEST_COMPONENTS_PROCS_VALUE)),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (i, d) in tests.iter().enumerate() {
|
||||||
|
let msg = format!("test[{}]: {:?}", i, d);
|
||||||
|
|
||||||
|
let result = get_guest_components_procs_value(d.param);
|
||||||
|
|
||||||
|
let msg = format!("{}: result: {:?}", msg, result);
|
||||||
|
|
||||||
|
assert_result!(d.result, result, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_config_builder_from_string() {
|
fn test_config_builder_from_string() {
|
||||||
let config = AgentConfig::from_str(
|
let config = AgentConfig::from_str(
|
||||||
|
@ -59,11 +59,11 @@ mod util;
|
|||||||
mod version;
|
mod version;
|
||||||
mod watcher;
|
mod watcher;
|
||||||
|
|
||||||
use config::GuestComponentsFeatures;
|
use config::GuestComponentsProcs;
|
||||||
use mount::{cgroups_mount, general_mount};
|
use mount::{cgroups_mount, general_mount};
|
||||||
use sandbox::Sandbox;
|
use sandbox::Sandbox;
|
||||||
use signal::setup_signal_handler;
|
use signal::setup_signal_handler;
|
||||||
use slog::{error, info, o, warn, Logger};
|
use slog::{debug, error, info, o, warn, Logger};
|
||||||
use uevent::watch_uevents;
|
use uevent::watch_uevents;
|
||||||
|
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
@ -403,8 +403,16 @@ async fn start_sandbox(
|
|||||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||||
sandbox.lock().await.sender = Some(tx);
|
sandbox.lock().await.sender = Some(tx);
|
||||||
|
|
||||||
if Path::new(CDH_PATH).exists() && Path::new(AA_PATH).exists() {
|
let gc_procs = config.guest_components_procs;
|
||||||
init_attestation_components(logger, config)?;
|
if gc_procs != GuestComponentsProcs::None {
|
||||||
|
if !attestation_binaries_available(logger, &gc_procs) {
|
||||||
|
warn!(
|
||||||
|
logger,
|
||||||
|
"attestation binaries requested for launch not available"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
init_attestation_components(logger, config)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// vsock:///dev/vsock, port
|
// vsock:///dev/vsock, port
|
||||||
@ -417,9 +425,33 @@ async fn start_sandbox(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if required attestation binaries are available on the rootfs.
|
||||||
|
fn attestation_binaries_available(logger: &Logger, procs: &GuestComponentsProcs) -> bool {
|
||||||
|
let binaries = match procs {
|
||||||
|
GuestComponentsProcs::AttestationAgent => vec![AA_PATH],
|
||||||
|
GuestComponentsProcs::ConfidentialDataHub => vec![AA_PATH, CDH_PATH],
|
||||||
|
GuestComponentsProcs::ApiServerRest => vec![AA_PATH, CDH_PATH, API_SERVER_PATH],
|
||||||
|
_ => vec![],
|
||||||
|
};
|
||||||
|
for binary in binaries.iter() {
|
||||||
|
if !Path::new(binary).exists() {
|
||||||
|
warn!(logger, "{} not found", binary);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
// Start-up attestation-agent, CDH and api-server-rest if they are packaged in the rootfs
|
// Start-up attestation-agent, CDH and api-server-rest if they are packaged in the rootfs
|
||||||
fn init_attestation_components(logger: &Logger, _config: &AgentConfig) -> Result<()> {
|
// and the corresponding procs are enabled in the agent configuration. the process will be
|
||||||
// The Attestation Agent will run for the duration of the guest.
|
// launched in the background and the function will return immediately.
|
||||||
|
fn init_attestation_components(logger: &Logger, config: &AgentConfig) -> Result<()> {
|
||||||
|
// skip launch of any guest-component
|
||||||
|
if config.guest_components_procs == GuestComponentsProcs::None {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!(logger, "spawning attestation-agent process {}", AA_PATH);
|
||||||
launch_process(
|
launch_process(
|
||||||
logger,
|
logger,
|
||||||
AA_PATH,
|
AA_PATH,
|
||||||
@ -429,32 +461,43 @@ fn init_attestation_components(logger: &Logger, _config: &AgentConfig) -> Result
|
|||||||
)
|
)
|
||||||
.map_err(|e| anyhow!("launch_process {} failed: {:?}", AA_PATH, e))?;
|
.map_err(|e| anyhow!("launch_process {} failed: {:?}", AA_PATH, e))?;
|
||||||
|
|
||||||
if let Err(e) = launch_process(
|
// skip launch of confidential-data-hub and api-server-rest
|
||||||
|
if config.guest_components_procs == GuestComponentsProcs::AttestationAgent {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
logger,
|
||||||
|
"spawning confidential-data-hub process {}", CDH_PATH
|
||||||
|
);
|
||||||
|
launch_process(
|
||||||
logger,
|
logger,
|
||||||
CDH_PATH,
|
CDH_PATH,
|
||||||
&vec![],
|
&vec![],
|
||||||
CDH_SOCKET,
|
CDH_SOCKET,
|
||||||
DEFAULT_LAUNCH_PROCESS_TIMEOUT,
|
DEFAULT_LAUNCH_PROCESS_TIMEOUT,
|
||||||
) {
|
)
|
||||||
error!(logger, "launch_process {} failed: {:?}", CDH_PATH, e);
|
.map_err(|e| anyhow!("launch_process {} failed: {:?}", CDH_PATH, e))?;
|
||||||
} else {
|
|
||||||
let features = _config.guest_components_rest_api;
|
// skip launch of api-server-rest
|
||||||
match features {
|
if config.guest_components_procs == GuestComponentsProcs::ConfidentialDataHub {
|
||||||
GuestComponentsFeatures::None => {}
|
return Ok(());
|
||||||
_ => {
|
|
||||||
if let Err(e) = launch_process(
|
|
||||||
logger,
|
|
||||||
API_SERVER_PATH,
|
|
||||||
&vec!["--features", &features.to_string()],
|
|
||||||
"",
|
|
||||||
0,
|
|
||||||
) {
|
|
||||||
error!(logger, "launch_process {} failed: {:?}", API_SERVER_PATH, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let features = config.guest_components_rest_api;
|
||||||
|
debug!(
|
||||||
|
logger,
|
||||||
|
"spawning api-server-rest process {} --features {}", API_SERVER_PATH, features
|
||||||
|
);
|
||||||
|
launch_process(
|
||||||
|
logger,
|
||||||
|
API_SERVER_PATH,
|
||||||
|
&vec!["--features", &features.to_string()],
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.map_err(|e| anyhow!("launch_process {} failed: {:?}", API_SERVER_PATH, e))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user