feat(opnsense-config): add caddy module with configuration management
Introduce a new Caddy module within opnsense-config to manage Caddy server configurations. This includes enabling/disabling Caddy, setting ports, and reloading/restarting the service via OPNsense shell commands. Additionally, provide a sample Caddy configuration file for PXE booting and a test file in the pxe-http-files directory.
This commit is contained in:
parent
925e84e4d2
commit
81d40ec163
@ -9,7 +9,9 @@ use harmony::{
|
||||
infra::opnsense::OPNSenseManagementInterface,
|
||||
inventory::Inventory,
|
||||
maestro::Maestro,
|
||||
modules::{okd::{dhcp::OKDBootstrapDhcpScore, dns::OKDBootstrapDnsScore}, tftp::TftpScore},
|
||||
modules::{
|
||||
http::HttpScore, okd::{dhcp::OKDBootstrapDhcpScore, dns::OKDBootstrapDnsScore}, tftp::TftpScore
|
||||
},
|
||||
topology::{LogicalHost, UnmanagedRouter, Url},
|
||||
};
|
||||
use harmony_macros::ip;
|
||||
@ -24,14 +26,8 @@ async fn main() {
|
||||
};
|
||||
|
||||
let opnsense = Arc::new(
|
||||
harmony::infra::opnsense::OPNSenseFirewall::new(
|
||||
firewall,
|
||||
None,
|
||||
"lan",
|
||||
"root",
|
||||
"opnsense",
|
||||
)
|
||||
.await,
|
||||
harmony::infra::opnsense::OPNSenseFirewall::new(firewall, None, "lan", "root", "opnsense")
|
||||
.await,
|
||||
);
|
||||
let lan_subnet = Ipv4Addr::new(10, 100, 8, 0);
|
||||
let gateway_ipv4 = Ipv4Addr::new(10, 100, 8, 1);
|
||||
@ -45,6 +41,7 @@ async fn main() {
|
||||
load_balancer: opnsense.clone(),
|
||||
firewall: opnsense.clone(),
|
||||
tftp_server: opnsense.clone(),
|
||||
http_server: opnsense.clone(),
|
||||
dhcp_server: opnsense.clone(),
|
||||
dns_server: opnsense.clone(),
|
||||
control_plane: vec![LogicalHost {
|
||||
@ -82,9 +79,13 @@ async fn main() {
|
||||
// harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology);
|
||||
|
||||
let tftp_score = TftpScore::new(Url::LocalFolder("../../../watchguard/tftpboot".to_string()));
|
||||
let http_score = HttpScore::new(Url::LocalFolder(
|
||||
"../../../watchguard/pxe-http-files".to_string(),
|
||||
));
|
||||
let maestro = Maestro::new(inventory, topology);
|
||||
// maestro.interpret(dns_score).await.unwrap();
|
||||
// maestro.interpret(dhcp_score).await.unwrap();
|
||||
// maestro.interpret(load_balancer_score).await.unwrap();
|
||||
maestro.interpret(tftp_score).await.unwrap();
|
||||
// maestro.interpret(tftp_score).await.unwrap();
|
||||
maestro.interpret(http_score).await.unwrap();
|
||||
}
|
||||
|
@ -14,7 +14,8 @@ pub enum InterpretName {
|
||||
OPNSenseDHCP,
|
||||
OPNSenseDns,
|
||||
LoadBalancer,
|
||||
Tftp
|
||||
Tftp,
|
||||
Http,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for InterpretName {
|
||||
@ -24,6 +25,7 @@ impl std::fmt::Display for InterpretName {
|
||||
InterpretName::OPNSenseDns => f.write_str("OPNSenseDns"),
|
||||
InterpretName::LoadBalancer => f.write_str("LoadBalancer"),
|
||||
InterpretName::Tftp => f.write_str("Tftp"),
|
||||
InterpretName::Http => f.write_str("Http"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
24
harmony-rs/harmony/src/domain/topology/http.rs
Normal file
24
harmony-rs/harmony/src/domain/topology/http.rs
Normal file
@ -0,0 +1,24 @@
|
||||
use crate::executors::ExecutorError;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use super::{IpAddress, Url};
|
||||
|
||||
#[async_trait]
|
||||
pub trait HttpServer: Send + Sync {
|
||||
async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError>;
|
||||
fn get_ip(&self) -> IpAddress;
|
||||
|
||||
// async fn set_ip(&self, ip: IpAddress) -> Result<(), ExecutorError>;
|
||||
async fn ensure_initialized(&self) -> Result<(), ExecutorError>;
|
||||
async fn commit_config(&self) -> Result<(), ExecutorError>;
|
||||
async fn reload_restart(&self) -> Result<(), ExecutorError>;
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for dyn HttpServer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!(
|
||||
"HttpServer serving files at {}",
|
||||
self.get_ip()
|
||||
))
|
||||
}
|
||||
}
|
@ -2,12 +2,14 @@ mod host_binding;
|
||||
mod load_balancer;
|
||||
mod router;
|
||||
mod tftp;
|
||||
mod http;
|
||||
pub use load_balancer::*;
|
||||
pub use router::*;
|
||||
mod network;
|
||||
pub use host_binding::*;
|
||||
pub use network::*;
|
||||
pub use tftp::*;
|
||||
pub use http::*;
|
||||
|
||||
use std::{net::IpAddr, sync::Arc};
|
||||
|
||||
@ -19,6 +21,7 @@ pub struct HAClusterTopology {
|
||||
pub firewall: Arc<dyn Firewall>,
|
||||
pub dhcp_server: Arc<dyn DhcpServer>,
|
||||
pub tftp_server: Arc<dyn TftpServer>,
|
||||
pub http_server: Arc<dyn HttpServer>,
|
||||
pub dns_server: Arc<dyn DnsServer>,
|
||||
pub control_plane: Vec<LogicalHost>,
|
||||
pub workers: Vec<LogicalHost>,
|
||||
|
75
harmony-rs/harmony/src/infra/opnsense/http.rs
Normal file
75
harmony-rs/harmony/src/infra/opnsense/http.rs
Normal file
@ -0,0 +1,75 @@
|
||||
use async_trait::async_trait;
|
||||
use log::{debug, info};
|
||||
|
||||
use crate::{
|
||||
executors::ExecutorError,
|
||||
topology::{HttpServer, IpAddress, Url},
|
||||
};
|
||||
|
||||
use super::OPNSenseFirewall;
|
||||
|
||||
#[async_trait]
|
||||
impl HttpServer for OPNSenseFirewall {
|
||||
async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError> {
|
||||
let http_root_path = "/usr/local/http";
|
||||
|
||||
let config = self.opnsense_config.read().await;
|
||||
info!("Uploading files from url {url} to {http_root_path}");
|
||||
match url {
|
||||
Url::LocalFolder(path) => {
|
||||
config
|
||||
.upload_files(path, http_root_path)
|
||||
.await
|
||||
.map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?;
|
||||
}
|
||||
Url::Remote(url) => todo!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_ip(&self) -> IpAddress {
|
||||
todo!();
|
||||
}
|
||||
|
||||
async fn commit_config(&self) -> Result<(), ExecutorError> {
|
||||
OPNSenseFirewall::commit_config(self).await
|
||||
}
|
||||
|
||||
async fn reload_restart(&self) -> Result<(), ExecutorError> {
|
||||
self.opnsense_config
|
||||
.write()
|
||||
.await
|
||||
.caddy()
|
||||
.reload_restart()
|
||||
.await
|
||||
.map_err(|e| ExecutorError::UnexpectedError(e.to_string()))
|
||||
}
|
||||
|
||||
async fn ensure_initialized(&self) -> Result<(), ExecutorError> {
|
||||
let mut config = self.opnsense_config.write().await;
|
||||
let caddy = config.caddy();
|
||||
if let None = caddy.get_full_config() {
|
||||
info!("Http config not available in opnsense config, installing package");
|
||||
config.install_package("os-caddy").await.map_err(|e| {
|
||||
ExecutorError::UnexpectedError(format!(
|
||||
"Executor failed when trying to install os-caddy package with error {e:?}"
|
||||
))
|
||||
})?;
|
||||
} else {
|
||||
info!("Http config available in opnsense config, assuming it is already installed");
|
||||
}
|
||||
info!("Adding custom caddy config files");
|
||||
config
|
||||
.upload_files(
|
||||
"../../../watchguard/caddy_config",
|
||||
"/usr/local/etc/caddy/caddy.d/",
|
||||
)
|
||||
.await
|
||||
.map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?;
|
||||
|
||||
info!("Enabling http server");
|
||||
config.caddy().enable(true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ mod firewall;
|
||||
mod load_balancer;
|
||||
mod management;
|
||||
mod tftp;
|
||||
mod http;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use management::*;
|
||||
|
@ -13,7 +13,7 @@ impl TftpServer for OPNSenseFirewall {
|
||||
async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError> {
|
||||
let tftp_root_path = "/usr/local/tftp";
|
||||
|
||||
let config = self.opnsense_config.write().await;
|
||||
let config = self.opnsense_config.read().await;
|
||||
info!("Uploading files from url {url} to {tftp_root_path}");
|
||||
match url {
|
||||
Url::LocalFolder(path) => {
|
||||
@ -28,7 +28,7 @@ impl TftpServer for OPNSenseFirewall {
|
||||
}
|
||||
|
||||
fn get_ip(&self) -> IpAddress {
|
||||
OPNSenseFirewall::get_ip(self)
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn set_ip(&self, ip: IpAddress) -> Result<(), ExecutorError> {
|
||||
|
64
harmony-rs/harmony/src/modules/http.rs
Normal file
64
harmony-rs/harmony/src/modules/http.rs
Normal file
@ -0,0 +1,64 @@
|
||||
use async_trait::async_trait;
|
||||
use derive_new::new;
|
||||
|
||||
use crate::{
|
||||
data::{Id, Version},
|
||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||
inventory::Inventory,
|
||||
score::Score,
|
||||
topology::{HAClusterTopology, Url},
|
||||
};
|
||||
|
||||
#[derive(Debug, new, Clone)]
|
||||
pub struct HttpScore {
|
||||
files_to_serve: Url,
|
||||
}
|
||||
|
||||
impl Score for HttpScore {
|
||||
type InterpretType = HttpInterpret;
|
||||
|
||||
fn create_interpret(self) -> Self::InterpretType {
|
||||
HttpInterpret::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, new, Clone)]
|
||||
pub struct HttpInterpret {
|
||||
score: HttpScore,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Interpret for HttpInterpret {
|
||||
async fn execute(
|
||||
&self,
|
||||
inventory: &Inventory,
|
||||
topology: &HAClusterTopology,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
let http_server = &topology.http_server;
|
||||
http_server.ensure_initialized().await?;
|
||||
// http_server.set_ip(topology.router.get_gateway()).await?;
|
||||
http_server.serve_files(&self.score.files_to_serve).await?;
|
||||
http_server.commit_config().await?;
|
||||
http_server.reload_restart().await?;
|
||||
Ok(Outcome::success(format!(
|
||||
"Http Server running and serving files from {}",
|
||||
self.score.files_to_serve
|
||||
)))
|
||||
}
|
||||
|
||||
fn get_name(&self) -> InterpretName {
|
||||
InterpretName::Http
|
||||
}
|
||||
|
||||
fn get_version(&self) -> Version {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_status(&self) -> InterpretStatus {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_children(&self) -> Vec<Id> {
|
||||
todo!()
|
||||
}
|
||||
}
|
@ -3,3 +3,4 @@ pub mod dns;
|
||||
pub mod okd;
|
||||
pub mod load_balancer;
|
||||
pub mod tftp;
|
||||
pub mod http;
|
||||
|
83
harmony-rs/opnsense-config-xml/src/data/caddy.rs
Normal file
83
harmony-rs/opnsense-config-xml/src/data/caddy.rs
Normal file
@ -0,0 +1,83 @@
|
||||
use yaserde::MaybeString;
|
||||
use yaserde_derive::{YaDeserialize, YaSerialize};
|
||||
|
||||
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
||||
pub struct Pischem {
|
||||
pub caddy: Caddy,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
||||
pub struct Caddy {
|
||||
pub general: CaddyGeneral,
|
||||
pub reverseproxy: MaybeString,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
||||
pub struct CaddyGeneral {
|
||||
pub enabled: u8,
|
||||
#[yaserde(rename = "EnableLayer4")]
|
||||
pub enable_layer4: Option<u8>,
|
||||
#[yaserde(rename = "HttpPort")]
|
||||
pub http_port: Option<u16>,
|
||||
#[yaserde(rename = "HttpsPort")]
|
||||
pub https_port: Option<u16>,
|
||||
#[yaserde(rename = "TlsEmail")]
|
||||
pub tls_email: MaybeString,
|
||||
#[yaserde(rename = "TlsAutoHttps")]
|
||||
pub tls_auto_https: MaybeString,
|
||||
#[yaserde(rename = "TlsDnsProvider")]
|
||||
pub tls_dns_provider: MaybeString,
|
||||
#[yaserde(rename = "TlsDnsApiKey")]
|
||||
pub tls_dns_api_key: MaybeString,
|
||||
#[yaserde(rename = "TlsDnsSecretApiKey")]
|
||||
pub tls_dns_secret_api_key: MaybeString,
|
||||
#[yaserde(rename = "TlsDnsOptionalField1")]
|
||||
pub tls_dns_optional_field1: MaybeString,
|
||||
#[yaserde(rename = "TlsDnsOptionalField2")]
|
||||
pub tls_dns_optional_field2: MaybeString,
|
||||
#[yaserde(rename = "TlsDnsOptionalField3")]
|
||||
pub tls_dns_optional_field3: MaybeString,
|
||||
#[yaserde(rename = "TlsDnsOptionalField4")]
|
||||
pub tls_dns_optional_field4: MaybeString,
|
||||
#[yaserde(rename = "TlsDnsPropagationTimeout")]
|
||||
pub tls_dns_propagation_timeout: MaybeString,
|
||||
#[yaserde(rename = "TlsDnsPropagationResolvers")]
|
||||
pub tls_dns_propagation_resolvers: MaybeString,
|
||||
pub accesslist: MaybeString,
|
||||
#[yaserde(rename = "DisableSuperuser")]
|
||||
pub disable_superuser: Option<i32>,
|
||||
#[yaserde(rename = "GracePeriod")]
|
||||
pub grace_period: Option<u16>,
|
||||
#[yaserde(rename = "HttpVersion")]
|
||||
pub http_version: MaybeString,
|
||||
#[yaserde(rename = "LogCredentials")]
|
||||
pub log_credentials: MaybeString,
|
||||
#[yaserde(rename = "LogAccessPlain")]
|
||||
pub log_access_plain: MaybeString,
|
||||
#[yaserde(rename = "LogAccessPlainKeep")]
|
||||
pub log_access_plain_keep: Option<u16>,
|
||||
#[yaserde(rename = "LogLevel")]
|
||||
pub log_level: MaybeString,
|
||||
#[yaserde(rename = "DynDnsSimpleHttp")]
|
||||
pub dyn_dns_simple_http: MaybeString,
|
||||
#[yaserde(rename = "DynDnsInterface")]
|
||||
pub dyn_dns_interface: MaybeString,
|
||||
#[yaserde(rename = "DynDnsInterval")]
|
||||
pub dyn_dns_interval: MaybeString,
|
||||
#[yaserde(rename = "DynDnsIpVersions")]
|
||||
pub dyn_dns_ip_versions: MaybeString,
|
||||
#[yaserde(rename = "DynDnsTtl")]
|
||||
pub dyn_dns_ttl: MaybeString,
|
||||
#[yaserde(rename = "DynDnsUpdateOnly")]
|
||||
pub dyn_dns_update_only: MaybeString,
|
||||
#[yaserde(rename = "AuthProvider")]
|
||||
pub auth_provider: MaybeString,
|
||||
#[yaserde(rename = "AuthToDomain")]
|
||||
pub auth_to_domain: MaybeString,
|
||||
#[yaserde(rename = "AuthToPort")]
|
||||
pub auth_to_port: MaybeString,
|
||||
#[yaserde(rename = "AuthToTls")]
|
||||
pub auth_to_tls: Option<i32>,
|
||||
#[yaserde(rename = "AuthToUri")]
|
||||
pub auth_to_uri: MaybeString,
|
||||
}
|
@ -2,6 +2,8 @@ mod opnsense;
|
||||
mod interfaces;
|
||||
mod dhcpd;
|
||||
mod haproxy;
|
||||
mod caddy;
|
||||
pub use caddy::*;
|
||||
pub use haproxy::*;
|
||||
pub use opnsense::*;
|
||||
pub use interfaces::*;
|
||||
|
@ -5,7 +5,7 @@ use uuid::Uuid;
|
||||
use yaserde::{MaybeString, NamedList, RawXml};
|
||||
use yaserde_derive::{YaDeserialize, YaSerialize};
|
||||
|
||||
use super::Interface;
|
||||
use super::{Interface, Pischem};
|
||||
|
||||
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
||||
#[yaserde(rename = "opnsense")]
|
||||
@ -44,7 +44,7 @@ pub struct OPNsense {
|
||||
pub wireless: Wireless,
|
||||
pub hasync: Hasync,
|
||||
#[yaserde(rename = "Pischem")]
|
||||
pub pischem: Option<RawXml>,
|
||||
pub pischem: Option<Pischem>,
|
||||
pub ifgroups: Ifgroups,
|
||||
}
|
||||
|
||||
@ -1370,7 +1370,6 @@ pub struct ConfigOpenVPN {
|
||||
pub StaticKeys: MaybeString,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
|
||||
pub struct StaticRoutes {
|
||||
#[yaserde(attribute)]
|
||||
|
@ -3,7 +3,7 @@ use std::{sync::Arc, time::Duration};
|
||||
use crate::{
|
||||
config::{SshConfigManager, SshCredentials, SshOPNSenseShell},
|
||||
error::Error,
|
||||
modules::{dhcp::DhcpConfig, dns::DnsConfig, load_balancer::LoadBalancerConfig, tftp::TftpConfig},
|
||||
modules::{caddy::CaddyConfig, dhcp::DhcpConfig, dns::DnsConfig, load_balancer::LoadBalancerConfig, tftp::TftpConfig},
|
||||
};
|
||||
use log::{info, trace};
|
||||
use opnsense_config_xml::OPNsense;
|
||||
@ -42,6 +42,10 @@ impl Config {
|
||||
TftpConfig::new(&mut self.opnsense, self.shell.clone())
|
||||
}
|
||||
|
||||
pub fn caddy(&mut self) -> CaddyConfig {
|
||||
CaddyConfig::new(&mut self.opnsense, self.shell.clone())
|
||||
}
|
||||
|
||||
pub fn load_balancer(&mut self) -> LoadBalancerConfig {
|
||||
LoadBalancerConfig::new(&mut self.opnsense, self.shell.clone())
|
||||
}
|
||||
@ -50,6 +54,17 @@ impl Config {
|
||||
self.shell.upload_folder(source, destination).await
|
||||
}
|
||||
|
||||
// Here maybe we should take ownership of `mut self` instead of `&mut self`
|
||||
// I don't think there can be faulty pointers to previous versions of the config but I have a
|
||||
// hard time wrapping my head around it right now :
|
||||
// - the caller has a mutable reference to us
|
||||
// - caller gets a reference to a piece of configuration (.haproxy.general.servers[0])
|
||||
// - caller calls install_package wich reloads the config from remote
|
||||
// - haproxy.general.servers[0] does not exist anymore
|
||||
// - broken?
|
||||
//
|
||||
// Although I did not try explicitely the above workflow so maybe rust prevents taking a
|
||||
// read-only reference across the &mut call
|
||||
pub async fn install_package(&mut self, package_name: &str) -> Result<(), Error> {
|
||||
info!("Installing opnsense package {package_name}");
|
||||
let output = self.shell
|
||||
|
49
harmony-rs/opnsense-config/src/modules/caddy.rs
Normal file
49
harmony-rs/opnsense-config/src/modules/caddy.rs
Normal file
@ -0,0 +1,49 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use opnsense_config_xml::{Caddy, OPNsense, Pischem};
|
||||
|
||||
use crate::{config::OPNsenseShell, Error};
|
||||
|
||||
pub struct CaddyConfig<'a> {
|
||||
opnsense: &'a mut OPNsense,
|
||||
opnsense_shell: Arc<dyn OPNsenseShell>,
|
||||
}
|
||||
|
||||
impl<'a> CaddyConfig<'a> {
|
||||
pub fn new(opnsense: &'a mut OPNsense, opnsense_shell: Arc<dyn OPNsenseShell>) -> Self {
|
||||
Self {
|
||||
opnsense,
|
||||
opnsense_shell,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_full_config(&self) -> &Option<Pischem> {
|
||||
&self.opnsense.pischem
|
||||
}
|
||||
|
||||
fn with_caddy<F, R>(&mut self, f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut Caddy) -> R,
|
||||
{
|
||||
match &mut self.opnsense.pischem.as_mut() {
|
||||
Some(pischem) => f(&mut pischem.caddy),
|
||||
None => unimplemented!("Accessing caddy config is not supported when not available yet"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enable(&mut self, enabled: bool) {
|
||||
self.with_caddy(|caddy| {caddy.general.enabled = enabled as u8;
|
||||
caddy.general.http_port = Some(8080);
|
||||
caddy.general.https_port = Some(8443);
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn reload_restart(&self) -> Result<(), Error> {
|
||||
self.opnsense_shell.exec("configctl caddy stop").await?;
|
||||
self.opnsense_shell.exec("configctl template reload OPNsense/Caddy").await?;
|
||||
self.opnsense_shell.exec("configctl template reload OPNsense/Caddy/rc.conf.d").await?;
|
||||
self.opnsense_shell.exec("configctl caddy validate").await?;
|
||||
self.opnsense_shell.exec("configctl caddy start").await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -2,3 +2,4 @@ pub mod dhcp;
|
||||
pub mod dns;
|
||||
pub mod load_balancer;
|
||||
pub mod tftp;
|
||||
pub mod caddy;
|
||||
|
4
watchguard/caddy_config/caddy_pxe.conf
Normal file
4
watchguard/caddy_config/caddy_pxe.conf
Normal file
@ -0,0 +1,4 @@
|
||||
:8080 {
|
||||
root * /usr/local/http
|
||||
file_server
|
||||
}
|
1
watchguard/pxe-http-files/paul
Normal file
1
watchguard/pxe-http-files/paul
Normal file
@ -0,0 +1 @@
|
||||
hey i am paul
|
Loading…
Reference in New Issue
Block a user