feat: support checking supported and enabled controller features (#41)

* feat: support checking supported and enabled controller features

* linting
This commit is contained in:
Mateusz Filipowicz
2025-03-02 22:22:18 +01:00
committed by GitHub
parent 4e6e9d97b7
commit a5955a6358
7 changed files with 291 additions and 1 deletions

View File

@@ -2,6 +2,7 @@
customizations:
client:
excludeResources:
- "DescribedFeature"
- "Dpi*"
- "FirewallZoneMatrix"
functions:
@@ -296,6 +297,43 @@ customizations:
returns:
- "[]FirewallZoneMatrix"
- "error"
- name: "ListFeatures"
resourceName: "DescribedFeature"
comment: "ListFeatures returns all features of the UniFi controller."
params:
- name: "ctx"
type: "context.Context"
- name: "site"
type: "string"
returns:
- "[]DescribedFeature"
- "error"
- name: "GetFeature"
resourceName: "DescribedFeature"
comment: "GetFeature returns a specific feature by it's name. Name is case-insensitive."
params:
- name: "ctx"
type: "context.Context"
- name: "site"
type: "string"
- name: "name"
type: "string"
returns:
- "*DescribedFeature"
- "error"
- name: "IsFeatureEnabled"
resourceName: "DescribedFeature"
comment: "IsFeatureEnabled returns if a specific feature is enabled by it's name. Name is case-insensitive."
params:
- name: "ctx"
type: "context.Context"
- name: "site"
type: "string"
- name: "name"
type: "string"
returns:
- "bool"
- "error"
resources:
Account:
fields:
@@ -318,6 +356,8 @@ customizations:
customUnmarshalType: "numberOrString"
DNSRecord:
resourcePath: "static-dns"
DescribedFeature:
resourcePath: "described-features?includeSystemFeatures=true" # TODO hack to get all features, because query params in requests are not yet supported
Device:
fields:
_all:

View File

@@ -0,0 +1,4 @@
{
"feature_exists": "true|false",
"name": ""
}

View File

@@ -114,3 +114,34 @@ for _, network := range networks {
fmt.Printf("Network: %s\n", network.Name)
}
```
## Checking if features are supported and enabled
The UniFi Go SDK provides a way to check if a feature is supported and enabled/disabled on the UniFi Controller.
This can be useful when you want to check if a feature is available before using it. Passed feature names are case-insensitive.
**Example:**
```go
if c.IsFeatureEnabled(ctx, "default", "feature-name") {
// Feature is enabled
} else {
// Feature is disabled
}
```
Library comes with a set of predefined feature names, which can be found in `github.com/filipowm/go-unifi/unifi/features` module. You can also use custom feature names.
For example, you can check if the `features.ZoneBasedFirewallMigration` is available on the controller (no `unifi.ErrNotFound` raised) and enabled:
```go
f, err := c.GetFeature(ctx, "default", features.ZoneBasedFirewallMigration)
if err != nil {
if errors.Is(err, unifi.ErrNotFound) {
log.Printf("Feature %s unavailable (not found)", features.ZoneBasedFirewallMigration)
} else {
log.Fatalf("Error getting feature: %v", err)
}
return false
}
return f.FeatureExists // `FeatureExists` is a boolean indicating if the feature is enabled
```

View File

@@ -170,6 +170,15 @@ type Client interface {
// ==== end of client methods for Dashboard resource ====
// GetFeature returns a specific feature by it's name. Name is case-insensitive.
GetFeature(ctx context.Context, site string, name string) (*DescribedFeature, error)
// IsFeatureEnabled returns if a specific feature is enabled by it's name. Name is case-insensitive.
IsFeatureEnabled(ctx context.Context, site string, name string) (bool, error)
// ListFeatures returns all features of the UniFi controller.
ListFeatures(ctx context.Context, site string) ([]DescribedFeature, error)
// ==== client methods for Device resource ====
// AdoptDevice adopts a device by MAC address.

100
unifi/described_feature.generated.go generated Normal file
View File

@@ -0,0 +1,100 @@
// Code generated from ace.jar fields *.json files
// DO NOT EDIT.
package unifi
import (
"context"
"encoding/json"
"fmt"
)
// just to fix compile issues with the import
var (
_ context.Context
_ fmt.Formatter
_ json.Marshaler
)
type DescribedFeature struct {
ID string `json:"_id,omitempty"`
SiteID string `json:"site_id,omitempty"`
Hidden bool `json:"attr_hidden,omitempty"`
HiddenID string `json:"attr_hidden_id,omitempty"`
NoDelete bool `json:"attr_no_delete,omitempty"`
NoEdit bool `json:"attr_no_edit,omitempty"`
FeatureExists bool `json:"feature_exists"`
Name string `json:"name,omitempty"`
}
func (dst *DescribedFeature) UnmarshalJSON(b []byte) error {
type Alias DescribedFeature
aux := &struct {
*Alias
}{
Alias: (*Alias)(dst),
}
err := json.Unmarshal(b, &aux)
if err != nil {
return fmt.Errorf("unable to unmarshal alias: %w", err)
}
return nil
}
func (c *client) listDescribedFeature(ctx context.Context, site string) ([]DescribedFeature, error) {
var respBody []DescribedFeature
err := c.Get(ctx, fmt.Sprintf("%s/site/%s/described-features?includeSystemFeatures=true", c.apiPaths.ApiV2Path, site), nil, &respBody)
if err != nil {
return nil, err
}
return respBody, nil
}
func (c *client) getDescribedFeature(ctx context.Context, site, id string) (*DescribedFeature, error) {
var respBody DescribedFeature
err := c.Get(ctx, fmt.Sprintf("%s/site/%s/described-features?includeSystemFeatures=true/%s", c.apiPaths.ApiV2Path, site, id), nil, &respBody)
if err != nil {
return nil, err
}
if respBody.ID == "" {
return nil, ErrNotFound
}
return &respBody, nil
}
func (c *client) deleteDescribedFeature(ctx context.Context, site, id string) error {
err := c.Delete(ctx, fmt.Sprintf("%s/site/%s/described-features?includeSystemFeatures=true/%s", c.apiPaths.ApiV2Path, site, id), struct{}{}, nil)
if err != nil {
return err
}
return nil
}
func (c *client) createDescribedFeature(ctx context.Context, site string, d *DescribedFeature) (*DescribedFeature, error) {
var respBody DescribedFeature
err := c.Post(ctx, fmt.Sprintf("%s/site/%s/described-features?includeSystemFeatures=true", c.apiPaths.ApiV2Path, site), d, &respBody)
if err != nil {
return nil, err
}
return &respBody, nil
}
func (c *client) updateDescribedFeature(ctx context.Context, site string, d *DescribedFeature) (*DescribedFeature, error) {
var respBody DescribedFeature
err := c.Put(ctx, fmt.Sprintf("%s/site/%s/described-features?includeSystemFeatures=true/%s", c.apiPaths.ApiV2Path, site, d.ID), d, &respBody)
if err != nil {
return nil, err
}
return &respBody, nil
}

View File

@@ -0,0 +1,32 @@
package unifi
import (
"context"
"strings"
)
func (c *client) ListFeatures(ctx context.Context, site string) ([]DescribedFeature, error) {
return c.listDescribedFeature(ctx, site)
}
func (c *client) GetFeature(ctx context.Context, site string, name string) (*DescribedFeature, error) {
features, err := c.ListFeatures(ctx, site)
if err != nil {
return nil, err
}
lowerName := strings.ToLower(name)
for _, f := range features {
if strings.ToLower(f.Name) == lowerName {
return &f, nil
}
}
return nil, ErrNotFound
}
func (c *client) IsFeatureEnabled(ctx context.Context, site string, name string) (bool, error) {
f, err := c.GetFeature(ctx, site, name)
if err != nil {
return false, err
}
return f.FeatureExists, nil
}

74
unifi/features/const.go Normal file
View File

@@ -0,0 +1,74 @@
package features
const (
AdBlocking = "AD_BLOCKING"
AllUnifiDevicesPage = "ALL_UNIFI_DEVICES_PAGE"
CustomDohServers = "CUSTOM_DOH_SERVERS"
Hotspot2Passpoint = "HOTSPOT2_PASSPOINT"
IgmpProxy = "IGMP_PROXY"
IpExclusionFromLeases = "IP_EXCLUSION_FROM_LEASES"
Ips = "IPS"
IpsEtPro = "IPS_ET_PRO"
IpsSignatureReport = "IPS_SIGNATURE_REPORT"
IpsecFqdn = "IPSEC_FQDN"
Ipv4ActiveLeaseReporting = "IPV4_ACTIVE_LEASE_REPORTING"
LegacyUiSupported = "LEGACY_UI_SUPPORTED"
LimitIpsCategories = "LIMIT_IPS_CATEGORIES"
LiveDeviceUpdates = "LIVE_DEVICE_UPDATES"
LockAp = "LOCK_AP"
NatPool = "NAT_POOL"
Netflow = "NETFLOW"
OspfDefaultRouteAnnouncement = "OSPF_DEFAULT_ROUTE_ANNOUNCEMENT"
OspfRouting = "OSPF_ROUTING"
OpenVpnClient = "OPENVPN_CLIENT"
OpenVpnClientTrafficRoutes = "OPENVPN_CLIENT_TRAFFIC_ROUTES"
OpenVpnEncryptionCiphers = "OPENVPN_ENCRYPTION_CIPHERS"
OpenVpnRemoteDisconnect = "OPENVPN_REMOTE_DISCONNECT"
OpenVpnServer = "OPENVPN_SERVER"
RadiusBatchUsers = "RADIUS_BATCH_USERS"
RadiusProfiles = "RADIUS_PROFILES"
RadiusServer = "RADIUS_SERVER"
ScorePage = "SCORE_PAGE"
SdwanHubSpoke = "SDWAN_HUB_SPOKE"
SdwanMesh = "SDWAN_MESH"
SpeedTest = "SPEED_TEST"
StaticDns = "STATIC_DNS"
SwitchBgpRouting = "SWITCH_BGP_ROUTING"
SwitchCustomAclRules = "SWITCH_CUSTOM_ACL_RULES"
SwitchGlobalAclRules = "SWITCH_GLOBAL_ACL_RULES"
Teleport = "TELEPORT"
TrafficMap = "TRAFFIC_MAP"
TrafficRouteKillSwitch = "TRAFFIC_ROUTE_KILL_SWITCH"
TrafficRoutes = "TRAFFIC_ROUTES"
TrafficRoutesIpsecS2sVpn = "TRAFFIC_ROUTES_IPSEC_S2S_VPN"
TrafficRoutesOpenVpnS2sVpn = "TRAFFIC_ROUTES_OPENVPN_S2S_VPN"
TrafficRuleAndRouteRegions = "TRAFFIC_RULE_AND_ROUTE_REGIONS"
TrafficRuleRateLimiting = "TRAFFIC_RULE_RATE_LIMITING"
TrafficRuleSchedules = "TRAFFIC_RULE_SCHEDULES"
UdapiGetBlocks = "UDAPI_GET_BLOCKS"
UcoreAutolinkDeviceUpdates = "UCORE_AUTOLINK_DEVICE_UPDATES"
UcorePartialDeviceUpdates = "UCORE_PARTIAL_DEVICE_UPDATES"
UidRadius = "UID_RADIUS"
UidRadiusGroupPolicy = "UID_RADIUS_GROUP_POLICY"
UidVpn = "UID_VPN"
UidVpnAllowWanLocal = "UID_VPN_ALLOW_WAN_LOCAL"
UidVpnOverrideDns = "UID_VPN_OVERRIDE_DNS"
UidVpnStrictClientCommonName = "UID_VPN_STRICT_CLIENT_COMMON_NAME"
UidVpnSupportUdp = "UID_VPN_SUPPORT_UDP"
UidWifi = "UID_WIFI"
UidWifiIot = "UID_WIFI_IOT"
UidWifiRadiusGroupPolicy = "UID_WIFI_RADIUS_GROUP_POLICY"
UnboundWanMonitor = "UNBOUND_WAN_MONITOR"
UserDefinedNatRules = "USER_DEFINED_NAT_RULES"
VisualProgramming = "VISUAL_PROGRAMMING"
WanDhcpRequestCos = "WAN_DHCP_REQUEST_COS"
WanDhcpv6Stateless = "WAN_DHCPV6_STATELESS"
WanDsLite = "WAN_DS_LITE"
WanLoadBalancingDistributedMode = "WAN_LOAD_BALANCING_DISTRIBUTED_MODE"
WifiConfigCreated = "WIFI_CONFIG_CREATED"
WifiMimo = "WIFI_MIMO"
WireguardVpnClient = "WIREGUARD_VPN_CLIENT"
WireguardVpnServer = "WIREGUARD_VPN_SERVER"
ZoneBasedFirewall = "ZONE_BASED_FIREWALL"
ZoneBasedFirewallMigration = "ZONE_BASED_FIREWALL_MIGRATION"
)