use std::path::PathBuf;
use clap::{Args, Parser, Subcommand, ValueEnum};
#[derive(Debug, Parser)]
#[command(
name = "unifly",
version,
about = "Manage UniFi networks from the command line",
long_about = "A powerful CLI for administering UniFi network controllers.\n\n\
Uses the official Integration API (v10.1.84) as primary interface,\n\
with legacy API fallback for features not yet in the official spec.",
propagate_version = true,
subcommand_required = true,
arg_required_else_help = true
)]
pub struct Cli {
#[command(flatten)]
pub global: GlobalOpts,
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Args)]
pub struct GlobalOpts {
#[arg(long, short = 'p', env = "UNIFI_PROFILE", global = true)]
pub profile: Option<String>,
#[arg(long, short = 'c', env = "UNIFI_URL", global = true)]
pub controller: Option<String>,
#[arg(long, short = 's', env = "UNIFI_SITE", global = true)]
pub site: Option<String>,
#[arg(long, env = "UNIFI_API_KEY", global = true, hide_env = true)]
pub api_key: Option<String>,
#[arg(
long,
short = 'o',
env = "UNIFI_OUTPUT",
default_value = "table",
global = true
)]
pub output: OutputFormat,
#[arg(long, default_value = "auto", global = true)]
pub color: ColorMode,
#[arg(long, short = 'v', action = clap::ArgAction::Count, global = true)]
pub verbose: u8,
#[arg(long, short = 'q', global = true)]
pub quiet: bool,
#[arg(long, short = 'y', global = true)]
pub yes: bool,
#[arg(long, short = 'k', env = "UNIFI_INSECURE", global = true)]
pub insecure: bool,
#[arg(long, env = "UNIFI_TIMEOUT", default_value = "30", global = true)]
pub timeout: u64,
}
#[derive(Debug, Clone, ValueEnum)]
pub enum OutputFormat {
Table,
Json,
JsonCompact,
Yaml,
Plain,
}
#[derive(Debug, Clone, ValueEnum)]
pub enum ColorMode {
Auto,
Always,
Never,
}
#[derive(Debug, Subcommand)]
pub enum Command {
Acl(AclArgs),
Admin(AdminArgs),
Alarms(AlarmsArgs),
#[command(alias = "cl")]
Clients(ClientsArgs),
Completions(CompletionsArgs),
Config(ConfigArgs),
Countries,
#[command(alias = "dev", alias = "d")]
Devices(DevicesArgs),
Dns(DnsArgs),
Dpi(DpiArgs),
Events(EventsArgs),
#[command(alias = "fw")]
Firewall(FirewallArgs),
Hotspot(HotspotArgs),
#[command(alias = "net", alias = "n")]
Networks(NetworksArgs),
Radius(RadiusArgs),
Sites(SitesArgs),
Stats(StatsArgs),
#[command(alias = "sys")]
System(SystemArgs),
#[command(alias = "topo")]
Topology,
TrafficLists(TrafficListsArgs),
Vpn(VpnArgs),
Wans(WansArgs),
#[command(alias = "w")]
Wifi(WifiArgs),
#[cfg(feature = "tui")]
Tui(TuiArgs),
}
#[cfg(feature = "tui")]
#[derive(Debug, Args)]
pub struct TuiArgs {
#[arg(long, env = "UNIFLY_THEME")]
pub theme: Option<String>,
#[arg(long, default_value = "/tmp/unifly-tui.log")]
pub log_file: std::path::PathBuf,
}
#[derive(Debug, Args)]
pub struct ListArgs {
#[arg(long, short = 'l', default_value = "25")]
pub limit: u32,
#[arg(long, default_value = "0")]
pub offset: u32,
#[arg(long, short = 'a')]
pub all: bool,
#[arg(long, short = 'f')]
pub filter: Option<String>,
}
#[derive(Debug, Args)]
pub struct DevicesArgs {
#[command(subcommand)]
pub command: DevicesCommand,
}
#[derive(Debug, Subcommand)]
pub enum DevicesCommand {
#[command(alias = "ls")]
List(ListArgs),
Get {
device: String,
},
Adopt {
#[arg(value_name = "MAC")]
mac: String,
#[arg(long)]
ignore_limit: bool,
},
Remove {
device: String,
},
Restart {
device: String,
},
Locate {
device: String,
#[arg(long, default_value = "true", action = clap::ArgAction::Set)]
on: bool,
},
PortCycle {
device: String,
#[arg(value_name = "PORT_IDX")]
port: u32,
},
Stats {
device: String,
},
Pending(ListArgs),
Upgrade {
device: String,
#[arg(long)]
url: Option<String>,
},
Provision {
device: String,
},
Speedtest,
Tags(ListArgs),
}
#[derive(Debug, Args)]
pub struct ClientsArgs {
#[command(subcommand)]
pub command: ClientsCommand,
}
#[derive(Debug, Subcommand)]
pub enum ClientsCommand {
#[command(alias = "ls")]
List(ListArgs),
#[command(alias = "search")]
Find {
query: String,
},
Get {
client: String,
},
Authorize {
client: String,
#[arg(long, required = true)]
minutes: u32,
#[arg(long)]
data_limit_mb: Option<u64>,
#[arg(long)]
rx_limit_kbps: Option<u64>,
#[arg(long)]
tx_limit_kbps: Option<u64>,
},
Unauthorize {
client: String,
},
Block {
mac: String,
},
Unblock {
mac: String,
},
Kick {
mac: String,
},
Forget {
mac: String,
},
#[command(alias = "reserve")]
SetIp {
mac: String,
#[arg(long)]
ip: String,
#[arg(long)]
network: Option<String>,
},
#[command(alias = "unreserve")]
RemoveIp {
mac: String,
},
}
#[derive(Debug, Args)]
pub struct NetworksArgs {
#[command(subcommand)]
pub command: NetworksCommand,
}
#[derive(Debug, Subcommand)]
pub enum NetworksCommand {
#[command(alias = "ls")]
List(ListArgs),
Get {
id: String,
},
Create {
#[arg(long, required_unless_present = "from_file")]
name: Option<String>,
#[arg(long, required_unless_present = "from_file", value_enum)]
management: Option<NetworkManagement>,
#[arg(long, value_parser = clap::value_parser!(u16).range(1..=4009))]
vlan: Option<u16>,
#[arg(long, default_value = "true", action = clap::ArgAction::Set)]
enabled: bool,
#[arg(long)]
ipv4_host: Option<String>,
#[arg(long)]
dhcp: bool,
#[arg(long)]
dhcp_start: Option<String>,
#[arg(long)]
dhcp_stop: Option<String>,
#[arg(long)]
dhcp_lease: Option<u32>,
#[arg(long)]
zone: Option<String>,
#[arg(long)]
isolated: bool,
#[arg(long, default_value = "true", action = clap::ArgAction::Set)]
internet: bool,
#[arg(long, short = 'F', conflicts_with_all = &["name", "management"])]
from_file: Option<PathBuf>,
},
Update {
id: String,
#[arg(long, short = 'F')]
from_file: Option<PathBuf>,
#[arg(long)]
name: Option<String>,
#[arg(long, action = clap::ArgAction::Set)]
enabled: Option<bool>,
#[arg(long, value_parser = clap::value_parser!(u16).range(1..=4009))]
vlan: Option<u16>,
},
Delete {
id: String,
#[arg(long)]
force: bool,
},
Refs {
id: String,
},
}
#[derive(Debug, Clone, ValueEnum)]
pub enum NetworkManagement {
Gateway,
Switch,
Unmanaged,
}
#[derive(Debug, Args)]
pub struct WifiArgs {
#[command(subcommand)]
pub command: WifiCommand,
}
#[derive(Debug, Subcommand)]
pub enum WifiCommand {
#[command(alias = "ls")]
List(ListArgs),
Get {
id: String,
},
Create {
#[arg(long, required_unless_present = "from_file")]
name: Option<String>,
#[arg(long, default_value = "standard", value_enum)]
broadcast_type: WifiBroadcastType,
#[arg(long, required_unless_present = "from_file")]
network: Option<String>,
#[arg(long, default_value = "wpa2-personal", value_enum)]
security: WifiSecurity,
#[arg(long)]
passphrase: Option<String>,
#[arg(long, value_delimiter = ',')]
frequencies: Option<Vec<f32>>,
#[arg(long)]
hidden: bool,
#[arg(long)]
band_steering: bool,
#[arg(long)]
fast_roaming: bool,
#[arg(long, short = 'F', conflicts_with_all = &["name", "network"])]
from_file: Option<PathBuf>,
},
Update {
id: String,
#[arg(long, short = 'F')]
from_file: Option<PathBuf>,
#[arg(long)]
name: Option<String>,
#[arg(long)]
passphrase: Option<String>,
#[arg(long, action = clap::ArgAction::Set)]
enabled: Option<bool>,
},
Delete {
id: String,
#[arg(long)]
force: bool,
},
}
#[derive(Debug, Clone, ValueEnum)]
pub enum WifiBroadcastType {
Standard,
IotOptimized,
}
#[derive(Debug, Clone, ValueEnum)]
pub enum WifiSecurity {
Open,
Wpa2Personal,
Wpa3Personal,
Wpa2Wpa3Personal,
Wpa2Enterprise,
Wpa3Enterprise,
Wpa2Wpa3Enterprise,
}
#[derive(Debug, Args)]
pub struct FirewallArgs {
#[command(subcommand)]
pub command: FirewallCommand,
}
#[derive(Debug, Subcommand)]
#[allow(clippy::large_enum_variant)]
pub enum FirewallCommand {
Policies(FirewallPoliciesArgs),
Zones(FirewallZonesArgs),
}
#[derive(Debug, Args)]
pub struct FirewallPoliciesArgs {
#[command(subcommand)]
pub command: FirewallPoliciesCommand,
}
#[derive(Debug, Subcommand)]
pub enum FirewallPoliciesCommand {
#[command(alias = "ls")]
List(ListArgs),
Get {
id: String,
},
Create {
#[arg(long, required_unless_present = "from_file")]
name: Option<String>,
#[arg(long, required_unless_present = "from_file", value_enum)]
action: Option<FirewallAction>,
#[arg(long, required_unless_present = "from_file")]
source_zone: Option<String>,
#[arg(long, required_unless_present = "from_file")]
dest_zone: Option<String>,
#[arg(long, default_value = "true", action = clap::ArgAction::Set)]
enabled: bool,
#[arg(long)]
description: Option<String>,
#[arg(long)]
logging: bool,
#[arg(long, value_delimiter = ',')]
src_network: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
src_ip: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
src_port: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
dst_network: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
dst_ip: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
dst_port: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
states: Option<Vec<String>>,
#[arg(long)]
ip_version: Option<String>,
#[arg(long, short = 'F', conflicts_with_all = &["name", "action", "source_zone", "dest_zone"])]
from_file: Option<PathBuf>,
},
Update {
id: String,
#[arg(long, value_delimiter = ',')]
src_network: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
src_ip: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
src_port: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
dst_network: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
dst_ip: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
dst_port: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
states: Option<Vec<String>>,
#[arg(long)]
ip_version: Option<String>,
#[arg(long, short = 'F')]
from_file: Option<PathBuf>,
},
Patch {
id: String,
#[arg(long, action = clap::ArgAction::Set)]
enabled: Option<bool>,
#[arg(long, action = clap::ArgAction::Set)]
logging: Option<bool>,
},
Delete {
id: String,
},
Reorder {
#[arg(long, required = true)]
source_zone: String,
#[arg(long, required = true)]
dest_zone: String,
#[arg(long, conflicts_with = "set")]
get: bool,
#[arg(long, value_delimiter = ',')]
set: Option<Vec<String>>,
},
}
#[derive(Debug, Clone, ValueEnum)]
pub enum FirewallAction {
Allow,
Block,
Reject,
}
#[derive(Debug, Args)]
pub struct FirewallZonesArgs {
#[command(subcommand)]
pub command: FirewallZonesCommand,
}
#[derive(Debug, Subcommand)]
pub enum FirewallZonesCommand {
#[command(alias = "ls")]
List(ListArgs),
Get {
id: String,
},
Create {
#[arg(long, required = true)]
name: String,
#[arg(long, value_delimiter = ',')]
networks: Option<Vec<String>>,
},
Update {
id: String,
#[arg(long)]
name: Option<String>,
#[arg(long, value_delimiter = ',')]
networks: Option<Vec<String>>,
},
Delete {
id: String,
},
}
#[derive(Debug, Args)]
pub struct AclArgs {
#[command(subcommand)]
pub command: AclCommand,
}
#[derive(Debug, Subcommand)]
pub enum AclCommand {
#[command(alias = "ls")]
List(ListArgs),
Get {
id: String,
},
Create {
#[arg(long, required_unless_present = "from_file")]
name: Option<String>,
#[arg(long, required_unless_present = "from_file", value_enum)]
rule_type: Option<AclRuleType>,
#[arg(long, required_unless_present = "from_file", value_enum)]
action: Option<AclAction>,
#[arg(long, required_unless_present = "from_file")]
source_zone: Option<String>,
#[arg(long, required_unless_present = "from_file")]
dest_zone: Option<String>,
#[arg(long)]
protocol: Option<String>,
#[arg(long)]
source_port: Option<String>,
#[arg(long)]
destination_port: Option<String>,
#[arg(long, short = 'F', conflicts_with_all = &["name", "rule_type", "source_zone", "dest_zone", "protocol", "source_port", "destination_port"])]
from_file: Option<PathBuf>,
},
Update {
id: String,
#[arg(long, short = 'F')]
from_file: Option<PathBuf>,
},
Delete {
id: String,
},
Reorder {
#[arg(long, conflicts_with = "set")]
get: bool,
#[arg(long, value_delimiter = ',')]
set: Option<Vec<String>>,
},
}
#[derive(Debug, Clone, ValueEnum)]
pub enum AclRuleType {
Ipv4,
Mac,
}
#[derive(Debug, Clone, ValueEnum)]
pub enum AclAction {
Allow,
Block,
}
#[derive(Debug, Args)]
pub struct DnsArgs {
#[command(subcommand)]
pub command: DnsCommand,
}
#[derive(Debug, Subcommand)]
pub enum DnsCommand {
#[command(alias = "ls")]
List(ListArgs),
Get {
id: String,
},
Create {
#[arg(long, required_unless_present = "from_file", value_enum)]
record_type: Option<DnsRecordType>,
#[arg(long, required_unless_present = "from_file")]
domain: Option<String>,
#[arg(long, required_unless_present = "from_file")]
value: Option<String>,
#[arg(long, default_value = "3600", value_parser = clap::value_parser!(u32).range(0..=86400))]
ttl: u32,
#[arg(long)]
priority: Option<u16>,
#[arg(long, short = 'F', conflicts_with_all = &["record_type", "domain"])]
from_file: Option<PathBuf>,
},
Update {
id: String,
#[arg(long, short = 'F')]
from_file: Option<PathBuf>,
},
Delete {
id: String,
},
}
#[derive(Debug, Clone, ValueEnum)]
pub enum DnsRecordType {
A,
Aaaa,
Cname,
Mx,
Txt,
Srv,
Forward,
}
#[derive(Debug, Args)]
pub struct TrafficListsArgs {
#[command(subcommand)]
pub command: TrafficListsCommand,
}
#[derive(Debug, Subcommand)]
pub enum TrafficListsCommand {
#[command(alias = "ls")]
List(ListArgs),
Get {
id: String,
},
Create {
#[arg(long, required_unless_present = "from_file")]
name: Option<String>,
#[arg(long, required_unless_present = "from_file", value_enum)]
list_type: Option<TrafficListType>,
#[arg(long, value_delimiter = ',', required_unless_present = "from_file")]
items: Option<Vec<String>>,
#[arg(long, short = 'F', conflicts_with_all = &["name", "list_type"])]
from_file: Option<PathBuf>,
},
Update {
id: String,
#[arg(long, short = 'F')]
from_file: Option<PathBuf>,
},
Delete {
id: String,
},
}
#[derive(Debug, Clone, ValueEnum)]
pub enum TrafficListType {
Ports,
Ipv4,
Ipv6,
}
#[derive(Debug, Args)]
pub struct HotspotArgs {
#[command(subcommand)]
pub command: HotspotCommand,
}
#[derive(Debug, Subcommand)]
pub enum HotspotCommand {
#[command(alias = "ls")]
List {
#[arg(long, short = 'l', default_value = "100")]
limit: u32,
#[arg(long, default_value = "0")]
offset: u32,
},
Get {
id: String,
},
Create {
#[arg(long, required = true)]
name: String,
#[arg(long, default_value = "1")]
count: u32,
#[arg(long, required = true)]
minutes: u32,
#[arg(long)]
guest_limit: Option<u32>,
#[arg(long)]
data_limit_mb: Option<u64>,
#[arg(long)]
rx_limit_kbps: Option<u64>,
#[arg(long)]
tx_limit_kbps: Option<u64>,
},
Delete {
id: String,
},
Purge {
#[arg(long, required = true)]
filter: String,
},
}
#[derive(Debug, Args)]
pub struct VpnArgs {
#[command(subcommand)]
pub command: VpnCommand,
}
#[derive(Debug, Subcommand)]
pub enum VpnCommand {
Servers(ListArgs),
Tunnels(ListArgs),
}
#[derive(Debug, Args)]
pub struct SitesArgs {
#[command(subcommand)]
pub command: SitesCommand,
}
#[derive(Debug, Subcommand)]
pub enum SitesCommand {
#[command(alias = "ls")]
List(ListArgs),
Create {
#[arg(long, required = true)]
name: String,
#[arg(long, required = true)]
description: String,
},
Delete {
name: String,
},
}
#[derive(Debug, Args)]
pub struct EventsArgs {
#[command(subcommand)]
pub command: EventsCommand,
}
#[derive(Debug, Subcommand)]
pub enum EventsCommand {
#[command(alias = "ls")]
List {
#[arg(long, short = 'l', default_value = "100")]
limit: u32,
#[arg(long, default_value = "24")]
within: u32,
},
Watch {
#[arg(long, value_delimiter = ',')]
types: Option<Vec<String>>,
},
}
#[derive(Debug, Args)]
pub struct AlarmsArgs {
#[command(subcommand)]
pub command: AlarmsCommand,
}
#[derive(Debug, Subcommand)]
pub enum AlarmsCommand {
#[command(alias = "ls")]
List {
#[arg(long)]
unarchived: bool,
#[arg(long, short = 'l', default_value = "100")]
limit: u32,
},
Archive {
id: String,
},
ArchiveAll,
}
#[derive(Debug, Args)]
pub struct StatsArgs {
#[command(subcommand)]
pub command: StatsCommand,
}
#[derive(Debug, Subcommand)]
pub enum StatsCommand {
Site(StatsQuery),
Device(StatsQuery),
Client(StatsQuery),
Gateway(StatsQuery),
Dpi {
#[arg(long, default_value = "by-app", value_enum)]
group_by: DpiGroupBy,
#[arg(long, value_delimiter = ',')]
macs: Option<Vec<String>>,
},
}
#[derive(Debug, Args)]
pub struct StatsQuery {
#[arg(long, default_value = "hourly", value_enum)]
pub interval: StatsInterval,
#[arg(long)]
pub start: Option<String>,
#[arg(long)]
pub end: Option<String>,
#[arg(long, value_delimiter = ',')]
pub attrs: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
pub macs: Option<Vec<String>>,
}
#[derive(Debug, Clone, ValueEnum)]
pub enum StatsInterval {
#[value(name = "5m")]
FiveMinutes,
Hourly,
Daily,
Monthly,
}
#[derive(Debug, Clone, ValueEnum)]
pub enum DpiGroupBy {
ByApp,
ByCat,
}
#[derive(Debug, Args)]
pub struct SystemArgs {
#[command(subcommand)]
pub command: SystemCommand,
}
#[derive(Debug, Subcommand)]
pub enum SystemCommand {
Info,
Health,
Sysinfo,
Backup(BackupArgs),
Reboot,
Poweroff,
}
#[derive(Debug, Args)]
pub struct BackupArgs {
#[command(subcommand)]
pub command: BackupCommand,
}
#[derive(Debug, Subcommand)]
pub enum BackupCommand {
Create,
#[command(alias = "ls")]
List,
Download {
filename: String,
#[arg(long = "path")]
path: Option<PathBuf>,
},
Delete {
filename: String,
},
}
#[derive(Debug, Args)]
pub struct AdminArgs {
#[command(subcommand)]
pub command: AdminCommand,
}
#[derive(Debug, Subcommand)]
pub enum AdminCommand {
#[command(alias = "ls")]
List,
Invite {
#[arg(long, required = true)]
name: String,
#[arg(long, required = true)]
email: String,
#[arg(long, default_value = "admin")]
role: String,
},
Revoke {
admin: String,
},
Update {
admin: String,
#[arg(long, required = true)]
role: String,
},
}
#[derive(Debug, Args)]
pub struct DpiArgs {
#[command(subcommand)]
pub command: DpiCommand,
}
#[derive(Debug, Subcommand)]
pub enum DpiCommand {
Apps(ListArgs),
Categories(ListArgs),
}
#[derive(Debug, Args)]
pub struct RadiusArgs {
#[command(subcommand)]
pub command: RadiusCommand,
}
#[derive(Debug, Subcommand)]
pub enum RadiusCommand {
Profiles(ListArgs),
}
#[derive(Debug, Args)]
pub struct WansArgs {
#[command(subcommand)]
pub command: WansCommand,
}
#[derive(Debug, Subcommand)]
pub enum WansCommand {
#[command(alias = "ls")]
List(ListArgs),
}
#[derive(Debug, Args)]
pub struct ConfigArgs {
#[command(subcommand)]
pub command: ConfigCommand,
}
#[derive(Debug, Subcommand)]
pub enum ConfigCommand {
Init,
Show,
Set {
key: String,
value: String,
},
Profiles,
Use {
name: String,
},
SetPassword {
#[arg(long)]
profile: Option<String>,
},
}
#[derive(Debug, Args)]
pub struct CompletionsArgs {
pub shell: clap_complete::Shell,
}