├── .gitattributes ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── acctest.yml │ ├── ci.yaml │ ├── dependabot.yml │ └── release.yml ├── .gitignore ├── .golangci.yaml ├── .goreleaser.yml ├── .vscode └── launch.json ├── LICENSE ├── Makefile ├── README.md ├── docker-compose.yaml ├── docs ├── data-sources │ ├── account.md │ ├── ap_group.md │ ├── dns_record.md │ ├── network.md │ ├── port_profile.md │ ├── radius_profile.md │ ├── user.md │ └── user_group.md ├── guides │ ├── csv-users.md │ └── multiple-site-firewall.md ├── index.md └── resources │ ├── account.md │ ├── device.md │ ├── dns_record.md │ ├── dynamic_dns.md │ ├── firewall_group.md │ ├── firewall_rule.md │ ├── network.md │ ├── port_forward.md │ ├── port_profile.md │ ├── radius_profile.md │ ├── setting_mgmt.md │ ├── setting_radius.md │ ├── setting_usg.md │ ├── site.md │ ├── static_route.md │ ├── user.md │ ├── user_group.md │ └── wlan.md ├── examples ├── csv_users │ ├── users.csv │ └── users.tf ├── data-sources │ ├── unifi_ap_group │ │ └── data-source.tf │ ├── unifi_network │ │ └── data-source.tf │ ├── unifi_port_profile │ │ └── data-source.tf │ └── unifi_user │ │ └── data-source.tf ├── multiple_site_firewall │ └── firewall.tf ├── provider │ ├── provider.tf │ ├── test.auto.tfvars │ ├── test.tf │ └── variables.tf └── resources │ ├── unifi_device │ └── resource.tf │ ├── unifi_dns_record │ └── resource.tf │ ├── unifi_dynamic_dns │ └── resource.tf │ ├── unifi_firewall_group │ ├── resource.tf │ └── test.auto.tfvars │ ├── unifi_firewall_rule │ ├── import.sh │ ├── resource.tf │ └── test.auto.tfvars │ ├── unifi_network │ ├── import.sh │ ├── resource.tf │ └── test.auto.tfvars │ ├── unifi_port_profile │ └── resource.tf │ ├── unifi_setting_mgmt │ └── resource.tf │ ├── unifi_site │ ├── import.sh │ └── resource.tf │ ├── unifi_static_route │ └── resource.tf │ ├── unifi_user │ ├── resource.tf │ ├── test.auto.tfvars │ └── test.tf │ ├── unifi_user_group │ ├── import.sh │ └── resource.tf │ └── unifi_wlan │ ├── import.sh │ └── resource.tf ├── go.mod ├── go.sum ├── internal └── provider │ ├── cidr.go │ ├── cidr_test.go │ ├── controller_versions.go │ ├── controller_versions_test.go │ ├── data_account.go │ ├── data_account_test.go │ ├── data_ap_group.go │ ├── data_ap_group_test.go │ ├── data_dns_record.go │ ├── data_dns_record_test.go │ ├── data_network.go │ ├── data_network_test.go │ ├── data_port_profile.go │ ├── data_port_profile_test.go │ ├── data_radius_profile.go │ ├── data_user.go │ ├── data_user_group.go │ ├── data_user_group_test.go │ ├── data_user_test.go │ ├── importer.go │ ├── lazy_client.go │ ├── mac.go │ ├── markdown.go │ ├── port_range.go │ ├── provider.go │ ├── provider_test.go │ ├── resource_account.go │ ├── resource_account_test.go │ ├── resource_device.go │ ├── resource_device_test.go │ ├── resource_dns_record.go │ ├── resource_dns_record_test.go │ ├── resource_dynamic_dns.go │ ├── resource_dynamic_dns_test.go │ ├── resource_firewall_group.go │ ├── resource_firewall_group_test.go │ ├── resource_firewall_rule.go │ ├── resource_firewall_rule_test.go │ ├── resource_network.go │ ├── resource_network_test.go │ ├── resource_port_forward.go │ ├── resource_port_forward_test.go │ ├── resource_port_profile.go │ ├── resource_port_profile_test.go │ ├── resource_radius_profile.go │ ├── resource_radius_profile_test.go │ ├── resource_setting_mgmt.go │ ├── resource_setting_mgmt_test.go │ ├── resource_setting_radius.go │ ├── resource_setting_radius_test.go │ ├── resource_setting_usg.go │ ├── resource_setting_usg_test.go │ ├── resource_site.go │ ├── resource_site_test.go │ ├── resource_static_route.go │ ├── resource_static_route_test.go │ ├── resource_user.go │ ├── resource_user_group.go │ ├── resource_user_group_test.go │ ├── resource_user_test.go │ ├── resource_wlan.go │ ├── resource_wlan_test.go │ └── strings.go ├── main.go ├── scripts └── init.d │ └── demo-mode ├── templates ├── guides │ ├── csv-users.md.tmpl │ └── multiple-site-firewall.md.tmpl └── index.md.tmpl └── tools └── tools.go /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.go diff=golang 4 | 5 | *.sh eol=lf 6 | 7 | *.jar binary 8 | *.wt binary 9 | *.bson binary 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: paultyng 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | # Check for updates to GitHub Actions every weekday 8 | interval: "daily" 9 | 10 | # Maintain dependencies for Go modules 11 | - package-ecosystem: "gomod" 12 | directory: "/" 13 | schedule: 14 | # Check for updates to Go modules every weekday 15 | interval: "daily" 16 | -------------------------------------------------------------------------------- /.github/workflows/acctest.yml: -------------------------------------------------------------------------------- 1 | name: Acceptance Tests 2 | on: 3 | pull_request: 4 | paths-ignore: 5 | - "README.md" 6 | push: 7 | paths-ignore: 8 | - "README.md" 9 | schedule: 10 | - cron: "0 13 * * *" 11 | jobs: 12 | test: 13 | name: Matrix Test 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 15 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | unifi_version: 20 | - "v6.5" 21 | - "v6" 22 | - "v7.0" 23 | - "v7.1" 24 | - "v7.2" 25 | - "v7.3" 26 | - "v7.4" 27 | - "v7" 28 | - "latest" 29 | steps: 30 | - uses: actions/checkout@v3 31 | - uses: actions/setup-go@v4 32 | with: 33 | go-version-file: "go.mod" 34 | check-latest: true 35 | 36 | # - uses: hoverkraft-tech/compose-action@v0.0.0 37 | # env: 38 | # UNIFI_VERSION: ${{ matrix.unifi_version }} 39 | 40 | # The acceptance tests sometimes timeout for some unknown reason. 41 | - name: TF acceptance tests 42 | uses: "nick-fields/retry@v3" 43 | with: 44 | timeout_minutes: 20 45 | max_attempts: 3 46 | command: make testacc TEST_TIMEOUT=1h UNIFI_STDOUT=true UNIFI_VERSION=${{ matrix.unifi_download_url && 'beta' || matrix.unifi_version }} UNIFI_DOWNLOAD_URL=${{ matrix.unifi_download_url }} 47 | retry_on: "timeout" 48 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | on: 3 | pull_request: {} 4 | push: 5 | branches: 6 | - "main" 7 | tags: 8 | - "v*" 9 | 10 | jobs: 11 | build: 12 | runs-on: "ubuntu-latest" 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-go@v4 16 | with: 17 | go-version-file: "go.mod" 18 | cache: true 19 | check-latest: true 20 | 21 | - run: "go build ./..." 22 | 23 | lint: 24 | runs-on: "ubuntu-latest" 25 | steps: 26 | - uses: actions/checkout@v3 27 | - uses: actions/setup-go@v4 28 | with: 29 | go-version-file: "go.mod" 30 | check-latest: true 31 | 32 | - uses: "golangci/golangci-lint-action@v3.7.1" 33 | with: 34 | skip-pkg-cache: true 35 | -------------------------------------------------------------------------------- /.github/workflows/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Autogenerated by https://github.com/appkins/ecu/github 2 | 3 | name: Dependabot auto-approve 4 | on: 5 | pull_request_target: 6 | types: [opened] 7 | 8 | permissions: 9 | pull-requests: write 10 | contents: write 11 | 12 | jobs: 13 | dependabot: 14 | runs-on: ubuntu-latest 15 | if: ${{ github.actor == 'dependabot[bot]' }} 16 | steps: 17 | - name: Dependabot metadata 18 | id: metadata 19 | uses: dependabot/fetch-metadata@v2.2.0 20 | with: 21 | github-token: "${{ secrets.GITHUB_TOKEN }}" 22 | - name: Approve a PR 23 | if: ${{steps.metadata.outputs.update-type != 'version-update:semver-major'}} 24 | run: gh pr review --approve "$PR_URL" 25 | env: 26 | PR_URL: ${{github.event.pull_request.html_url}} 27 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 28 | - name: Enable auto-merge for Dependabot PRs 29 | run: gh pr merge --auto --merge "$PR_URL" 30 | env: 31 | PR_URL: ${{github.event.pull_request.html_url}} 32 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | workflow_dispatch: 8 | inputs: 9 | tag: 10 | description: "Tag to release" 11 | required: false 12 | default: "v0.41.1" 13 | 14 | # Releases need permissions to read and write the repository contents. 15 | # GitHub considers creating releases and uploading assets as writing contents. 16 | permissions: 17 | contents: write 18 | 19 | jobs: 20 | goreleaser: 21 | runs-on: ubuntu-latest 22 | concurrency: release 23 | steps: 24 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 25 | with: 26 | # Allow goreleaser to access older tag information. 27 | fetch-depth: 0 28 | ref: ${{ inputs.tag || github.ref }} 29 | 30 | - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 31 | with: 32 | go-version-file: "go.mod" 33 | cache: true 34 | - name: Import GPG key 35 | uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0 36 | id: import_gpg 37 | with: 38 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 39 | passphrase: ${{ secrets.PASSPHRASE }} 40 | - name: Run GoReleaser 41 | uses: goreleaser/goreleaser-action@v4.6.0 42 | with: 43 | version: latest 44 | args: release --parallelism 2 --clean 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | 3 | .vscode 4 | dist 5 | terraform-provider-unifi 6 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | linters: 3 | disable-all: true 4 | enable: 5 | - "gofmt" 6 | - "gosimple" 7 | - "govet" 8 | - "ineffassign" 9 | - "makezero" 10 | - "misspell" 11 | - "nakedret" 12 | - "nilerr" 13 | - "staticcheck" 14 | - "structcheck" 15 | - "unconvert" 16 | - "unused" 17 | 18 | run: 19 | timeout: '5m' 20 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | builds: 3 | - env: 4 | - CGO_ENABLED=0 5 | mod_timestamp: "{{ .CommitTimestamp }}" 6 | flags: 7 | - -trimpath 8 | ldflags: 9 | - "-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}" 10 | goos: 11 | - freebsd 12 | - windows 13 | - linux 14 | - darwin 15 | goarch: 16 | - amd64 17 | - "386" 18 | - arm 19 | - arm64 20 | ignore: 21 | - goos: darwin 22 | goarch: "386" 23 | binary: "{{ .ProjectName }}_v{{ .Version }}" 24 | archives: 25 | - format: zip 26 | name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 27 | checksum: 28 | name_template: "{{ .ProjectName }}_{{ .Version }}_SHA256SUMS" 29 | algorithm: sha256 30 | signs: 31 | - artifacts: checksum 32 | args: 33 | - "--batch" 34 | - "--local-user" 35 | - "{{ .Env.GPG_FINGERPRINT }}" 36 | - "--output" 37 | - "${signature}" 38 | - "--detach-sign" 39 | - "${artifact}" 40 | changelog: 41 | disable: true 42 | use: github-native 43 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Acceptance Tests", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "test", 12 | // this assumes your workspace is the root of the repo 13 | "program": "${fileDirname}", 14 | "env": { 15 | "TF_ACC": "1", 16 | }, 17 | "args": [], 18 | }, 19 | // You could pair this configuration with an exec configuration that runs Terraform as 20 | // a compound launch configuration: 21 | // https://code.visualstudio.com/docs/editor/debugging#_compound-launch-configurations 22 | { 23 | "name": "Debug - Attach External CLI", 24 | "type": "go", 25 | "request": "launch", 26 | "mode": "debug", 27 | // this assumes your workspace is the root of the repo 28 | "program": "${workspaceFolder}", 29 | "env": {}, 30 | "args": [ 31 | // pass the debug flag for reattaching 32 | "-debug", 33 | ], 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TEST ?= ./... 2 | TESTARGS ?= 3 | TEST_COUNT ?= 1 4 | TEST_TIMEOUT ?= 10m 5 | 6 | .PHONY: default 7 | default: build 8 | 9 | .PHONY: build 10 | build: 11 | go install 12 | 13 | .PHONY: testacc 14 | testacc: 15 | TF_ACC=1 go test $(TEST) -v -count $(TEST_COUNT) -timeout $(TEST_TIMEOUT) $(TESTARGS) 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Acceptance Tests](https://github.com/ubiquiti-community/terraform-provider-unifi/workflows/Acceptance%20Tests/badge.svg?event=push) 2 | 3 | # Unifi Terraform Provider (terraform-provider-unifi) 4 | 5 | **Note** You can't (for obvious reasons) configure your network while connected to something that may disconnect (like the WiFi). Use a hard-wired connection to your controller to use this provider. 6 | 7 | Functionality first needs to be added to the [go-unifi](https://github.com/ubiquiti-community/go-unifi) SDK. 8 | 9 | ## Documentation 10 | 11 | You can browse documentation on the [Terraform provider registry](https://registry.terraform.io/providers/paultyng/unifi/latest/docs). 12 | 13 | ## Supported Unifi Controller Versions 14 | 15 | As of version [v0.34](https://github.com/ubiquiti-community/terraform-provider-unifi/releases/tag/v0.34.0), this provider only supports version 6 of the Unifi controller software. If you need v5 support, you can pin an older version of the provider. 16 | 17 | The docker, UDM, and UDM-Pro versions are slightly different (the API is proxied a little differently) but for the most part should all be supported. Individual patch versions of the controller are generally not tested for compatibility, just the latest stable versions. 18 | 19 | ## Using the Provider 20 | 21 | ### Terraform 1.0 and above 22 | 23 | You can use the provider via the [Terraform provider registry](https://registry.terraform.io/providers/paultyng/unifi). 24 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | services: 3 | unifi: 4 | image: "jacobalberty/unifi:${UNIFI_VERSION:-latest}" 5 | init: true 6 | restart: "always" 7 | environment: 8 | PKGURL: "${UNIFI_DOWNLOAD_URL:-}" 9 | UNIFI_STDOUT: "true" 10 | ports: 11 | - "${UNIFI_HTTP_PORT:-8080}:8080/tcp" 12 | - "${UNIFI_HTTPS_PORT:-8443}:8443/tcp" 13 | volumes: 14 | - "./scripts/init.d:/usr/local/unifi/init.d:ro" 15 | -------------------------------------------------------------------------------- /docs/data-sources/account.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_account Data Source - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_account data source can be used to retrieve RADIUS user accounts 7 | --- 8 | 9 | # unifi_account (Data Source) 10 | 11 | `unifi_account` data source can be used to retrieve RADIUS user accounts 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `name` (String) The name of the account to look up 21 | 22 | ### Optional 23 | 24 | - `site` (String) The name of the site the account is associated with. 25 | 26 | ### Read-Only 27 | 28 | - `id` (String) The ID of this account. 29 | - `network_id` (String) ID of the network for this account 30 | - `password` (String, Sensitive) The password of the account. 31 | - `tunnel_medium_type` (Number) See RFC2868 section 3.2 32 | - `tunnel_type` (Number) See RFC2868 section 3.1 33 | -------------------------------------------------------------------------------- /docs/data-sources/ap_group.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_ap_group Data Source - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_ap_group data source can be used to retrieve the ID for an AP group by name. 7 | --- 8 | 9 | # unifi_ap_group (Data Source) 10 | 11 | `unifi_ap_group` data source can be used to retrieve the ID for an AP group by name. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | data "unifi_ap_group" "default" { 17 | } 18 | ``` 19 | 20 | 21 | ## Schema 22 | 23 | ### Optional 24 | 25 | - `name` (String) The name of the AP group to look up, leave blank to look up the default AP group. 26 | - `site` (String) The name of the site the AP group is associated with. 27 | 28 | ### Read-Only 29 | 30 | - `id` (String) The ID of this AP group. 31 | -------------------------------------------------------------------------------- /docs/data-sources/dns_record.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_dns_record Data Source - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_dns_record data source can be used to retrieve the ID for an DNS record by name. 7 | --- 8 | 9 | # unifi_dns_record (Data Source) 10 | 11 | `unifi_dns_record` data source can be used to retrieve the ID for an DNS record by name. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Optional 19 | 20 | - `name` (String) The name of the DNS record to look up, leave blank to look up the default DNS record. 21 | - `port` (Number) The port of the DNS record. 22 | - `priority` (Number) The priority of the DNS record. 23 | - `record_type` (String) The type of the DNS record. 24 | - `site` (String) The name of the site the DNS record is associated with. 25 | - `ttl` (Number) The TTL of the DNS record. 26 | - `value` (String) The value of the DNS record. 27 | - `weight` (Number) The weight of the DNS record. 28 | 29 | ### Read-Only 30 | 31 | - `id` (String) The ID of this DNS record. 32 | -------------------------------------------------------------------------------- /docs/data-sources/network.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_network Data Source - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_network data source can be used to retrieve settings for a network by name or ID. 7 | --- 8 | 9 | # unifi_network (Data Source) 10 | 11 | `unifi_network` data source can be used to retrieve settings for a network by name or ID. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | #retrieve network data by unifi network name 17 | data "unifi_network" "lan_network" { 18 | name = "Default" 19 | } 20 | 21 | #retrieve network data from user record 22 | data "unifi_user" "my_device" { 23 | mac = "01:23:45:67:89:ab" 24 | } 25 | data "unifi_network" "my_network" { 26 | id = data.unifi_user.my_device.network_id 27 | } 28 | ``` 29 | 30 | 31 | ## Schema 32 | 33 | ### Optional 34 | 35 | - `id` (String) The ID of the network. 36 | - `name` (String) The name of the network. 37 | - `site` (String) The name of the site to associate the network with. 38 | 39 | ### Read-Only 40 | 41 | - `dhcp_dns` (List of String) IPv4 addresses for the DNS server to be returned from the DHCP server. 42 | - `dhcp_enabled` (Boolean) whether DHCP is enabled or not on this network. 43 | - `dhcp_lease` (Number) lease time for DHCP addresses. 44 | - `dhcp_start` (String) The IPv4 address where the DHCP range of addresses starts. 45 | - `dhcp_stop` (String) The IPv4 address where the DHCP range of addresses stops. 46 | - `dhcp_v6_dns` (List of String) Specifies the IPv6 addresses for the DNS server to be returned from the DHCP server. Used if `dhcp_v6_dns_auto` is set to `false`. 47 | - `dhcp_v6_dns_auto` (Boolean) Specifies DNS source to propagate. If set `false` the entries in `dhcp_v6_dns` are used, the upstream entries otherwise 48 | - `dhcp_v6_enabled` (Boolean) Enable stateful DHCPv6 for static configuration. 49 | - `dhcp_v6_lease` (Number) Specifies the lease time for DHCPv6 addresses. 50 | - `dhcp_v6_start` (String) Start address of the DHCPv6 range. Used in static DHCPv6 configuration. 51 | - `dhcp_v6_stop` (String) End address of the DHCPv6 range. Used in static DHCPv6 configuration. 52 | - `dhcpd_boot_enabled` (Boolean) Toggles on the DHCP boot options. will be set to true if you have dhcpd_boot_filename, and dhcpd_boot_server set. 53 | - `dhcpd_boot_filename` (String) the file to PXE boot from on the dhcpd_boot_server. 54 | - `dhcpd_boot_server` (String) IPv4 address of a TFTP server to network boot from. 55 | - `domain_name` (String) The domain name of this network. 56 | - `igmp_snooping` (Boolean) Specifies whether IGMP snooping is enabled or not. 57 | - `ipv6_interface_type` (String) Specifies which type of IPv6 connection to use. Must be one of either `static`, `pd`, or `none`. 58 | - `ipv6_pd_interface` (String) Specifies which WAN interface to use for IPv6 PD. Must be one of either `wan` or `wan2`. 59 | - `ipv6_pd_prefixid` (String) Specifies the IPv6 Prefix ID. 60 | - `ipv6_pd_start` (String) Start address of the DHCPv6 range. Used if `ipv6_interface_type` is set to `pd`. 61 | - `ipv6_pd_stop` (String) End address of the DHCPv6 range. Used if `ipv6_interface_type` is set to `pd`. 62 | - `ipv6_ra_enable` (Boolean) Specifies whether to enable router advertisements or not. 63 | - `ipv6_ra_preferred_lifetime` (Number) Lifetime in which the address can be used. Address becomes deprecated afterwards. Must be lower than or equal to `ipv6_ra_valid_lifetime` 64 | - `ipv6_ra_priority` (String) IPv6 router advertisement priority. Must be one of either `high`, `medium`, or `low` 65 | - `ipv6_ra_valid_lifetime` (Number) Total lifetime in which the address can be used. Must be equal to or greater than `ipv6_ra_preferred_lifetime`. 66 | - `ipv6_static_subnet` (String) Specifies the static IPv6 subnet (when ipv6_interface_type is 'static'). 67 | - `multicast_dns` (Boolean) Specifies whether Multicast DNS (mDNS) is enabled or not on the network (Controller >=v7). 68 | - `network_group` (String) The group of the network. 69 | - `purpose` (String) The purpose of the network. One of `corporate`, `guest`, `wan`, or `vlan-only`. 70 | - `subnet` (String) The subnet of the network (CIDR address). 71 | - `vlan_id` (Number) The VLAN ID of the network. 72 | - `wan_dhcp_v6_pd_size` (Number) Specifies the IPv6 prefix size to request from ISP. Must be a number between 48 and 64. 73 | - `wan_dns` (List of String) DNS servers IPs of the WAN. 74 | - `wan_egress_qos` (Number) Specifies the WAN egress quality of service. 75 | - `wan_gateway` (String) The IPv4 gateway of the WAN. 76 | - `wan_gateway_v6` (String) The IPv6 gateway of the WAN. 77 | - `wan_ip` (String) The IPv4 address of the WAN. 78 | - `wan_ipv6` (String) The IPv6 address of the WAN. 79 | - `wan_netmask` (String) The IPv4 netmask of the WAN. 80 | - `wan_networkgroup` (String) Specifies the WAN network group. One of either `WAN`, `WAN2` or `WAN_LTE_FAILOVER`. 81 | - `wan_prefixlen` (Number) The IPv6 prefix length of the WAN. Must be between 1 and 128. 82 | - `wan_type` (String) Specifies the IPV4 WAN connection type. One of either `disabled`, `static`, `dhcp`, or `pppoe`. 83 | - `wan_type_v6` (String) Specifies the IPV6 WAN connection type. Must be one of either `disabled`, `static`, or `dhcpv6`. 84 | - `wan_username` (String) Specifies the IPV4 WAN username. 85 | - `x_wan_password` (String) Specifies the IPV4 WAN password. 86 | -------------------------------------------------------------------------------- /docs/data-sources/port_profile.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_port_profile Data Source - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_port_profile data source can be used to retrieve the ID for a port profile by name. 7 | --- 8 | 9 | # unifi_port_profile (Data Source) 10 | 11 | `unifi_port_profile` data source can be used to retrieve the ID for a port profile by name. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | data "unifi_port_profile" "all" { 17 | } 18 | ``` 19 | 20 | 21 | ## Schema 22 | 23 | ### Optional 24 | 25 | - `name` (String) The name of the port profile to look up. Defaults to `All`. 26 | - `site` (String) The name of the site the port profile is associated with. 27 | 28 | ### Read-Only 29 | 30 | - `id` (String) The ID of this port profile. 31 | -------------------------------------------------------------------------------- /docs/data-sources/radius_profile.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_radius_profile Data Source - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_radius_profile data source can be used to retrieve the ID for a RADIUS profile by name. 7 | --- 8 | 9 | # unifi_radius_profile (Data Source) 10 | 11 | `unifi_radius_profile` data source can be used to retrieve the ID for a RADIUS profile by name. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Optional 19 | 20 | - `name` (String) The name of the RADIUS profile to look up. Defaults to `Default`. 21 | - `site` (String) The name of the site the RADIUS profile is associated with. 22 | 23 | ### Read-Only 24 | 25 | - `id` (String) The ID of this AP group. 26 | -------------------------------------------------------------------------------- /docs/data-sources/user.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_user Data Source - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_user retrieves properties of a user (or "client" in the UI) of the network by MAC address. 7 | --- 8 | 9 | # unifi_user (Data Source) 10 | 11 | `unifi_user` retrieves properties of a user (or "client" in the UI) of the network by MAC address. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | data "unifi_user" "client" { 17 | mac = "01:23:45:67:89:ab" 18 | } 19 | ``` 20 | 21 | 22 | ## Schema 23 | 24 | ### Required 25 | 26 | - `mac` (String) The MAC address of the user. 27 | 28 | ### Optional 29 | 30 | - `site` (String) The name of the site the user is associated with. 31 | 32 | ### Read-Only 33 | 34 | - `blocked` (Boolean) Specifies whether this user should be blocked from the network. 35 | - `dev_id_override` (Number) Override the device fingerprint. 36 | - `fixed_ip` (String) fixed IPv4 address set for this user. 37 | - `hostname` (String) The hostname of the user. 38 | - `id` (String) The ID of the user. 39 | - `ip` (String) The IP address of the user. 40 | - `local_dns_record` (String) The local DNS record for this user. 41 | - `name` (String) The name of the user. 42 | - `network_id` (String) The network ID for this user. 43 | - `note` (String) A note with additional information for the user. 44 | - `user_group_id` (String) The user group ID for the user. 45 | -------------------------------------------------------------------------------- /docs/data-sources/user_group.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_user_group Data Source - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_user_group data source can be used to retrieve the ID for a user group by name. 7 | --- 8 | 9 | # unifi_user_group (Data Source) 10 | 11 | `unifi_user_group` data source can be used to retrieve the ID for a user group by name. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Optional 19 | 20 | - `name` (String) The name of the user group to look up. Defaults to `Default`. 21 | - `site` (String) The name of the site the user group is associated with. 22 | 23 | ### Read-Only 24 | 25 | - `id` (String) The ID of this AP group. 26 | - `qos_rate_max_down` (Number) 27 | - `qos_rate_max_up` (Number) 28 | -------------------------------------------------------------------------------- /docs/guides/csv-users.md: -------------------------------------------------------------------------------- 1 | --- 2 | subcategory: "" 3 | page_title: "Manage Users/Clients in a CSV - Unifi Provider" 4 | description: |- 5 | An example of using a CSV to manage all of your users of your network. 6 | --- 7 | 8 | # Manage Users in a CSV 9 | 10 | Given a CSV file with the following content: 11 | 12 | ```csv 13 | mac,name,note 14 | 01:23:45:67:89:AB,My Device,custom note 15 | ``` 16 | 17 | You could create/manage a `unifi_user` for every row/MAC address in the CSV with the following config: 18 | 19 | ```terraform 20 | locals { 21 | userscsv = csvdecode(file("${path.module}/users.csv")) 22 | users = { for user in local.userscsv : user.mac => user } 23 | } 24 | 25 | resource "unifi_user" "user" { 26 | for_each = local.users 27 | 28 | mac = each.key 29 | name = each.value.name 30 | # append an optional additional note 31 | note = trimspace("${each.value.note}\n\nmanaged by TF") 32 | 33 | allow_existing = true 34 | skip_forget_on_destroy = true 35 | } 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/guides/multiple-site-firewall.md: -------------------------------------------------------------------------------- 1 | --- 2 | subcategory: "" 3 | page_title: "Manage Firewall Rules for Multiple Sites - Unifi Provider" 4 | description: |- 5 | An example of applying firewall rules to multiple sites. 6 | --- 7 | 8 | # Manage Firewall Rules for Multiple Sites 9 | 10 | The provider takes a default site value but all resources in the provider should allow overriding of the 11 | site you are managing. In order to apply and manage a firewall rule across multiple sites, you simply 12 | need to provide different values for the `site` attribute to `unifi_firewall_rule`: 13 | 14 | ```terraform 15 | resource "unifi_firewall_rule" "rule" { 16 | # list of sites 17 | for_each = toset(["default", "vq98kwez", "bfa2l6i7"]) 18 | # use the key of the list as the site value 19 | site = each.key 20 | 21 | name = "drop all" 22 | action = "drop" 23 | ruleset = "LAN_IN" 24 | 25 | rule_index = 2011 26 | 27 | protocol = "all" 28 | 29 | dst_address = var.ip_address 30 | } 31 | ``` 32 | 33 | You could optionally load lists of sites from JSON/CSV, variables, or other sources. 34 | 35 | When you apply this configuration it will create the same firewall rule on every site in the list. 36 | If you need to update the rule, you simply make an update to the rule definition and Terraform will 37 | apply/update it across all the sites. If you add / or remove a site from the list, Terraform will also 38 | handle creating or removing the rule on the subsequent `terraform apply`. 39 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "" 3 | page_title: "Provider: Unifi" 4 | description: |- 5 | The Unifi provider provides resources to interact with a Unifi controller API. 6 | --- 7 | 8 | # Unifi Provider 9 | 10 | The Unifi provider provides resources to interact with a Unifi controller API. 11 | 12 | It is not recommended to use your own account for management of your controller. A user specific to 13 | Terraform is recommended. You can create a **Limited Admin** with **Local Access Only** and 14 | provide that information for authentication. Two-factor authentication is not supported in the provider. 15 | 16 | ## Example Usage 17 | 18 | ```terraform 19 | provider "unifi" { 20 | username = var.username # optionally use UNIFI_USERNAME env var 21 | password = var.password # optionally use UNIFI_PASSWORD env var 22 | api_url = var.api_url # optionally use UNIFI_API env var 23 | 24 | # you may need to allow insecure TLS communications unless you have configured 25 | # certificates for your controller 26 | allow_insecure = var.insecure # optionally use UNIFI_INSECURE env var 27 | 28 | # if you are not configuring the default site, you can change the site 29 | # site = "foo" or optionally use UNIFI_SITE env var 30 | } 31 | ``` 32 | 33 | 34 | ## Schema 35 | 36 | ### Optional 37 | 38 | - `allow_insecure` (Boolean) Skip verification of TLS certificates of API requests. You may need to set this to `true` if you are using your local API without setting up a signed certificate. Can be specified with the `UNIFI_INSECURE` environment variable. 39 | - `api_url` (String) URL of the controller API. Can be specified with the `UNIFI_API` environment variable. You should **NOT** supply the path (`/api`), the SDK will discover the appropriate paths. This is to support UDM Pro style API paths as well as more standard controller paths. 40 | - `password` (String) Password for the user accessing the API. Can be specified with the `UNIFI_PASSWORD` environment variable. 41 | - `site` (String) The site in the Unifi controller this provider will manage. Can be specified with the `UNIFI_SITE` environment variable. Default: `default` 42 | - `username` (String) Local user name for the Unifi controller API. Can be specified with the `UNIFI_USERNAME` environment variable. 43 | -------------------------------------------------------------------------------- /docs/resources/account.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_account Resource - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_account manages a RADIUS user account 7 | To authenticate devices based on MAC address, use the MAC address as the username and password under client creation. 8 | Convert lowercase letters to uppercase, and also remove colons or periods from the MAC address. 9 | ATTENTION: If the user profile does not include a VLAN, the client will fall back to the untagged VLAN. 10 | NOTE: MAC-based authentication accounts can only be used for wireless and wired clients. L2TP remote access does not apply. 11 | --- 12 | 13 | # unifi_account (Resource) 14 | 15 | `unifi_account` manages a RADIUS user account 16 | 17 | To authenticate devices based on MAC address, use the MAC address as the username and password under client creation. 18 | Convert lowercase letters to uppercase, and also remove colons or periods from the MAC address. 19 | 20 | ATTENTION: If the user profile does not include a VLAN, the client will fall back to the untagged VLAN. 21 | 22 | NOTE: MAC-based authentication accounts can only be used for wireless and wired clients. L2TP remote access does not apply. 23 | 24 | 25 | 26 | 27 | ## Schema 28 | 29 | ### Required 30 | 31 | - `name` (String) The name of the account. 32 | - `password` (String, Sensitive) The password of the account. 33 | 34 | ### Optional 35 | 36 | - `network_id` (String) ID of the network for this account 37 | - `site` (String) The name of the site to associate the account with. 38 | - `tunnel_medium_type` (Number) See [RFC 2868](https://www.rfc-editor.org/rfc/rfc2868) section 3.2 Defaults to `6`. 39 | - `tunnel_type` (Number) See [RFC 2868](https://www.rfc-editor.org/rfc/rfc2868) section 3.1 Defaults to `13`. 40 | 41 | ### Read-Only 42 | 43 | - `id` (String) The ID of the account. 44 | -------------------------------------------------------------------------------- /docs/resources/device.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_device Resource - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_device manages a device of the network. 7 | Devices are adopted by the controller, so it is not possible for this resource to be created through Terraform, the create operation instead will simply start managing the device specified by MAC address. It's safer to start this process with an explicit import of the device. 8 | --- 9 | 10 | # unifi_device (Resource) 11 | 12 | `unifi_device` manages a device of the network. 13 | 14 | Devices are adopted by the controller, so it is not possible for this resource to be created through Terraform, the create operation instead will simply start managing the device specified by MAC address. It's safer to start this process with an explicit import of the device. 15 | 16 | ## Example Usage 17 | 18 | ```terraform 19 | data "unifi_port_profile" "disabled" { 20 | # look up the built-in disabled port profile 21 | name = "Disabled" 22 | } 23 | 24 | resource "unifi_port_profile" "poe" { 25 | name = "poe" 26 | forward = "customize" 27 | 28 | native_networkconf_id = var.native_network_id 29 | tagged_networkconf_ids = [ 30 | var.some_vlan_network_id, 31 | ] 32 | 33 | poe_mode = "auto" 34 | } 35 | 36 | resource "unifi_device" "us_24_poe" { 37 | # optionally specify MAC address to skip manually importing 38 | # manual import is the safest way to add a device 39 | mac = "01:23:45:67:89:AB" 40 | 41 | name = "Switch with POE" 42 | 43 | port_override { 44 | number = 1 45 | name = "port w/ poe" 46 | port_profile_id = unifi_port_profile.poe.id 47 | } 48 | 49 | port_override { 50 | number = 2 51 | name = "disabled" 52 | port_profile_id = data.unifi_port_profile.disabled.id 53 | } 54 | 55 | # port aggregation for ports 11 and 12 56 | port_override { 57 | number = 11 58 | op_mode = "aggregate" 59 | aggregate_num_ports = 2 60 | } 61 | } 62 | ``` 63 | 64 | 65 | ## Schema 66 | 67 | ### Optional 68 | 69 | - `allow_adoption` (Boolean) Specifies whether this resource should tell the controller to adopt the device on create. Defaults to `true`. 70 | - `forget_on_destroy` (Boolean) Specifies whether this resource should tell the controller to forget the device on destroy. Defaults to `true`. 71 | - `mac` (String) The MAC address of the device. This can be specified so that the provider can take control of a device (since devices are created through adoption). 72 | - `name` (String) The name of the device. 73 | - `port_override` (Block Set) Settings overrides for specific switch ports. (see [below for nested schema](#nestedblock--port_override)) 74 | - `site` (String) The name of the site to associate the device with. 75 | 76 | ### Read-Only 77 | 78 | - `disabled` (Boolean) Specifies whether this device should be disabled. 79 | - `id` (String) The ID of the device. 80 | 81 | 82 | ### Nested Schema for `port_override` 83 | 84 | Required: 85 | 86 | - `number` (Number) Switch port number. 87 | 88 | Optional: 89 | 90 | - `aggregate_num_ports` (Number) Number of ports in the aggregate. 91 | - `name` (String) Human-readable name of the port. 92 | - `op_mode` (String) Operating mode of the port, valid values are `switch`, `mirror`, and `aggregate`. Defaults to `switch`. 93 | - `poe_mode` (String) PoE mode of the port; valid values are `auto`, `pasv24`, `passthrough`, and `off`. 94 | - `port_profile_id` (String) ID of the Port Profile used on this port. 95 | -------------------------------------------------------------------------------- /docs/resources/dns_record.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_dns_record Resource - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_dns_record manages DNS record settings for different providers. 7 | --- 8 | 9 | # unifi_dns_record (Resource) 10 | 11 | `unifi_dns_record` manages DNS record settings for different providers. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "unifi_dns_record" "test" { 17 | name = "my-network" 18 | enabled = true 19 | port = 0 20 | priority = 10 21 | record_type = "A" 22 | ttl = 300 23 | value = "my-network.example.com" 24 | } 25 | ``` 26 | 27 | 28 | ## Schema 29 | 30 | ### Required 31 | 32 | - `name` (String) The key of the DNS record. 33 | - `port` (Number) The port of the DNS record. 34 | - `value` (String) The value of the DNS record. 35 | 36 | ### Optional 37 | 38 | - `enabled` (Boolean) Whether the DNS record is enabled. Defaults to `true`. 39 | - `priority` (Number) The priority of the DNS record. 40 | - `record_type` (String) The type of the DNS record. 41 | - `site` (String) The name of the site to associate the DNS record with. 42 | - `ttl` (Number) The TTL of the DNS record. 43 | - `weight` (Number) The weight of the DNS record. 44 | 45 | ### Read-Only 46 | 47 | - `id` (String) The ID of the DNS record. 48 | -------------------------------------------------------------------------------- /docs/resources/dynamic_dns.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_dynamic_dns Resource - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_dynamic_dns manages dynamic DNS settings for different providers. 7 | --- 8 | 9 | # unifi_dynamic_dns (Resource) 10 | 11 | `unifi_dynamic_dns` manages dynamic DNS settings for different providers. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "unifi_dynamic_dns" "test" { 17 | service = "dyndns" 18 | 19 | host_name = "my-network.example.com" 20 | 21 | server = "domains.google.com" 22 | login = var.dns_login 23 | password = var.dns_password 24 | } 25 | ``` 26 | 27 | 28 | ## Schema 29 | 30 | ### Required 31 | 32 | - `host_name` (String) The host name to update in the dynamic DNS service. 33 | - `service` (String) The Dynamic DNS service provider, various values are supported (for example `dyndns`, etc.). 34 | 35 | ### Optional 36 | 37 | - `interface` (String) The interface for the dynamic DNS. Can be `wan` or `wan2`. Defaults to `wan`. 38 | - `login` (String) The server for the dynamic DNS service. 39 | - `password` (String, Sensitive) The server for the dynamic DNS service. 40 | - `server` (String) The server for the dynamic DNS service. 41 | - `site` (String) The name of the site to associate the dynamic DNS with. 42 | 43 | ### Read-Only 44 | 45 | - `id` (String) The ID of the dynamic DNS. 46 | -------------------------------------------------------------------------------- /docs/resources/firewall_group.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_firewall_group Resource - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_firewall_group manages groups of addresses or ports for use in firewall rules (unifi_firewall_rule). 7 | --- 8 | 9 | # unifi_firewall_group (Resource) 10 | 11 | `unifi_firewall_group` manages groups of addresses or ports for use in firewall rules (`unifi_firewall_rule`). 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | variable "laptop_ips" { 17 | type = list(string) 18 | } 19 | 20 | resource "unifi_firewall_group" "can_print" { 21 | name = "can-print" 22 | type = "address-group" 23 | 24 | members = var.laptop_ips 25 | } 26 | ``` 27 | 28 | 29 | ## Schema 30 | 31 | ### Required 32 | 33 | - `members` (Set of String) The members of the firewall group. 34 | - `name` (String) The name of the firewall group. 35 | - `type` (String) The type of the firewall group. Must be one of: `address-group`, `port-group`, or `ipv6-address-group`. 36 | 37 | ### Optional 38 | 39 | - `site` (String) The name of the site to associate the firewall group with. 40 | 41 | ### Read-Only 42 | 43 | - `id` (String) The ID of the firewall group. 44 | -------------------------------------------------------------------------------- /docs/resources/firewall_rule.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_firewall_rule Resource - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_firewall_rule manages an individual firewall rule on the gateway. 7 | --- 8 | 9 | # unifi_firewall_rule (Resource) 10 | 11 | `unifi_firewall_rule` manages an individual firewall rule on the gateway. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | variable "ip_address" { 17 | type = string 18 | } 19 | 20 | resource "unifi_firewall_rule" "drop_all" { 21 | name = "drop all" 22 | action = "drop" 23 | ruleset = "LAN_IN" 24 | 25 | rule_index = 2011 26 | 27 | protocol = "all" 28 | 29 | dst_address = var.ip_address 30 | } 31 | ``` 32 | 33 | 34 | ## Schema 35 | 36 | ### Required 37 | 38 | - `action` (String) The action of the firewall rule. Must be one of `drop`, `accept`, or `reject`. 39 | - `name` (String) The name of the firewall rule. 40 | - `rule_index` (Number) The index of the rule. Must be >= 2000 < 3000 or >= 4000 < 5000. 41 | - `ruleset` (String) The ruleset for the rule. This is from the perspective of the security gateway. Must be one of `WAN_IN`, `WAN_OUT`, `WAN_LOCAL`, `LAN_IN`, `LAN_OUT`, `LAN_LOCAL`, `GUEST_IN`, `GUEST_OUT`, `GUEST_LOCAL`, `WANv6_IN`, `WANv6_OUT`, `WANv6_LOCAL`, `LANv6_IN`, `LANv6_OUT`, `LANv6_LOCAL`, `GUESTv6_IN`, `GUESTv6_OUT`, or `GUESTv6_LOCAL`. 42 | 43 | ### Optional 44 | 45 | - `dst_address` (String) The destination address of the firewall rule. 46 | - `dst_address_ipv6` (String) The IPv6 destination address of the firewall rule. 47 | - `dst_firewall_group_ids` (Set of String) The destination firewall group IDs of the firewall rule. 48 | - `dst_network_id` (String) The destination network ID of the firewall rule. 49 | - `dst_network_type` (String) The destination network type of the firewall rule. Can be one of `ADDRv4` or `NETv4`. Defaults to `NETv4`. 50 | - `dst_port` (String) The destination port of the firewall rule. 51 | - `enabled` (Boolean) Specifies whether the rule should be enabled. Defaults to `true`. 52 | - `icmp_typename` (String) ICMP type name. 53 | - `icmp_v6_typename` (String) ICMPv6 type name. 54 | - `ip_sec` (String) Specify whether the rule matches on IPsec packets. Can be one of `match-ipset` or `match-none`. 55 | - `logging` (Boolean) Enable logging for the firewall rule. 56 | - `protocol` (String) The protocol of the rule. 57 | - `protocol_v6` (String) The IPv6 protocol of the rule. 58 | - `site` (String) The name of the site to associate the firewall rule with. 59 | - `src_address` (String) The source address for the firewall rule. 60 | - `src_address_ipv6` (String) The IPv6 source address for the firewall rule. 61 | - `src_firewall_group_ids` (Set of String) The source firewall group IDs for the firewall rule. 62 | - `src_mac` (String) The source MAC address of the firewall rule. 63 | - `src_network_id` (String) The source network ID for the firewall rule. 64 | - `src_network_type` (String) The source network type of the firewall rule. Can be one of `ADDRv4` or `NETv4`. Defaults to `NETv4`. 65 | - `src_port` (String) The source port of the firewall rule. 66 | - `state_established` (Boolean) Match where the state is established. 67 | - `state_invalid` (Boolean) Match where the state is invalid. 68 | - `state_new` (Boolean) Match where the state is new. 69 | - `state_related` (Boolean) Match where the state is related. 70 | 71 | ### Read-Only 72 | 73 | - `id` (String) The ID of the firewall rule. 74 | 75 | ## Import 76 | 77 | Import is supported using the following syntax: 78 | 79 | ```shell 80 | # import using the ID from the controller API/UI 81 | terraform import unifi_firewall_rule.my_rule 5f7080eb6b8969064f80494f 82 | ``` 83 | -------------------------------------------------------------------------------- /docs/resources/port_forward.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_port_forward Resource - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_port_forward manages a port forwarding rule on the gateway. 7 | --- 8 | 9 | # unifi_port_forward (Resource) 10 | 11 | `unifi_port_forward` manages a port forwarding rule on the gateway. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Optional 19 | 20 | - `dst_port` (String) The destination port for the forwarding. 21 | - `enabled` (Boolean, Deprecated) Specifies whether the port forwarding rule is enabled or not. Defaults to `true`. This will attribute will be removed in a future release. Instead of disabling a port forwarding rule you can remove it from your configuration. 22 | - `fwd_ip` (String) The IPv4 address to forward traffic to. 23 | - `fwd_port` (String) The port to forward traffic to. 24 | - `log` (Boolean) Specifies whether to log forwarded traffic or not. Defaults to `false`. 25 | - `name` (String) The name of the port forwarding rule. 26 | - `port_forward_interface` (String) The port forwarding interface. Can be `wan`, `wan2`, or `both`. 27 | - `protocol` (String) The protocol for the port forwarding rule. Can be `tcp`, `udp`, or `tcp_udp`. Defaults to `tcp_udp`. 28 | - `site` (String) The name of the site to associate the port forwarding rule with. 29 | - `src_ip` (String) The source IPv4 address (or CIDR) of the port forwarding rule. For all traffic, specify `any`. Defaults to `any`. 30 | 31 | ### Read-Only 32 | 33 | - `id` (String) The ID of the port forwarding rule. 34 | -------------------------------------------------------------------------------- /docs/resources/port_profile.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_port_profile Resource - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_port_profile manages a port profile for use on network switches. 7 | --- 8 | 9 | # unifi_port_profile (Resource) 10 | 11 | `unifi_port_profile` manages a port profile for use on network switches. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | variable "vlan_id" { 17 | default = 10 18 | } 19 | 20 | resource "unifi_network" "vlan" { 21 | name = "wifi-vlan" 22 | purpose = "corporate" 23 | 24 | subnet = "10.0.0.1/24" 25 | vlan_id = var.vlan_id 26 | dhcp_start = "10.0.0.6" 27 | dhcp_stop = "10.0.0.254" 28 | dhcp_enabled = true 29 | } 30 | 31 | resource "unifi_port_profile" "poe_disabled" { 32 | name = "POE Disabled" 33 | 34 | native_networkconf_id = unifi_network.vlan.id 35 | poe_mode = "off" 36 | } 37 | ``` 38 | 39 | 40 | ## Schema 41 | 42 | ### Optional 43 | 44 | - `autoneg` (Boolean) Enable link auto negotiation for the port profile. When set to `true` this overrides `speed`. Defaults to `true`. 45 | - `dot1x_ctrl` (String) The type of 802.1X control to use. Can be `auto`, `force_authorized`, `force_unauthorized`, `mac_based` or `multi_host`. Defaults to `force_authorized`. 46 | - `dot1x_idle_timeout` (Number) The timeout, in seconds, to use when using the MAC Based 802.1X control. Can be between 0 and 65535 Defaults to `300`. 47 | - `egress_rate_limit_kbps` (Number) The egress rate limit, in kpbs, for the port profile. Can be between `64` and `9999999`. 48 | - `egress_rate_limit_kbps_enabled` (Boolean) Enable egress rate limiting for the port profile. Defaults to `false`. 49 | - `forward` (String) The type forwarding to use for the port profile. Can be `all`, `native`, `customize` or `disabled`. Defaults to `native`. 50 | - `full_duplex` (Boolean) Enable full duplex for the port profile. Defaults to `false`. 51 | - `isolation` (Boolean) Enable port isolation for the port profile. Defaults to `false`. 52 | - `lldpmed_enabled` (Boolean) Enable LLDP-MED for the port profile. Defaults to `true`. 53 | - `lldpmed_notify_enabled` (Boolean) Enable LLDP-MED topology change notifications for the port profile. 54 | - `name` (String) The name of the port profile. 55 | - `native_networkconf_id` (String) The ID of network to use as the main network on the port profile. 56 | - `op_mode` (String) The operation mode for the port profile. Can only be `switch` Defaults to `switch`. 57 | - `poe_mode` (String) The POE mode for the port profile. Can be one of `auto`, `passv24`, `passthrough` or `off`. 58 | - `port_security_enabled` (Boolean) Enable port security for the port profile. Defaults to `false`. 59 | - `port_security_mac_address` (Set of String) The MAC addresses associated with the port security for the port profile. 60 | - `priority_queue1_level` (Number) The priority queue 1 level for the port profile. Can be between 0 and 100. 61 | - `priority_queue2_level` (Number) The priority queue 2 level for the port profile. Can be between 0 and 100. 62 | - `priority_queue3_level` (Number) The priority queue 3 level for the port profile. Can be between 0 and 100. 63 | - `priority_queue4_level` (Number) The priority queue 4 level for the port profile. Can be between 0 and 100. 64 | - `site` (String) The name of the site to associate the port profile with. 65 | - `speed` (Number) The link speed to set for the port profile. Can be one of `10`, `100`, `1000`, `2500`, `5000`, `10000`, `20000`, `25000`, `40000`, `50000` or `100000` 66 | - `stormctrl_bcast_enabled` (Boolean) Enable broadcast Storm Control for the port profile. Defaults to `false`. 67 | - `stormctrl_bcast_level` (Number) The broadcast Storm Control level for the port profile. Can be between 0 and 100. 68 | - `stormctrl_bcast_rate` (Number) The broadcast Storm Control rate for the port profile. Can be between 0 and 14880000. 69 | - `stormctrl_mcast_enabled` (Boolean) Enable multicast Storm Control for the port profile. Defaults to `false`. 70 | - `stormctrl_mcast_level` (Number) The multicast Storm Control level for the port profile. Can be between 0 and 100. 71 | - `stormctrl_mcast_rate` (Number) The multicast Storm Control rate for the port profile. Can be between 0 and 14880000. 72 | - `stormctrl_type` (String) The type of Storm Control to use for the port profile. Can be one of `level` or `rate`. 73 | - `stormctrl_ucast_enabled` (Boolean) Enable unknown unicast Storm Control for the port profile. Defaults to `false`. 74 | - `stormctrl_ucast_level` (Number) The unknown unicast Storm Control level for the port profile. Can be between 0 and 100. 75 | - `stormctrl_ucast_rate` (Number) The unknown unicast Storm Control rate for the port profile. Can be between 0 and 14880000. 76 | - `stp_port_mode` (Boolean) Enable spanning tree protocol on the port profile. Defaults to `true`. 77 | - `tagged_vlan_mgmt` (String) The IDs of networks to tag traffic with for the port profile. 78 | - `voice_networkconf_id` (String) The ID of network to use as the voice network on the port profile. 79 | 80 | ### Read-Only 81 | 82 | - `id` (String) The ID of the port profile. 83 | -------------------------------------------------------------------------------- /docs/resources/radius_profile.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_radius_profile Resource - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_radius_profile manages RADIUS profiles. 7 | --- 8 | 9 | # unifi_radius_profile (Resource) 10 | 11 | `unifi_radius_profile` manages RADIUS profiles. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `name` (String) The name of the profile. 21 | 22 | ### Optional 23 | 24 | - `accounting_enabled` (Boolean) Specifies whether to use RADIUS accounting. Defaults to `false`. 25 | - `acct_server` (Block List) RADIUS accounting servers. (see [below for nested schema](#nestedblock--acct_server)) 26 | - `auth_server` (Block List) RADIUS authentication servers. (see [below for nested schema](#nestedblock--auth_server)) 27 | - `interim_update_enabled` (Boolean) Specifies whether to use interim_update. Defaults to `false`. 28 | - `interim_update_interval` (Number) Specifies interim_update interval. Defaults to `3600`. 29 | - `site` (String) The name of the site to associate the settings with. 30 | - `use_usg_acct_server` (Boolean) Specifies whether to use usg as a RADIUS accounting server. Defaults to `false`. 31 | - `use_usg_auth_server` (Boolean) Specifies whether to use usg as a RADIUS authentication server. Defaults to `false`. 32 | - `vlan_enabled` (Boolean) Specifies whether to use vlan on wired connections. Defaults to `false`. 33 | - `vlan_wlan_mode` (String) Specifies whether to use vlan on wireless connections. Must be one of `disabled`, `optional`, or `required`. Defaults to ``. 34 | 35 | ### Read-Only 36 | 37 | - `id` (String) The ID of the settings. 38 | 39 | 40 | ### Nested Schema for `acct_server` 41 | 42 | Required: 43 | 44 | - `ip` (String) IP address of accounting service server. 45 | - `xsecret` (String, Sensitive) RADIUS secret. 46 | 47 | Optional: 48 | 49 | - `port` (Number) Port of accounting service. Defaults to `1813`. 50 | 51 | 52 | 53 | ### Nested Schema for `auth_server` 54 | 55 | Required: 56 | 57 | - `ip` (String) IP address of authentication service server. 58 | - `xsecret` (String, Sensitive) RADIUS secret. 59 | 60 | Optional: 61 | 62 | - `port` (Number) Port of authentication service. Defaults to `1812`. 63 | -------------------------------------------------------------------------------- /docs/resources/setting_mgmt.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_setting_mgmt Resource - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_setting_mgmt manages settings for a unifi site. 7 | --- 8 | 9 | # unifi_setting_mgmt (Resource) 10 | 11 | `unifi_setting_mgmt` manages settings for a unifi site. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "unifi_site" "example" { 17 | description = "example" 18 | } 19 | 20 | resource "unifi_setting_mgmt" "example" { 21 | site = unifi_site.example.name 22 | auto_upgrade = true 23 | } 24 | ``` 25 | 26 | 27 | ## Schema 28 | 29 | ### Optional 30 | 31 | - `auto_upgrade` (Boolean) Automatically upgrade device firmware. 32 | - `site` (String) The name of the site to associate the settings with. 33 | - `ssh_enabled` (Boolean) Enable SSH authentication. 34 | - `ssh_key` (Block Set) SSH key. (see [below for nested schema](#nestedblock--ssh_key)) 35 | 36 | ### Read-Only 37 | 38 | - `id` (String) The ID of the settings. 39 | 40 | 41 | ### Nested Schema for `ssh_key` 42 | 43 | Required: 44 | 45 | - `name` (String) Name of SSH key. 46 | - `type` (String) Type of SSH key, e.g. ssh-rsa. 47 | 48 | Optional: 49 | 50 | - `comment` (String) Comment. 51 | - `key` (String) Public SSH key. 52 | -------------------------------------------------------------------------------- /docs/resources/setting_radius.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_setting_radius Resource - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_setting_radius manages settings for the built-in RADIUS server. 7 | --- 8 | 9 | # unifi_setting_radius (Resource) 10 | 11 | `unifi_setting_radius` manages settings for the built-in RADIUS server. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Optional 19 | 20 | - `accounting_enabled` (Boolean) Enable RADIUS accounting Defaults to `false`. 21 | - `accounting_port` (Number) The port for accounting communications. Defaults to `1813`. 22 | - `auth_port` (Number) The port for authentication communications. Defaults to `1812`. 23 | - `enabled` (Boolean) RAIDUS server enabled. Defaults to `true`. 24 | - `interim_update_interval` (Number) Statistics will be collected from connected clients at this interval. Defaults to `3600`. 25 | - `secret` (String, Sensitive) RAIDUS secret passphrase. Defaults to ``. 26 | - `site` (String) The name of the site to associate the settings with. 27 | - `tunneled_reply` (Boolean) Encrypt communication between the server and the client. Defaults to `true`. 28 | 29 | ### Read-Only 30 | 31 | - `id` (String) The ID of the settings. 32 | -------------------------------------------------------------------------------- /docs/resources/setting_usg.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_setting_usg Resource - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_setting_usg manages settings for a Unifi Security Gateway. 7 | --- 8 | 9 | # unifi_setting_usg (Resource) 10 | 11 | `unifi_setting_usg` manages settings for a Unifi Security Gateway. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Optional 19 | 20 | - `dhcp_relay_servers` (List of String) The DHCP relay servers. 21 | - `firewall_guest_default_log` (Boolean) Whether the guest firewall log is enabled. 22 | - `firewall_lan_default_log` (Boolean) Whether the LAN firewall log is enabled. 23 | - `firewall_wan_default_log` (Boolean) Whether the WAN firewall log is enabled. 24 | - `multicast_dns_enabled` (Boolean) Whether multicast DNS is enabled. 25 | - `site` (String) The name of the site to associate the settings with. 26 | 27 | ### Read-Only 28 | 29 | - `id` (String) The ID of the settings. 30 | -------------------------------------------------------------------------------- /docs/resources/site.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_site Resource - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_site manages Unifi sites 7 | --- 8 | 9 | # unifi_site (Resource) 10 | 11 | `unifi_site` manages Unifi sites 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "unifi_site" "mysite" { 17 | description = "mysite" 18 | } 19 | ``` 20 | 21 | 22 | ## Schema 23 | 24 | ### Required 25 | 26 | - `description` (String) The description of the site. 27 | 28 | ### Read-Only 29 | 30 | - `id` (String) The ID of the site. 31 | - `name` (String) The name of the site. 32 | 33 | ## Import 34 | 35 | Import is supported using the following syntax: 36 | 37 | ```shell 38 | # import using the API/UI ID 39 | terraform import unifi_site.mysite 5fe6261995fe130013456a36 40 | 41 | # import using the name (short ID) 42 | terraform import unifi_site.mysite vq98kwez 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/resources/static_route.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_static_route Resource - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_static_route manages a static route. 7 | --- 8 | 9 | # unifi_static_route (Resource) 10 | 11 | `unifi_static_route` manages a static route. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "unifi_static_route" "nexthop" { 17 | type = "nexthop-route" 18 | network = "172.17.0.0/16" 19 | name = "basic nexthop" 20 | distance = 1 21 | next_hop = "172.16.0.1" 22 | } 23 | 24 | resource "unifi_static_route" "blackhole" { 25 | type = "blackhole" 26 | network = var.blackhole_cidr 27 | name = "blackhole traffice to cidr" 28 | distance = 1 29 | } 30 | 31 | resource "unifi_static_route" "interface" { 32 | type = "interface-route" 33 | network = var.wan2_cidr 34 | name = "send traffic over wan2" 35 | distance = 1 36 | interface = "WAN2" 37 | } 38 | ``` 39 | 40 | 41 | ## Schema 42 | 43 | ### Required 44 | 45 | - `distance` (Number) The distance of the static route. 46 | - `name` (String) The name of the static route. 47 | - `network` (String) The network subnet address. 48 | - `type` (String) The type of static route. Can be `interface-route`, `nexthop-route`, or `blackhole`. 49 | 50 | ### Optional 51 | 52 | - `interface` (String) The interface of the static route (only valid for `interface-route` type). This can be `WAN1`, `WAN2`, or a network ID. 53 | - `next_hop` (String) The next hop of the static route (only valid for `nexthop-route` type). 54 | - `site` (String) The name of the site to associate the static route with. 55 | 56 | ### Read-Only 57 | 58 | - `id` (String) The ID of the static route. 59 | -------------------------------------------------------------------------------- /docs/resources/user.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_user Resource - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_user manages a user (or "client" in the UI) of the network, these are identified by unique MAC addresses. 7 | Users are created in the controller when observed on the network, so the resource defaults to allowing itself to just take over management of a MAC address, but this can be turned off. 8 | --- 9 | 10 | # unifi_user (Resource) 11 | 12 | `unifi_user` manages a user (or "client" in the UI) of the network, these are identified by unique MAC addresses. 13 | 14 | Users are created in the controller when observed on the network, so the resource defaults to allowing itself to just take over management of a MAC address, but this can be turned off. 15 | 16 | ## Example Usage 17 | 18 | ```terraform 19 | resource "unifi_user" "test" { 20 | mac = "01:23:45:67:89:AB" 21 | name = "some client" 22 | note = "my note" 23 | 24 | fixed_ip = "10.0.0.50" 25 | network_id = unifi_network.my_vlan.id 26 | } 27 | ``` 28 | 29 | 30 | ## Schema 31 | 32 | ### Required 33 | 34 | - `mac` (String) The MAC address of the user. 35 | - `name` (String) The name of the user. 36 | 37 | ### Optional 38 | 39 | - `allow_existing` (Boolean) Specifies whether this resource should just take over control of an existing user. Defaults to `true`. 40 | - `blocked` (Boolean) Specifies whether this user should be blocked from the network. 41 | - `dev_id_override` (Number) Override the device fingerprint. 42 | - `fixed_ip` (String) A fixed IPv4 address for this user. 43 | - `local_dns_record` (String) Specifies the local DNS record for this user. 44 | - `network_id` (String) The network ID for this user. 45 | - `note` (String) A note with additional information for the user. 46 | - `site` (String) The name of the site to associate the user with. 47 | - `skip_forget_on_destroy` (Boolean) Specifies whether this resource should tell the controller to "forget" the user on destroy. Defaults to `false`. 48 | - `user_group_id` (String) The user group ID for the user. 49 | 50 | ### Read-Only 51 | 52 | - `hostname` (String) The hostname of the user. 53 | - `id` (String) The ID of the user. 54 | - `ip` (String) The IP address of the user. 55 | -------------------------------------------------------------------------------- /docs/resources/user_group.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_user_group Resource - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_user_group manages a user group (called "client group" in the UI), which can be used to limit bandwidth for groups of users. 7 | --- 8 | 9 | # unifi_user_group (Resource) 10 | 11 | `unifi_user_group` manages a user group (called "client group" in the UI), which can be used to limit bandwidth for groups of users. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "unifi_user_group" "wifi" { 17 | name = "wifi" 18 | 19 | qos_rate_max_down = 2000 # 2mbps 20 | qos_rate_max_up = 10 # 10kbps 21 | } 22 | ``` 23 | 24 | 25 | ## Schema 26 | 27 | ### Required 28 | 29 | - `name` (String) The name of the user group. 30 | 31 | ### Optional 32 | 33 | - `qos_rate_max_down` (Number) The QOS maximum download rate. Defaults to `-1`. 34 | - `qos_rate_max_up` (Number) The QOS maximum upload rate. Defaults to `-1`. 35 | - `site` (String) The name of the site to associate the user group with. 36 | 37 | ### Read-Only 38 | 39 | - `id` (String) The ID of the user group. 40 | 41 | ## Import 42 | 43 | Import is supported using the following syntax: 44 | 45 | ```shell 46 | # import using the ID 47 | terraform import unifi_user_group.wifi 5fe6261995fe130013456a36 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/resources/wlan.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "unifi_wlan Resource - terraform-provider-unifi" 4 | subcategory: "" 5 | description: |- 6 | unifi_wlan manages a WiFi network / SSID. 7 | --- 8 | 9 | # unifi_wlan (Resource) 10 | 11 | `unifi_wlan` manages a WiFi network / SSID. 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | variable "vlan_id" { 17 | default = 10 18 | } 19 | 20 | data "unifi_ap_group" "default" { 21 | } 22 | 23 | data "unifi_user_group" "default" { 24 | } 25 | 26 | resource "unifi_network" "vlan" { 27 | name = "wifi-vlan" 28 | purpose = "corporate" 29 | 30 | subnet = "10.0.0.1/24" 31 | vlan_id = var.vlan_id 32 | dhcp_start = "10.0.0.6" 33 | dhcp_stop = "10.0.0.254" 34 | dhcp_enabled = true 35 | } 36 | 37 | resource "unifi_wlan" "wifi" { 38 | name = "myssid" 39 | passphrase = "12345678" 40 | security = "wpapsk" 41 | 42 | # enable WPA2/WPA3 support 43 | wpa3_support = true 44 | wpa3_transition = true 45 | pmf_mode = "optional" 46 | 47 | network_id = unifi_network.vlan.id 48 | ap_group_ids = [data.unifi_ap_group.default.id] 49 | user_group_id = data.unifi_user_group.default.id 50 | } 51 | ``` 52 | 53 | 54 | ## Schema 55 | 56 | ### Required 57 | 58 | - `name` (String) The SSID of the network. 59 | - `security` (String) The type of WiFi security for this network. Valid values are: `wpapsk`, `wpaeap`, and `open`. 60 | - `user_group_id` (String) ID of the user group to use for this network. 61 | 62 | ### Optional 63 | 64 | - `ap_group_ids` (Set of String) IDs of the AP groups to use for this network. 65 | - `bss_transition` (Boolean) Improves client transitions between APs when they have a weak signal. Defaults to `true`. 66 | - `fast_roaming_enabled` (Boolean) Enables 802.11r fast roaming. Defaults to `false`. 67 | - `hide_ssid` (Boolean) Indicates whether or not to hide the SSID from broadcast. 68 | - `is_guest` (Boolean) Indicates that this is a guest WLAN and should use guest behaviors. 69 | - `l2_isolation` (Boolean) Isolates stations on layer 2 (ethernet) level. Defaults to `false`. 70 | - `mac_filter_enabled` (Boolean) Indicates whether or not the MAC filter is turned of for the network. 71 | - `mac_filter_list` (Set of String) List of MAC addresses to filter (only valid if `mac_filter_enabled` is `true`). 72 | - `mac_filter_policy` (String) MAC address filter policy (only valid if `mac_filter_enabled` is `true`). Defaults to `deny`. 73 | - `minimum_data_rate_2g_kbps` (Number) Set minimum data rate control for 2G devices, in Kbps. Use `0` to disable minimum data rates. Valid values are: `1000`, `2000`, `5500`, `6000`, `9000`, `11000`, `12000`, `18000`, `24000`, `36000`, `48000`, and `54000`. 74 | - `minimum_data_rate_5g_kbps` (Number) Set minimum data rate control for 5G devices, in Kbps. Use `0` to disable minimum data rates. Valid values are: `6000`, `9000`, `12000`, `18000`, `24000`, `36000`, `48000`, and `54000`. 75 | - `multicast_enhance` (Boolean) Indicates whether or not Multicast Enhance is turned of for the network. 76 | - `network_id` (String) ID of the network for this SSID 77 | - `no2ghz_oui` (Boolean) Connect high performance clients to 5 GHz only. Defaults to `true`. 78 | - `passphrase` (String, Sensitive) The passphrase for the network, this is only required if `security` is not set to `open`. 79 | - `pmf_mode` (String) Enable Protected Management Frames. This cannot be disabled if using WPA 3. Valid values are `required`, `optional` and `disabled`. Defaults to `disabled`. 80 | - `proxy_arp` (Boolean) Reduces airtime usage by allowing APs to "proxy" common broadcast frames as unicast. Defaults to `false`. 81 | - `radius_profile_id` (String) ID of the RADIUS profile to use when security `wpaeap`. You can query this via the `unifi_radius_profile` data source. 82 | - `schedule` (Block List) Start and stop schedules for the WLAN (see [below for nested schema](#nestedblock--schedule)) 83 | - `site` (String) The name of the site to associate the wlan with. 84 | - `uapsd` (Boolean) Enable Unscheduled Automatic Power Save Delivery. Defaults to `false`. 85 | - `wlan_band` (String) Radio band your WiFi network will use. Defaults to `both`. 86 | - `wpa3_support` (Boolean) Enable WPA 3 support (security must be `wpapsk` and PMF must be turned on). 87 | - `wpa3_transition` (Boolean) Enable WPA 3 and WPA 2 support (security must be `wpapsk` and `wpa3_support` must be true). 88 | 89 | ### Read-Only 90 | 91 | - `id` (String) The ID of the network. 92 | 93 | 94 | ### Nested Schema for `schedule` 95 | 96 | Required: 97 | 98 | - `day_of_week` (String) Day of week for the block. Valid values are `sun`, `mon`, `tue`, `wed`, `thu`, `fri`, `sat`. 99 | - `duration` (Number) Length of the block in minutes. 100 | - `start_hour` (Number) Start hour for the block (0-23). 101 | 102 | Optional: 103 | 104 | - `name` (String) Name of the block. 105 | - `start_minute` (Number) Start minute for the block (0-59). Defaults to `0`. 106 | 107 | ## Import 108 | 109 | Import is supported using the following syntax: 110 | 111 | ```shell 112 | # import from provider configured site 113 | terraform import unifi_wlan.mywlan 5dc28e5e9106d105bdc87217 114 | 115 | # import from another site 116 | terraform import unifi_wlan.mywlan bfa2l6i7:5dc28e5e9106d105bdc87217 117 | ``` 118 | -------------------------------------------------------------------------------- /examples/csv_users/users.csv: -------------------------------------------------------------------------------- 1 | mac,name,note 2 | 01:23:45:67:89:AB,My Device,custom note 3 | -------------------------------------------------------------------------------- /examples/csv_users/users.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | userscsv = csvdecode(file("${path.module}/users.csv")) 3 | users = { for user in local.userscsv : user.mac => user } 4 | } 5 | 6 | resource "unifi_user" "user" { 7 | for_each = local.users 8 | 9 | mac = each.key 10 | name = each.value.name 11 | # append an optional additional note 12 | note = trimspace("${each.value.note}\n\nmanaged by TF") 13 | 14 | allow_existing = true 15 | skip_forget_on_destroy = true 16 | } -------------------------------------------------------------------------------- /examples/data-sources/unifi_ap_group/data-source.tf: -------------------------------------------------------------------------------- 1 | data "unifi_ap_group" "default" { 2 | } -------------------------------------------------------------------------------- /examples/data-sources/unifi_network/data-source.tf: -------------------------------------------------------------------------------- 1 | #retrieve network data by unifi network name 2 | data "unifi_network" "lan_network" { 3 | name = "Default" 4 | } 5 | 6 | #retrieve network data from user record 7 | data "unifi_user" "my_device" { 8 | mac = "01:23:45:67:89:ab" 9 | } 10 | data "unifi_network" "my_network" { 11 | id = data.unifi_user.my_device.network_id 12 | } 13 | -------------------------------------------------------------------------------- /examples/data-sources/unifi_port_profile/data-source.tf: -------------------------------------------------------------------------------- 1 | data "unifi_port_profile" "all" { 2 | } 3 | -------------------------------------------------------------------------------- /examples/data-sources/unifi_user/data-source.tf: -------------------------------------------------------------------------------- 1 | data "unifi_user" "client" { 2 | mac = "01:23:45:67:89:ab" 3 | } 4 | -------------------------------------------------------------------------------- /examples/multiple_site_firewall/firewall.tf: -------------------------------------------------------------------------------- 1 | resource "unifi_firewall_rule" "rule" { 2 | # list of sites 3 | for_each = toset(["default", "vq98kwez", "bfa2l6i7"]) 4 | # use the key of the list as the site value 5 | site = each.key 6 | 7 | name = "drop all" 8 | action = "drop" 9 | ruleset = "LAN_IN" 10 | 11 | rule_index = 2011 12 | 13 | protocol = "all" 14 | 15 | dst_address = var.ip_address 16 | } 17 | -------------------------------------------------------------------------------- /examples/provider/provider.tf: -------------------------------------------------------------------------------- 1 | provider "unifi" { 2 | username = var.username # optionally use UNIFI_USERNAME env var 3 | password = var.password # optionally use UNIFI_PASSWORD env var 4 | api_url = var.api_url # optionally use UNIFI_API env var 5 | 6 | # you may need to allow insecure TLS communications unless you have configured 7 | # certificates for your controller 8 | allow_insecure = var.insecure # optionally use UNIFI_INSECURE env var 9 | 10 | # if you are not configuring the default site, you can change the site 11 | # site = "foo" or optionally use UNIFI_SITE env var 12 | } -------------------------------------------------------------------------------- /examples/provider/test.auto.tfvars: -------------------------------------------------------------------------------- 1 | username = "tfacctest" 2 | password = "tfacctest1234" 3 | 4 | # this assumes the default port for acc testing 5 | api_url = "https://localhost:8443/api/" 6 | insecure = true -------------------------------------------------------------------------------- /examples/provider/test.tf: -------------------------------------------------------------------------------- 1 | data "unifi_ap_group" "default" { 2 | } 3 | -------------------------------------------------------------------------------- /examples/provider/variables.tf: -------------------------------------------------------------------------------- 1 | variable "username" { 2 | } 3 | 4 | variable "password" { 5 | } 6 | 7 | variable "api_url" { 8 | } 9 | 10 | variable "insecure" { 11 | default = false 12 | } -------------------------------------------------------------------------------- /examples/resources/unifi_device/resource.tf: -------------------------------------------------------------------------------- 1 | data "unifi_port_profile" "disabled" { 2 | # look up the built-in disabled port profile 3 | name = "Disabled" 4 | } 5 | 6 | resource "unifi_port_profile" "poe" { 7 | name = "poe" 8 | forward = "customize" 9 | 10 | native_networkconf_id = var.native_network_id 11 | tagged_networkconf_ids = [ 12 | var.some_vlan_network_id, 13 | ] 14 | 15 | poe_mode = "auto" 16 | } 17 | 18 | resource "unifi_device" "us_24_poe" { 19 | # optionally specify MAC address to skip manually importing 20 | # manual import is the safest way to add a device 21 | mac = "01:23:45:67:89:AB" 22 | 23 | name = "Switch with POE" 24 | 25 | port_override { 26 | number = 1 27 | name = "port w/ poe" 28 | port_profile_id = unifi_port_profile.poe.id 29 | } 30 | 31 | port_override { 32 | number = 2 33 | name = "disabled" 34 | port_profile_id = data.unifi_port_profile.disabled.id 35 | } 36 | 37 | # port aggregation for ports 11 and 12 38 | port_override { 39 | number = 11 40 | op_mode = "aggregate" 41 | aggregate_num_ports = 2 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/resources/unifi_dns_record/resource.tf: -------------------------------------------------------------------------------- 1 | resource "unifi_dns_record" "test" { 2 | name = "my-network" 3 | enabled = true 4 | port = 0 5 | priority = 10 6 | record_type = "A" 7 | ttl = 300 8 | value = "my-network.example.com" 9 | } 10 | -------------------------------------------------------------------------------- /examples/resources/unifi_dynamic_dns/resource.tf: -------------------------------------------------------------------------------- 1 | resource "unifi_dynamic_dns" "test" { 2 | service = "dyndns" 3 | 4 | host_name = "my-network.example.com" 5 | 6 | server = "domains.google.com" 7 | login = var.dns_login 8 | password = var.dns_password 9 | } 10 | -------------------------------------------------------------------------------- /examples/resources/unifi_firewall_group/resource.tf: -------------------------------------------------------------------------------- 1 | variable "laptop_ips" { 2 | type = list(string) 3 | } 4 | 5 | resource "unifi_firewall_group" "can_print" { 6 | name = "can-print" 7 | type = "address-group" 8 | 9 | members = var.laptop_ips 10 | } -------------------------------------------------------------------------------- /examples/resources/unifi_firewall_group/test.auto.tfvars: -------------------------------------------------------------------------------- 1 | # these values are used in tests 2 | laptop_ips = ["192.168.1.25"] -------------------------------------------------------------------------------- /examples/resources/unifi_firewall_rule/import.sh: -------------------------------------------------------------------------------- 1 | # import using the ID from the controller API/UI 2 | terraform import unifi_firewall_rule.my_rule 5f7080eb6b8969064f80494f 3 | -------------------------------------------------------------------------------- /examples/resources/unifi_firewall_rule/resource.tf: -------------------------------------------------------------------------------- 1 | variable "ip_address" { 2 | type = string 3 | } 4 | 5 | resource "unifi_firewall_rule" "drop_all" { 6 | name = "drop all" 7 | action = "drop" 8 | ruleset = "LAN_IN" 9 | 10 | rule_index = 2011 11 | 12 | protocol = "all" 13 | 14 | dst_address = var.ip_address 15 | } -------------------------------------------------------------------------------- /examples/resources/unifi_firewall_rule/test.auto.tfvars: -------------------------------------------------------------------------------- 1 | # these values are used in tests 2 | ip_address = "192.168.1.1" -------------------------------------------------------------------------------- /examples/resources/unifi_network/import.sh: -------------------------------------------------------------------------------- 1 | # import from provider configured site 2 | terraform import unifi_network.mynetwork 5dc28e5e9106d105bdc87217 3 | 4 | # import from another site 5 | terraform import unifi_network.mynetwork bfa2l6i7:5dc28e5e9106d105bdc87217 6 | 7 | # import network by name 8 | terraform import unifi_network.mynetwork name=LAN 9 | -------------------------------------------------------------------------------- /examples/resources/unifi_network/resource.tf: -------------------------------------------------------------------------------- 1 | variable "vlan_id" { 2 | default = 10 3 | } 4 | 5 | resource "unifi_network" "vlan" { 6 | name = "wifi-vlan" 7 | purpose = "corporate" 8 | 9 | subnet = "10.0.0.1/24" 10 | vlan_id = var.vlan_id 11 | dhcp_start = "10.0.0.6" 12 | dhcp_stop = "10.0.0.254" 13 | dhcp_enabled = true 14 | } 15 | 16 | resource "unifi_network" "wan" { 17 | name = "wan" 18 | purpose = "wan" 19 | 20 | wan_networkgroup = "WAN" 21 | wan_type = "pppoe" 22 | wan_ip = "192.168.1.1" 23 | wan_egress_qos = 1 24 | wan_username = "username" 25 | x_wan_password = "password" 26 | } 27 | -------------------------------------------------------------------------------- /examples/resources/unifi_network/test.auto.tfvars: -------------------------------------------------------------------------------- 1 | vlan_id = 41 -------------------------------------------------------------------------------- /examples/resources/unifi_port_profile/resource.tf: -------------------------------------------------------------------------------- 1 | variable "vlan_id" { 2 | default = 10 3 | } 4 | 5 | resource "unifi_network" "vlan" { 6 | name = "wifi-vlan" 7 | purpose = "corporate" 8 | 9 | subnet = "10.0.0.1/24" 10 | vlan_id = var.vlan_id 11 | dhcp_start = "10.0.0.6" 12 | dhcp_stop = "10.0.0.254" 13 | dhcp_enabled = true 14 | } 15 | 16 | resource "unifi_port_profile" "poe_disabled" { 17 | name = "POE Disabled" 18 | 19 | native_networkconf_id = unifi_network.vlan.id 20 | poe_mode = "off" 21 | } 22 | -------------------------------------------------------------------------------- /examples/resources/unifi_setting_mgmt/resource.tf: -------------------------------------------------------------------------------- 1 | resource "unifi_site" "example" { 2 | description = "example" 3 | } 4 | 5 | resource "unifi_setting_mgmt" "example" { 6 | site = unifi_site.example.name 7 | auto_upgrade = true 8 | } 9 | -------------------------------------------------------------------------------- /examples/resources/unifi_site/import.sh: -------------------------------------------------------------------------------- 1 | # import using the API/UI ID 2 | terraform import unifi_site.mysite 5fe6261995fe130013456a36 3 | 4 | # import using the name (short ID) 5 | terraform import unifi_site.mysite vq98kwez 6 | -------------------------------------------------------------------------------- /examples/resources/unifi_site/resource.tf: -------------------------------------------------------------------------------- 1 | resource "unifi_site" "mysite" { 2 | description = "mysite" 3 | } 4 | -------------------------------------------------------------------------------- /examples/resources/unifi_static_route/resource.tf: -------------------------------------------------------------------------------- 1 | resource "unifi_static_route" "nexthop" { 2 | type = "nexthop-route" 3 | network = "172.17.0.0/16" 4 | name = "basic nexthop" 5 | distance = 1 6 | next_hop = "172.16.0.1" 7 | } 8 | 9 | resource "unifi_static_route" "blackhole" { 10 | type = "blackhole" 11 | network = var.blackhole_cidr 12 | name = "blackhole traffice to cidr" 13 | distance = 1 14 | } 15 | 16 | resource "unifi_static_route" "interface" { 17 | type = "interface-route" 18 | network = var.wan2_cidr 19 | name = "send traffic over wan2" 20 | distance = 1 21 | interface = "WAN2" 22 | } 23 | -------------------------------------------------------------------------------- /examples/resources/unifi_user/resource.tf: -------------------------------------------------------------------------------- 1 | resource "unifi_user" "test" { 2 | mac = "01:23:45:67:89:AB" 3 | name = "some client" 4 | note = "my note" 5 | 6 | fixed_ip = "10.0.0.50" 7 | network_id = unifi_network.my_vlan.id 8 | } -------------------------------------------------------------------------------- /examples/resources/unifi_user/test.auto.tfvars: -------------------------------------------------------------------------------- 1 | vlan_id = 42 -------------------------------------------------------------------------------- /examples/resources/unifi_user/test.tf: -------------------------------------------------------------------------------- 1 | variable "vlan_id" { 2 | default = 10 3 | } 4 | 5 | resource "unifi_network" "my_vlan" { 6 | name = "wifi-vlan" 7 | purpose = "corporate" 8 | 9 | subnet = "10.0.0.1/24" 10 | vlan_id = var.vlan_id 11 | dhcp_start = "10.0.0.6" 12 | dhcp_stop = "10.0.0.254" 13 | dhcp_enabled = true 14 | } -------------------------------------------------------------------------------- /examples/resources/unifi_user_group/import.sh: -------------------------------------------------------------------------------- 1 | # import using the ID 2 | terraform import unifi_user_group.wifi 5fe6261995fe130013456a36 -------------------------------------------------------------------------------- /examples/resources/unifi_user_group/resource.tf: -------------------------------------------------------------------------------- 1 | resource "unifi_user_group" "wifi" { 2 | name = "wifi" 3 | 4 | qos_rate_max_down = 2000 # 2mbps 5 | qos_rate_max_up = 10 # 10kbps 6 | } -------------------------------------------------------------------------------- /examples/resources/unifi_wlan/import.sh: -------------------------------------------------------------------------------- 1 | # import from provider configured site 2 | terraform import unifi_wlan.mywlan 5dc28e5e9106d105bdc87217 3 | 4 | # import from another site 5 | terraform import unifi_wlan.mywlan bfa2l6i7:5dc28e5e9106d105bdc87217 6 | -------------------------------------------------------------------------------- /examples/resources/unifi_wlan/resource.tf: -------------------------------------------------------------------------------- 1 | variable "vlan_id" { 2 | default = 10 3 | } 4 | 5 | data "unifi_ap_group" "default" { 6 | } 7 | 8 | data "unifi_user_group" "default" { 9 | } 10 | 11 | resource "unifi_network" "vlan" { 12 | name = "wifi-vlan" 13 | purpose = "corporate" 14 | 15 | subnet = "10.0.0.1/24" 16 | vlan_id = var.vlan_id 17 | dhcp_start = "10.0.0.6" 18 | dhcp_stop = "10.0.0.254" 19 | dhcp_enabled = true 20 | } 21 | 22 | resource "unifi_wlan" "wifi" { 23 | name = "myssid" 24 | passphrase = "12345678" 25 | security = "wpapsk" 26 | 27 | # enable WPA2/WPA3 support 28 | wpa3_support = true 29 | wpa3_transition = true 30 | pmf_mode = "optional" 31 | 32 | network_id = unifi_network.vlan.id 33 | ap_group_ids = [data.unifi_ap_group.default.id] 34 | user_group_id = data.unifi_user_group.default.id 35 | } 36 | -------------------------------------------------------------------------------- /internal/provider/cidr.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | ) 9 | 10 | func cidrValidate(raw any, key string) ([]string, []error) { 11 | v, ok := raw.(string) 12 | if !ok { 13 | return nil, []error{fmt.Errorf("expected string, got %T", raw)} 14 | } 15 | 16 | _, _, err := net.ParseCIDR(v) 17 | if err != nil { 18 | return nil, []error{err} 19 | } 20 | 21 | return nil, nil 22 | } 23 | 24 | func cidrDiffSuppress(k, old, new string, d *schema.ResourceData) bool { 25 | _, oldNet, err := net.ParseCIDR(old) 26 | if err != nil { 27 | return false 28 | } 29 | 30 | _, newNet, err := net.ParseCIDR(new) 31 | if err != nil { 32 | return false 33 | } 34 | 35 | return oldNet.String() == newNet.String() 36 | } 37 | 38 | func cidrZeroBased(cidr string) string { 39 | _, cidrNet, err := net.ParseCIDR(cidr) 40 | if err != nil { 41 | return "" 42 | } 43 | 44 | return cidrNet.String() 45 | } 46 | 47 | func cidrOneBased(cidr string) string { 48 | _, cidrNet, err := net.ParseCIDR(cidr) 49 | if err != nil { 50 | return "" 51 | } 52 | 53 | cidrNet.IP[3]++ 54 | 55 | return cidrNet.String() 56 | } 57 | -------------------------------------------------------------------------------- /internal/provider/cidr_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCIDRValidate(t *testing.T) { 8 | for _, c := range []struct { 9 | expectedError string 10 | cidr string 11 | }{ 12 | {"invalid CIDR address: ", ""}, 13 | {"invalid CIDR address: abc", "abc"}, 14 | {"invalid CIDR address: 192.1.2.3", "192.1.2.3"}, 15 | {"invalid CIDR address: 500.1.2.3/20", "500.1.2.3/20"}, 16 | {"invalid CIDR address: 192.1.2.3/500", "192.1.2.3/500"}, 17 | 18 | {"", "192.1.2.1/20"}, 19 | } { 20 | t.Run(c.cidr, func(t *testing.T) { 21 | _, actualErrs := cidrValidate(c.cidr, "key") 22 | switch len(actualErrs) { 23 | case 0: 24 | if c.expectedError != "" { 25 | t.Fatalf("expected no error, got %d: %#v", len(actualErrs), actualErrs) 26 | } 27 | case 1: 28 | actualErr := actualErrs[0].Error() 29 | if actualErr != c.expectedError { 30 | t.Fatalf("expected %q, got %q", c.expectedError, actualErr) 31 | } 32 | default: 33 | t.Fatalf("expected 0 or 1 errors, got %d: %#v", len(actualErrs), actualErrs) 34 | } 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/provider/controller_versions.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/go-version" 7 | ) 8 | 9 | var ( 10 | controllerV6 = version.Must(version.NewVersion("6.0.0")) 11 | controllerV7 = version.Must(version.NewVersion("7.0.0")) 12 | 13 | // https://community.ui.com/releases/UniFi-Network-Controller-6-1-61/62f1ad38-1ac5-430c-94b0-becbb8f71d7d 14 | controllerVersionWPA3 = version.Must(version.NewVersion("6.1.61")) 15 | ) 16 | 17 | func (c *client) ControllerVersion() *version.Version { 18 | return version.Must(version.NewVersion(c.c.Version())) 19 | } 20 | 21 | func checkMinimumControllerVersion(versionString string) error { 22 | v, err := version.NewVersion(versionString) 23 | if err != nil { 24 | return err 25 | } 26 | if v.LessThan(controllerV6) { 27 | return fmt.Errorf("Controller version %q or greater is required to use the provider, found %q.", controllerV6, v) 28 | } 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/provider/controller_versions_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/go-version" 7 | ) 8 | 9 | func preCheckMinVersion(t *testing.T, min *version.Version) { 10 | v, err := version.NewVersion(testClient.Version()) 11 | if err != nil { 12 | t.Fatalf("error parsing version: %s", err) 13 | } 14 | if v.LessThan(min) { 15 | t.Skipf("skipping test on controller version %q (need at least %q)", v, min) 16 | } 17 | } 18 | 19 | func preCheckVersionConstraint(t *testing.T, cs string) { 20 | v, err := version.NewVersion(testClient.Version()) 21 | if err != nil { 22 | t.Fatalf("Error parsing version: %s", err) 23 | } 24 | 25 | c, err := version.NewConstraint(cs) 26 | if err != nil { 27 | t.Fatalf("Error parsing version constriant: %s", err) 28 | } 29 | 30 | if !c.Check(v) { 31 | t.Skipf("Skipping test on controller version %q (constrained to %q)", v, c) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internal/provider/data_account.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | ) 9 | 10 | func dataAccount() *schema.Resource { 11 | return &schema.Resource{ 12 | Description: "`unifi_account` data source can be used to retrieve RADIUS user accounts", 13 | 14 | ReadContext: dataAccountRead, 15 | 16 | Schema: map[string]*schema.Schema{ 17 | "id": { 18 | Description: "The ID of this account.", 19 | Type: schema.TypeString, 20 | Computed: true, 21 | }, 22 | "site": { 23 | Description: "The name of the site the account is associated with.", 24 | Type: schema.TypeString, 25 | Computed: true, 26 | Optional: true, 27 | }, 28 | "name": { 29 | Description: "The name of the account to look up", 30 | Type: schema.TypeString, 31 | Required: true, 32 | }, 33 | 34 | "password": { 35 | Description: "The password of the account.", 36 | Type: schema.TypeString, 37 | Computed: true, 38 | Sensitive: true, 39 | }, 40 | "tunnel_type": { 41 | Description: "See RFC2868 section 3.1", // @TODO: better documentation https://help.ui.com/hc/en-us/articles/360015268353-UniFi-USG-UDM-Configuring-RADIUS-Server#6 42 | Type: schema.TypeInt, 43 | Computed: true, 44 | }, 45 | "tunnel_medium_type": { 46 | Description: "See RFC2868 section 3.2", // @TODO: better documentation https://help.ui.com/hc/en-us/articles/360015268353-UniFi-USG-UDM-Configuring-RADIUS-Server#6 47 | Type: schema.TypeInt, 48 | Computed: true, 49 | }, 50 | "network_id": { 51 | Description: "ID of the network for this account", 52 | Type: schema.TypeString, 53 | Computed: true, 54 | }, 55 | }, 56 | } 57 | } 58 | 59 | func dataAccountRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 60 | c := meta.(*client) 61 | 62 | name := d.Get("name").(string) 63 | site := d.Get("site").(string) 64 | if site == "" { 65 | site = c.site 66 | } 67 | 68 | accounts, err := c.c.ListAccounts(ctx, site) 69 | if err != nil { 70 | return diag.FromErr(err) 71 | } 72 | for _, account := range accounts { 73 | if account.Name == name { 74 | d.SetId(account.ID) 75 | d.Set("name", account.Name) 76 | d.Set("password", account.XPassword) 77 | d.Set("tunnel_type", account.TunnelType) 78 | d.Set("tunnel_medium_type", account.TunnelMediumType) 79 | d.Set("network_id", account.NetworkID) 80 | d.Set("site", site) 81 | return nil 82 | } 83 | } 84 | 85 | return diag.Errorf("Account not found with name %s", name) 86 | } 87 | -------------------------------------------------------------------------------- /internal/provider/data_account_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 6 | "testing" 7 | ) 8 | 9 | func TestAccDataAccount_default(t *testing.T) { 10 | resource.ParallelTest(t, resource.TestCase{ 11 | PreCheck: func() { 12 | preCheck(t) 13 | }, 14 | ProviderFactories: providerFactories, 15 | // TODO: CheckDestroy: , 16 | Steps: []resource.TestStep{ 17 | { 18 | Config: testAccDataAccountConfig("tfusertest", "secure_1234"), 19 | Check: resource.ComposeTestCheckFunc(), 20 | }, 21 | }, 22 | }) 23 | } 24 | 25 | func TestAccDataAccount_mac(t *testing.T) { 26 | resource.ParallelTest(t, resource.TestCase{ 27 | PreCheck: func() { 28 | preCheck(t) 29 | }, 30 | ProviderFactories: providerFactories, 31 | // TODO: CheckDestroy: , 32 | Steps: []resource.TestStep{ 33 | { 34 | Config: testAccDataMacAccountConfig("00B0D06FC226"), 35 | Check: resource.ComposeTestCheckFunc(), 36 | }, 37 | }, 38 | }) 39 | } 40 | 41 | func testAccDataAccountConfig(name, password string) string { 42 | return fmt.Sprintf(` 43 | resource "unifi_account" "test" { 44 | name = "%s" 45 | password = "%s" 46 | } 47 | 48 | data "unifi_account" "test" { 49 | name = "%s" 50 | depends_on = [ 51 | unifi_account.test 52 | ] 53 | } 54 | `, name, password, name) 55 | } 56 | 57 | func testAccDataMacAccountConfig(mac string) string { 58 | return fmt.Sprintf(` 59 | resource "unifi_account" "test" { 60 | name = "%s" 61 | password = "%s" 62 | } 63 | 64 | data "unifi_account" "test" { 65 | name = "%s" 66 | depends_on = [ 67 | unifi_account.test 68 | ] 69 | } 70 | `, mac, mac, mac) 71 | } 72 | -------------------------------------------------------------------------------- /internal/provider/data_ap_group.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | ) 9 | 10 | func dataAPGroup() *schema.Resource { 11 | return &schema.Resource{ 12 | Description: "`unifi_ap_group` data source can be used to retrieve the ID for an AP group by name.", 13 | 14 | ReadContext: dataAPGroupRead, 15 | 16 | Schema: map[string]*schema.Schema{ 17 | "id": { 18 | Description: "The ID of this AP group.", 19 | Type: schema.TypeString, 20 | Computed: true, 21 | }, 22 | "site": { 23 | Description: "The name of the site the AP group is associated with.", 24 | Type: schema.TypeString, 25 | Computed: true, 26 | Optional: true, 27 | }, 28 | "name": { 29 | Description: "The name of the AP group to look up, leave blank to look up the default AP group.", 30 | Type: schema.TypeString, 31 | Optional: true, 32 | }, 33 | }, 34 | } 35 | } 36 | 37 | func dataAPGroupRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 38 | c := meta.(*client) 39 | 40 | name := d.Get("name").(string) 41 | site := d.Get("site").(string) 42 | if site == "" { 43 | site = c.site 44 | } 45 | 46 | groups, err := c.c.ListAPGroup(ctx, site) 47 | if err != nil { 48 | return diag.FromErr(err) 49 | } 50 | for _, g := range groups { 51 | if (name == "" && g.HiddenID == "default") || g.Name == name { 52 | d.SetId(g.ID) 53 | d.Set("site", site) 54 | return nil 55 | } 56 | } 57 | 58 | return diag.Errorf("AP group not found with name %s", name) 59 | } 60 | -------------------------------------------------------------------------------- /internal/provider/data_ap_group_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 7 | ) 8 | 9 | func TestAccDataAPGroup_default(t *testing.T) { 10 | resource.ParallelTest(t, resource.TestCase{ 11 | PreCheck: func() { 12 | preCheck(t) 13 | }, 14 | ProviderFactories: providerFactories, 15 | // TODO: CheckDestroy: , 16 | Steps: []resource.TestStep{ 17 | { 18 | Config: testAccDataAPGroupConfig_default, 19 | Check: resource.ComposeTestCheckFunc( 20 | // testCheckNetworkExists(t, "name"), 21 | ), 22 | }, 23 | }, 24 | }) 25 | } 26 | 27 | const testAccDataAPGroupConfig_default = ` 28 | data "unifi_ap_group" "default" { 29 | } 30 | ` 31 | -------------------------------------------------------------------------------- /internal/provider/data_dns_record.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | ) 9 | 10 | func dataDNSRecord() *schema.Resource { 11 | return &schema.Resource{ 12 | Description: "`unifi_dns_record` data source can be used to retrieve the ID for an DNS record by name.", 13 | 14 | ReadContext: dataDNSRecordRead, 15 | 16 | Schema: map[string]*schema.Schema{ 17 | "id": { 18 | Description: "The ID of this DNS record.", 19 | Type: schema.TypeString, 20 | Computed: true, 21 | }, 22 | "site": { 23 | Description: "The name of the site the DNS record is associated with.", 24 | Type: schema.TypeString, 25 | Computed: true, 26 | Optional: true, 27 | }, 28 | "name": { 29 | Description: "The name of the DNS record to look up, leave blank to look up the default DNS record.", 30 | Type: schema.TypeString, 31 | Optional: true, 32 | }, 33 | "port": { 34 | Description: "The port of the DNS record.", 35 | Type: schema.TypeInt, 36 | Optional: true, 37 | }, 38 | "priority": { 39 | Description: "The priority of the DNS record.", 40 | Type: schema.TypeInt, 41 | Optional: true, 42 | }, 43 | "record_type": { 44 | Description: "The type of the DNS record.", 45 | Type: schema.TypeString, 46 | Optional: true, 47 | }, 48 | "ttl": { 49 | Description: "The TTL of the DNS record.", 50 | Type: schema.TypeInt, 51 | Optional: true, 52 | }, 53 | "value": { 54 | Description: "The value of the DNS record.", 55 | Type: schema.TypeString, 56 | Optional: true, 57 | }, 58 | "weight": { 59 | Description: "The weight of the DNS record.", 60 | Type: schema.TypeInt, 61 | Optional: true, 62 | }, 63 | }, 64 | } 65 | } 66 | 67 | func dataDNSRecordRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 68 | c := meta.(*client) 69 | 70 | name := d.Get("name").(string) 71 | site := d.Get("site").(string) 72 | if site == "" { 73 | site = c.site 74 | } 75 | 76 | groups, err := c.c.ListDNSRecord(ctx, site) 77 | if err != nil { 78 | return diag.FromErr(err) 79 | } 80 | for _, g := range groups { 81 | if (name == "" && g.HiddenID == "default") || g.Key == name { 82 | d.SetId(g.ID) 83 | d.Set("site", site) 84 | d.Set("port", g.Port) 85 | d.Set("priority", g.Priority) 86 | d.Set("record_type", g.RecordType) 87 | d.Set("ttl", g.Ttl) 88 | d.Set("value", g.Value) 89 | d.Set("weight", g.Weight) 90 | 91 | return nil 92 | } 93 | } 94 | 95 | return diag.Errorf("DNS record not found with name %s", name) 96 | } 97 | -------------------------------------------------------------------------------- /internal/provider/data_dns_record_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 7 | ) 8 | 9 | func TestAccDataDNSRecord_default(t *testing.T) { 10 | resource.ParallelTest(t, resource.TestCase{ 11 | PreCheck: func() { 12 | preCheck(t) 13 | }, 14 | ProviderFactories: providerFactories, 15 | // TODO: CheckDestroy: , 16 | Steps: []resource.TestStep{ 17 | { 18 | Config: testAccDataDNSRecordConfig_default, 19 | Check: resource.ComposeTestCheckFunc( 20 | // testCheckNetworkExists(t, "name"), 21 | ), 22 | }, 23 | }, 24 | }) 25 | } 26 | 27 | const testAccDataDNSRecordConfig_default = ` 28 | data "unifi_dns_record" "default" { 29 | } 30 | ` 31 | -------------------------------------------------------------------------------- /internal/provider/data_network_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/go-version" 8 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 9 | ) 10 | 11 | func TestAccDataNetwork_byName(t *testing.T) { 12 | defaultName := "Default" 13 | v, err := version.NewVersion(testClient.Version()) 14 | if err != nil { 15 | t.Fatalf("error parsing version: %s", err) 16 | } 17 | if v.LessThan(controllerV7) { 18 | defaultName = "LAN" 19 | } 20 | 21 | resource.ParallelTest(t, resource.TestCase{ 22 | PreCheck: func() { 23 | preCheck(t) 24 | }, 25 | ProviderFactories: providerFactories, 26 | // TODO: CheckDestroy: , 27 | Steps: []resource.TestStep{ 28 | { 29 | Config: testAccDataNetworkConfig_byName(defaultName), 30 | Check: resource.ComposeTestCheckFunc( 31 | // testCheckNetworkExists(t, "name"), 32 | ), 33 | }, 34 | }, 35 | }) 36 | } 37 | 38 | func TestAccDataNetwork_byID(t *testing.T) { 39 | defaultName := "Default" 40 | v, err := version.NewVersion(testClient.Version()) 41 | if err != nil { 42 | t.Fatalf("error parsing version: %s", err) 43 | } 44 | if v.LessThan(controllerV7) { 45 | defaultName = "LAN" 46 | } 47 | 48 | resource.ParallelTest(t, resource.TestCase{ 49 | PreCheck: func() { 50 | preCheck(t) 51 | }, 52 | ProviderFactories: providerFactories, 53 | // TODO: CheckDestroy: , 54 | Steps: []resource.TestStep{ 55 | { 56 | Config: testAccDataNetworkConfig_byID(defaultName), 57 | Check: resource.ComposeTestCheckFunc( 58 | // testCheckNetworkExists(t, "name"), 59 | ), 60 | }, 61 | }, 62 | }) 63 | } 64 | 65 | func testAccDataNetworkConfig_byName(name string) string { 66 | return fmt.Sprintf(` 67 | data "unifi_network" "lan" { 68 | name = %q 69 | } 70 | `, name) 71 | } 72 | 73 | func testAccDataNetworkConfig_byID(name string) string { 74 | return fmt.Sprintf(` 75 | data "unifi_network" "lan" { 76 | name = %q 77 | } 78 | 79 | data "unifi_network" "lan_id" { 80 | id = data.unifi_network.lan.id 81 | } 82 | `, name) 83 | } 84 | -------------------------------------------------------------------------------- /internal/provider/data_port_profile.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | ) 9 | 10 | func dataPortProfile() *schema.Resource { 11 | return &schema.Resource{ 12 | Description: "`unifi_port_profile` data source can be used to retrieve the ID for a port profile by name.", 13 | 14 | ReadContext: dataPortProfileRead, 15 | 16 | Schema: map[string]*schema.Schema{ 17 | "id": { 18 | Description: "The ID of this port profile.", 19 | Type: schema.TypeString, 20 | Computed: true, 21 | }, 22 | "site": { 23 | Description: "The name of the site the port profile is associated with.", 24 | Type: schema.TypeString, 25 | Computed: true, 26 | Optional: true, 27 | }, 28 | "name": { 29 | Description: "The name of the port profile to look up.", 30 | Type: schema.TypeString, 31 | Optional: true, 32 | Default: "All", 33 | }, 34 | }, 35 | } 36 | } 37 | 38 | func dataPortProfileRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 39 | c := meta.(*client) 40 | 41 | name := d.Get("name").(string) 42 | site := d.Get("site").(string) 43 | if site == "" { 44 | site = c.site 45 | } 46 | 47 | groups, err := c.c.ListPortProfile(ctx, site) 48 | if err != nil { 49 | return diag.FromErr(err) 50 | } 51 | for _, g := range groups { 52 | if g.Name == name { 53 | d.SetId(g.ID) 54 | 55 | d.Set("site", site) 56 | 57 | return nil 58 | } 59 | } 60 | 61 | return diag.Errorf("port profile not found with name %s", name) 62 | } 63 | -------------------------------------------------------------------------------- /internal/provider/data_port_profile_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 7 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 8 | ) 9 | 10 | func TestAccDataPortProfile_default(t *testing.T) { 11 | resource.ParallelTest(t, resource.TestCase{ 12 | PreCheck: func() { 13 | preCheck(t) 14 | preCheckVersionConstraint(t, "< 7.4") 15 | }, 16 | ProviderFactories: providerFactories, 17 | // TODO: CheckDestroy: , 18 | Steps: []resource.TestStep{ 19 | { 20 | Config: testAccDataPortProfileConfig_default, 21 | Check: resource.ComposeTestCheckFunc(), 22 | }, 23 | }, 24 | }) 25 | } 26 | 27 | func TestAccDataPortProfile_multiple_providers(t *testing.T) { 28 | resource.ParallelTest(t, resource.TestCase{ 29 | PreCheck: func() { 30 | preCheck(t) 31 | preCheckVersionConstraint(t, "< 7.4") 32 | }, 33 | ProviderFactories: map[string]func() (*schema.Provider, error){ 34 | "unifi2": func() (*schema.Provider, error) { 35 | return New("acctest")(), nil 36 | }, 37 | "unifi3": func() (*schema.Provider, error) { 38 | return New("acctest")(), nil 39 | }, 40 | }, 41 | // TODO: CheckDestroy: , 42 | Steps: []resource.TestStep{ 43 | { 44 | Config: ` 45 | data "unifi_port_profile" "unifi2" { 46 | provider = "unifi2" 47 | } 48 | data "unifi_port_profile" "unifi3" { 49 | provider = "unifi3" 50 | } 51 | `, 52 | Check: resource.ComposeTestCheckFunc( 53 | // testCheckNetworkExists(t, "name"), 54 | ), 55 | }, 56 | }, 57 | }) 58 | } 59 | 60 | const testAccDataPortProfileConfig_default = ` 61 | data "unifi_port_profile" "default" { 62 | } 63 | ` 64 | -------------------------------------------------------------------------------- /internal/provider/data_radius_profile.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | ) 9 | 10 | func dataRADIUSProfile() *schema.Resource { 11 | return &schema.Resource{ 12 | Description: "`unifi_radius_profile` data source can be used to retrieve the ID for a RADIUS profile by name.", 13 | 14 | ReadContext: dataRADIUSProfileRead, 15 | 16 | Schema: map[string]*schema.Schema{ 17 | "id": { 18 | Description: "The ID of this AP group.", 19 | Type: schema.TypeString, 20 | Computed: true, 21 | }, 22 | "site": { 23 | Description: "The name of the site the RADIUS profile is associated with.", 24 | Type: schema.TypeString, 25 | Computed: true, 26 | Optional: true, 27 | }, 28 | "name": { 29 | Description: "The name of the RADIUS profile to look up.", 30 | Type: schema.TypeString, 31 | Optional: true, 32 | Default: "Default", 33 | }, 34 | }, 35 | } 36 | } 37 | 38 | func dataRADIUSProfileRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 39 | c := meta.(*client) 40 | 41 | name := d.Get("name").(string) 42 | site := d.Get("site").(string) 43 | if site == "" { 44 | site = c.site 45 | } 46 | 47 | profiles, err := c.c.ListRADIUSProfile(ctx, site) 48 | if err != nil { 49 | return diag.FromErr(err) 50 | } 51 | for _, g := range profiles { 52 | if g.Name == name { 53 | d.SetId(g.ID) 54 | d.Set("site", site) 55 | return nil 56 | } 57 | } 58 | 59 | return diag.Errorf("RADIUS profile not found with name %s", name) 60 | } 61 | -------------------------------------------------------------------------------- /internal/provider/data_user.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" 10 | ) 11 | 12 | func dataUser() *schema.Resource { 13 | return &schema.Resource{ 14 | Description: "`unifi_user` retrieves properties of a user (or \"client\" in the UI) of the network by MAC address.", 15 | 16 | ReadContext: dataUserRead, 17 | 18 | Schema: map[string]*schema.Schema{ 19 | "site": { 20 | Description: "The name of the site the user is associated with.", 21 | Type: schema.TypeString, 22 | Computed: true, 23 | Optional: true, 24 | }, 25 | "mac": { 26 | Description: "The MAC address of the user.", 27 | Type: schema.TypeString, 28 | Required: true, 29 | DiffSuppressFunc: macDiffSuppressFunc, 30 | ValidateFunc: validation.StringMatch(macAddressRegexp, "Mac address is invalid"), 31 | }, 32 | 33 | // read-only / computed 34 | "id": { 35 | Description: "The ID of the user.", 36 | Type: schema.TypeString, 37 | Computed: true, 38 | }, 39 | "name": { 40 | Description: "The name of the user.", 41 | Type: schema.TypeString, 42 | Computed: true, 43 | }, 44 | "user_group_id": { 45 | Description: "The user group ID for the user.", 46 | Type: schema.TypeString, 47 | Computed: true, 48 | }, 49 | "note": { 50 | Description: "A note with additional information for the user.", 51 | Type: schema.TypeString, 52 | Computed: true, 53 | }, 54 | "fixed_ip": { 55 | Description: "fixed IPv4 address set for this user.", 56 | Type: schema.TypeString, 57 | Computed: true, 58 | }, 59 | "network_id": { 60 | Description: "The network ID for this user.", 61 | Type: schema.TypeString, 62 | Computed: true, 63 | }, 64 | "blocked": { 65 | Description: "Specifies whether this user should be blocked from the network.", 66 | Type: schema.TypeBool, 67 | Computed: true, 68 | }, 69 | "dev_id_override": { 70 | Description: "Override the device fingerprint.", 71 | Type: schema.TypeInt, 72 | Computed: true, 73 | }, 74 | "hostname": { 75 | Description: "The hostname of the user.", 76 | Type: schema.TypeString, 77 | Computed: true, 78 | }, 79 | "ip": { 80 | Description: "The IP address of the user.", 81 | Type: schema.TypeString, 82 | Computed: true, 83 | }, 84 | "local_dns_record": { 85 | Description: "The local DNS record for this user.", 86 | Type: schema.TypeString, 87 | Computed: true, 88 | }, 89 | }, 90 | } 91 | } 92 | 93 | func dataUserRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 94 | c := meta.(*client) 95 | 96 | site := d.Get("site").(string) 97 | if site == "" { 98 | site = c.site 99 | } 100 | mac := d.Get("mac").(string) 101 | 102 | macResp, err := c.c.GetUserByMAC(ctx, site, strings.ToLower(mac)) 103 | if err != nil { 104 | return diag.FromErr(err) 105 | } 106 | 107 | resp, err := c.c.GetUser(ctx, site, macResp.ID) 108 | if err != nil { 109 | return diag.FromErr(err) 110 | } 111 | 112 | // for some reason the IP address is only on this endpoint, so issue another request 113 | 114 | resp.IP = macResp.IP 115 | fixedIP := "" 116 | if resp.UseFixedIP { 117 | fixedIP = resp.FixedIP 118 | } 119 | localDnsRecord := "" 120 | if resp.LocalDNSRecordEnabled { 121 | localDnsRecord = resp.LocalDNSRecord 122 | } 123 | d.SetId(resp.ID) 124 | d.Set("site", site) 125 | d.Set("mac", resp.MAC) 126 | d.Set("name", resp.Name) 127 | d.Set("user_group_id", resp.UserGroupID) 128 | d.Set("note", resp.Note) 129 | d.Set("fixed_ip", fixedIP) 130 | d.Set("network_id", resp.NetworkID) 131 | d.Set("blocked", resp.Blocked) 132 | d.Set("dev_id_override", resp.DevIdOverride) 133 | d.Set("hostname", resp.Hostname) 134 | d.Set("ip", resp.IP) 135 | d.Set("ip", localDnsRecord) 136 | 137 | return nil 138 | } 139 | -------------------------------------------------------------------------------- /internal/provider/data_user_group.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | ) 9 | 10 | func dataUserGroup() *schema.Resource { 11 | return &schema.Resource{ 12 | Description: "`unifi_user_group` data source can be used to retrieve the ID for a user group by name.", 13 | 14 | ReadContext: dataUserGroupRead, 15 | 16 | Schema: map[string]*schema.Schema{ 17 | "id": { 18 | Description: "The ID of this AP group.", 19 | Type: schema.TypeString, 20 | Computed: true, 21 | }, 22 | "site": { 23 | Description: "The name of the site the user group is associated with.", 24 | Type: schema.TypeString, 25 | Computed: true, 26 | Optional: true, 27 | }, 28 | "name": { 29 | Description: "The name of the user group to look up.", 30 | Type: schema.TypeString, 31 | Optional: true, 32 | Default: "Default", 33 | }, 34 | 35 | "qos_rate_max_down": { 36 | Type: schema.TypeInt, 37 | Computed: true, 38 | }, 39 | "qos_rate_max_up": { 40 | Type: schema.TypeInt, 41 | Computed: true, 42 | }, 43 | }, 44 | } 45 | } 46 | 47 | func dataUserGroupRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 48 | c := meta.(*client) 49 | 50 | name := d.Get("name").(string) 51 | site := d.Get("site").(string) 52 | if site == "" { 53 | site = c.site 54 | } 55 | 56 | groups, err := c.c.ListUserGroup(ctx, site) 57 | if err != nil { 58 | return diag.FromErr(err) 59 | } 60 | for _, g := range groups { 61 | if g.Name == name { 62 | d.SetId(g.ID) 63 | 64 | d.Set("site", site) 65 | d.Set("qos_rate_max_down", g.QOSRateMaxDown) 66 | d.Set("qos_rate_max_up", g.QOSRateMaxUp) 67 | 68 | return nil 69 | } 70 | } 71 | 72 | return diag.Errorf("user group not found with name %s", name) 73 | } 74 | -------------------------------------------------------------------------------- /internal/provider/data_user_group_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 7 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 8 | ) 9 | 10 | func TestAccDataUserGroup_default(t *testing.T) { 11 | resource.ParallelTest(t, resource.TestCase{ 12 | PreCheck: func() { preCheck(t) }, 13 | ProviderFactories: providerFactories, 14 | // TODO: CheckDestroy: , 15 | Steps: []resource.TestStep{ 16 | { 17 | Config: testAccDataUserGroupConfig_default, 18 | Check: resource.ComposeTestCheckFunc( 19 | // testCheckNetworkExists(t, "name"), 20 | ), 21 | }, 22 | }, 23 | }) 24 | } 25 | 26 | func TestAccDataUserGroup_multiple_providers(t *testing.T) { 27 | resource.ParallelTest(t, resource.TestCase{ 28 | PreCheck: func() { preCheck(t) }, 29 | ProviderFactories: map[string]func() (*schema.Provider, error){ 30 | "unifi2": func() (*schema.Provider, error) { 31 | return New("acctest")(), nil 32 | }, 33 | "unifi3": func() (*schema.Provider, error) { 34 | return New("acctest")(), nil 35 | }, 36 | }, 37 | // TODO: CheckDestroy: , 38 | Steps: []resource.TestStep{ 39 | { 40 | Config: ` 41 | data "unifi_user_group" "unifi2" { 42 | provider = "unifi2" 43 | } 44 | data "unifi_user_group" "unifi3" { 45 | provider = "unifi3" 46 | } 47 | `, 48 | Check: resource.ComposeTestCheckFunc( 49 | // testCheckNetworkExists(t, "name"), 50 | ), 51 | }, 52 | }, 53 | }) 54 | } 55 | 56 | const testAccDataUserGroupConfig_default = ` 57 | data "unifi_user_group" "default" { 58 | } 59 | ` 60 | -------------------------------------------------------------------------------- /internal/provider/data_user_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 9 | "github.com/ubiquiti-community/go-unifi/unifi" 10 | ) 11 | 12 | func TestAccDataUser_default(t *testing.T) { 13 | mac, unallocateTestMac := allocateTestMac(t) 14 | defer unallocateTestMac() 15 | 16 | resource.ParallelTest(t, resource.TestCase{ 17 | PreCheck: func() { 18 | //preCheck(t) 19 | 20 | _, err := testClient.CreateUser(context.Background(), "default", &unifi.User{ 21 | MAC: mac, 22 | Name: "tfacc-User-Data", 23 | Note: "tfacc-User-Data", 24 | }) 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | }, 29 | //PreCheck: func() { preCheck(t) }, 30 | ProviderFactories: providerFactories, 31 | Steps: []resource.TestStep{ 32 | { 33 | Config: testAccDataUserConfig_default(mac), 34 | Check: resource.ComposeTestCheckFunc(), 35 | }, 36 | }, 37 | }) 38 | } 39 | 40 | func testAccDataUserConfig_default(mac string) string { 41 | return fmt.Sprintf(` 42 | data "unifi_user" "test" { 43 | mac = "%s" 44 | } 45 | `, mac) 46 | } 47 | -------------------------------------------------------------------------------- /internal/provider/importer.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | ) 9 | 10 | func importSiteAndID(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { 11 | if id := d.Id(); strings.Contains(id, ":") { 12 | importParts := strings.SplitN(id, ":", 2) 13 | d.SetId(importParts[1]) 14 | d.Set("site", importParts[0]) 15 | } 16 | return []*schema.ResourceData{d}, nil 17 | } 18 | -------------------------------------------------------------------------------- /internal/provider/mac.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | ) 9 | 10 | var macAddressRegexp = regexp.MustCompile("^([0-9a-fA-F][0-9a-fA-F][-:]){5}([0-9a-fA-F][0-9a-fA-F])$") 11 | 12 | func cleanMAC(mac string) string { 13 | return strings.TrimSpace(strings.ReplaceAll(strings.ToLower(mac), "-", ":")) 14 | } 15 | 16 | func macDiffSuppressFunc(k, old, new string, d *schema.ResourceData) bool { 17 | old = cleanMAC(old) 18 | new = cleanMAC(new) 19 | return old == new 20 | } 21 | -------------------------------------------------------------------------------- /internal/provider/markdown.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import "strconv" 4 | 5 | func markdownValueListInt(values []int) string { 6 | switch { 7 | case len(values) == 0: 8 | return "" 9 | case len(values) == 1: 10 | return "`" + strconv.Itoa(values[0]) + "`" 11 | default: 12 | s := "" 13 | for i := 0; i < len(values)-1; i++ { 14 | s += "`" + strconv.Itoa(values[i]) + "`, " 15 | } 16 | s += " and `" + strconv.Itoa(values[len(values)-1]) + "`" 17 | return s 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/provider/port_range.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" 7 | ) 8 | 9 | var ( 10 | portRangeRegexp = regexp.MustCompile("(([1-9][0-9]{0,3}|[1-5][0-9]{4}|[6][0-4][0-9]{3}|[6][5][0-4][0-9]{2}|[6][5][5][0-2][0-9]|[6][5][5][3][0-5])|([1-9][0-9]{0,3}|[1-5][0-9]{4}|[6][0-4][0-9]{3}|[6][5][0-4][0-9]{2}|[6][5][5][0-2][0-9]|[6][5][5][3][0-5])-([1-9][0-9]{0,3}|[1-5][0-9]{4}|[6][0-4][0-9]{3}|[6][5][0-4][0-9]{2}|[6][5][5][0-2][0-9]|[6][5][5][3][0-5]))+(,([1-9][0-9]{0,3}|[1-5][0-9]{4}|[6][0-4][0-9]{3}|[6][5][0-4][0-9]{2}|[6][5][5][0-2][0-9]|[6][5][5][3][0-5])|,([1-9][0-9]{0,3}|[1-5][0-9]{4}|[6][0-4][0-9]{3}|[6][5][0-4][0-9]{2}|[6][5][5][0-2][0-9]|[6][5][5][3][0-5])-([1-9][0-9]{0,3}|[1-5][0-9]{4}|[6][0-4][0-9]{3}|[6][5][0-4][0-9]{2}|[6][5][5][0-2][0-9]|[6][5][5][3][0-5])){0,14}") 11 | validatePortRange = validation.StringMatch(portRangeRegexp, "invalid port range") 12 | ) 13 | -------------------------------------------------------------------------------- /internal/provider/provider_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "math" 8 | "net" 9 | "os" 10 | "sync" 11 | "testing" 12 | 13 | "github.com/apparentlymart/go-cidr/cidr" 14 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 15 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 16 | "github.com/hashicorp/terraform-plugin-testing/terraform" 17 | "github.com/testcontainers/testcontainers-go" 18 | "github.com/testcontainers/testcontainers-go/modules/compose" 19 | "github.com/ubiquiti-community/go-unifi/unifi" 20 | ) 21 | 22 | var providerFactories = map[string]func() (*schema.Provider, error){ 23 | "unifi": func() (*schema.Provider, error) { 24 | return New("acctest")(), nil 25 | }, 26 | } 27 | 28 | var testClient *unifi.Client 29 | 30 | func TestMain(m *testing.M) { 31 | if os.Getenv("TF_ACC") == "" { 32 | // short circuit non acceptance test runs 33 | os.Exit(m.Run()) 34 | } 35 | 36 | os.Exit(runAcceptanceTests(m)) 37 | } 38 | 39 | func runAcceptanceTests(m *testing.M) int { 40 | dc, err := compose.NewDockerCompose("../../docker-compose.yaml") 41 | if err != nil { 42 | panic(err) 43 | } 44 | 45 | ctx, cancel := context.WithCancel(context.Background()) 46 | defer cancel() 47 | 48 | if err = dc.WithOsEnv().Up(ctx, compose.Wait(true)); err != nil { 49 | panic(err) 50 | } 51 | 52 | defer func() { 53 | if err := dc.Down(context.Background(), compose.RemoveOrphans(true), compose.RemoveImagesLocal); err != nil { 54 | panic(err) 55 | } 56 | }() 57 | 58 | container, err := dc.ServiceContainer(ctx, "unifi") 59 | if err != nil { 60 | panic(err) 61 | } 62 | 63 | // Dump the container logs on exit. 64 | // 65 | // TODO: Use https://pkg.go.dev/github.com/testcontainers/testcontainers-go#LogConsumer instead. 66 | defer func() { 67 | if os.Getenv("UNIFI_STDOUT") == "" { 68 | return 69 | } 70 | 71 | stream, err := container.Logs(ctx) 72 | if err != nil { 73 | panic(err) 74 | } 75 | 76 | buffer := new(bytes.Buffer) 77 | buffer.ReadFrom(stream) 78 | testcontainers.Logger.Printf("%s", buffer) 79 | }() 80 | 81 | endpoint, err := container.PortEndpoint(ctx, "8443/tcp", "https") 82 | if err != nil { 83 | panic(err) 84 | } 85 | 86 | const user = "admin" 87 | const password = "admin" 88 | 89 | if err = os.Setenv("UNIFI_USERNAME", user); err != nil { 90 | panic(err) 91 | } 92 | 93 | if err = os.Setenv("UNIFI_PASSWORD", password); err != nil { 94 | panic(err) 95 | } 96 | 97 | if err = os.Setenv("UNIFI_INSECURE", "true"); err != nil { 98 | panic(err) 99 | } 100 | 101 | if err = os.Setenv("UNIFI_API", endpoint); err != nil { 102 | panic(err) 103 | } 104 | 105 | testClient = &unifi.Client{} 106 | setHTTPClient(testClient, true, "unifi") 107 | testClient.SetBaseURL(endpoint) 108 | if err = testClient.Login(ctx, user, password); err != nil { 109 | panic(err) 110 | } 111 | 112 | return m.Run() 113 | } 114 | 115 | func importStep(name string, ignore ...string) resource.TestStep { 116 | step := resource.TestStep{ 117 | ResourceName: name, 118 | ImportState: true, 119 | ImportStateVerify: true, 120 | } 121 | 122 | if len(ignore) > 0 { 123 | step.ImportStateVerifyIgnore = ignore 124 | } 125 | 126 | return step 127 | } 128 | 129 | func siteAndIDImportStateIDFunc(resourceName string) func(*terraform.State) (string, error) { 130 | return func(s *terraform.State) (string, error) { 131 | rs, ok := s.RootModule().Resources[resourceName] 132 | if !ok { 133 | return "", fmt.Errorf("not found: %s", resourceName) 134 | } 135 | networkID := rs.Primary.Attributes["id"] 136 | site := rs.Primary.Attributes["site"] 137 | return site + ":" + networkID, nil 138 | } 139 | } 140 | 141 | func preCheck(t *testing.T) { 142 | variables := []string{ 143 | "UNIFI_USERNAME", 144 | "UNIFI_PASSWORD", 145 | "UNIFI_API", 146 | } 147 | 148 | for _, variable := range variables { 149 | value := os.Getenv(variable) 150 | if value == "" { 151 | t.Fatalf("`%s` must be set for acceptance tests!", variable) 152 | } 153 | } 154 | } 155 | 156 | const ( 157 | vlanMin = 2 158 | vlanMax = 4095 159 | ) 160 | 161 | var ( 162 | network = &net.IPNet{ 163 | IP: net.IPv4(10, 0, 0, 0).To4(), 164 | Mask: net.IPv4Mask(255, 0, 0, 0), 165 | } 166 | 167 | vlanLock sync.Mutex 168 | vlanNext = vlanMin 169 | ) 170 | 171 | func getTestVLAN(t *testing.T) (*net.IPNet, int) { 172 | vlanLock.Lock() 173 | defer vlanLock.Unlock() 174 | 175 | vlan := vlanNext 176 | vlanNext++ 177 | 178 | subnet, err := cidr.Subnet(network, int(math.Ceil(math.Log2(vlanMax))), vlan) 179 | if err != nil { 180 | t.Error(err) 181 | } 182 | 183 | return subnet, vlan 184 | } 185 | -------------------------------------------------------------------------------- /internal/provider/resource_account.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" 9 | "github.com/ubiquiti-community/go-unifi/unifi" 10 | ) 11 | 12 | func resourceAccount() *schema.Resource { 13 | return &schema.Resource{ 14 | Description: "`unifi_account` manages a RADIUS user account\n\n" + 15 | "To authenticate devices based on MAC address, use the MAC address as the username and password under client creation. \n" + 16 | "Convert lowercase letters to uppercase, and also remove colons or periods from the MAC address. \n\n" + 17 | "ATTENTION: If the user profile does not include a VLAN, the client will fall back to the untagged VLAN. \n\n" + 18 | "NOTE: MAC-based authentication accounts can only be used for wireless and wired clients. L2TP remote access does not apply.", 19 | 20 | CreateContext: resourceAccountCreate, 21 | ReadContext: resourceAccountRead, 22 | UpdateContext: resourceAccountUpdate, 23 | DeleteContext: resourceAccountDelete, 24 | Importer: &schema.ResourceImporter{ 25 | StateContext: importSiteAndID, 26 | }, 27 | 28 | Schema: map[string]*schema.Schema{ 29 | "id": { 30 | Description: "The ID of the account.", 31 | Type: schema.TypeString, 32 | Computed: true, 33 | }, 34 | "site": { 35 | Description: "The name of the site to associate the account with.", 36 | Type: schema.TypeString, 37 | Computed: true, 38 | Optional: true, 39 | ForceNew: true, 40 | }, 41 | "name": { 42 | Description: "The name of the account.", 43 | Type: schema.TypeString, 44 | Required: true, 45 | }, 46 | "password": { 47 | Description: "The password of the account.", 48 | Type: schema.TypeString, 49 | Required: true, 50 | Sensitive: true, 51 | }, 52 | "tunnel_type": { 53 | Description: "See [RFC 2868](https://www.rfc-editor.org/rfc/rfc2868) section 3.1", // @TODO: better documentation https://help.ui.com/hc/en-us/articles/360015268353-UniFi-USG-UDM-Configuring-RADIUS-Server#6 54 | Type: schema.TypeInt, 55 | Optional: true, 56 | Default: 13, 57 | ValidateFunc: validation.IntBetween(1, 13), 58 | }, 59 | "tunnel_medium_type": { 60 | Description: "See [RFC 2868](https://www.rfc-editor.org/rfc/rfc2868) section 3.2", // @TODO: better documentation https://help.ui.com/hc/en-us/articles/360015268353-UniFi-USG-UDM-Configuring-RADIUS-Server#6 61 | Type: schema.TypeInt, 62 | Optional: true, 63 | Default: 6, 64 | ValidateFunc: validation.IntBetween(1, 15), 65 | }, 66 | "network_id": { 67 | Description: "ID of the network for this account", 68 | Type: schema.TypeString, 69 | Optional: true, 70 | }, 71 | }, 72 | } 73 | } 74 | 75 | func resourceAccountCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 76 | c := meta.(*client) 77 | 78 | req, err := resourceAccountGetResourceData(d) 79 | if err != nil { 80 | return diag.FromErr(err) 81 | } 82 | 83 | site := d.Get("site").(string) 84 | if site == "" { 85 | site = c.site 86 | } 87 | 88 | resp, err := c.c.CreateAccount(ctx, site, req) 89 | if err != nil { 90 | return diag.FromErr(err) 91 | } 92 | 93 | d.SetId(resp.ID) 94 | 95 | return resourceAccountSetResourceData(resp, d, site) 96 | } 97 | 98 | func resourceAccountUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 99 | c := meta.(*client) 100 | 101 | site := d.Get("site").(string) 102 | if site == "" { 103 | site = c.site 104 | } 105 | 106 | req, err := resourceAccountGetResourceData(d) 107 | if err != nil { 108 | return diag.FromErr(err) 109 | } 110 | 111 | req.ID = d.Id() 112 | req.SiteID = site 113 | 114 | resp, err := c.c.UpdateAccount(ctx, site, req) 115 | if err != nil { 116 | return diag.FromErr(err) 117 | } 118 | 119 | return resourceAccountSetResourceData(resp, d, site) 120 | } 121 | 122 | func resourceAccountDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 123 | c := meta.(*client) 124 | 125 | //name := d.Get("name").(string) 126 | site := d.Get("site").(string) 127 | if site == "" { 128 | site = c.site 129 | } 130 | 131 | id := d.Id() 132 | err := c.c.DeleteAccount(ctx, site, id) 133 | if _, ok := err.(*unifi.NotFoundError); ok { 134 | return nil 135 | } 136 | return diag.FromErr(err) 137 | } 138 | 139 | func resourceAccountRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 140 | c := meta.(*client) 141 | 142 | id := d.Id() 143 | 144 | site := d.Get("site").(string) 145 | if site == "" { 146 | site = c.site 147 | } 148 | 149 | resp, err := c.c.GetAccount(ctx, site, id) 150 | if _, ok := err.(*unifi.NotFoundError); ok { 151 | d.SetId("") 152 | return nil 153 | } 154 | if err != nil { 155 | return diag.FromErr(err) 156 | } 157 | 158 | return resourceAccountSetResourceData(resp, d, site) 159 | } 160 | 161 | func resourceAccountSetResourceData(resp *unifi.Account, d *schema.ResourceData, site string) diag.Diagnostics { 162 | d.Set("site", site) 163 | d.Set("name", resp.Name) 164 | d.Set("password", resp.XPassword) 165 | d.Set("tunnel_type", resp.TunnelType) 166 | d.Set("tunnel_medium_type", resp.TunnelMediumType) 167 | d.Set("network_id", resp.NetworkID) 168 | return nil 169 | } 170 | 171 | func resourceAccountGetResourceData(d *schema.ResourceData) (*unifi.Account, error) { 172 | return &unifi.Account{ 173 | Name: d.Get("name").(string), 174 | XPassword: d.Get("password").(string), 175 | TunnelType: d.Get("tunnel_type").(int), 176 | TunnelMediumType: d.Get("tunnel_medium_type").(int), 177 | NetworkID: d.Get("network_id").(string), 178 | }, nil 179 | } 180 | -------------------------------------------------------------------------------- /internal/provider/resource_account_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 8 | ) 9 | 10 | func TestAccAccount_basic(t *testing.T) { 11 | resource.ParallelTest(t, resource.TestCase{ 12 | PreCheck: func() { preCheck(t) }, 13 | ProviderFactories: providerFactories, 14 | // TODO: CheckDestroy: , 15 | Steps: []resource.TestStep{ 16 | { 17 | Config: testAccAccountConfig("tfacc", "secure"), 18 | Check: resource.ComposeTestCheckFunc( 19 | // testCheckNetworkExists(t, "name"), 20 | resource.TestCheckResourceAttr("unifi_account.test", "name", "tfacc"), 21 | ), 22 | }, 23 | importStep("unifi_account.test"), 24 | }, 25 | }) 26 | } 27 | 28 | func TestAccAccount_mac(t *testing.T) { 29 | resource.ParallelTest(t, resource.TestCase{ 30 | PreCheck: func() { preCheck(t) }, 31 | ProviderFactories: providerFactories, 32 | // TODO: CheckDestroy: , 33 | Steps: []resource.TestStep{ 34 | { 35 | Config: testAccAccountConfig("00B0D06FC226", "00B0D06FC226"), 36 | Check: resource.ComposeTestCheckFunc( 37 | // testCheckNetworkExists(t, "name"), 38 | resource.TestCheckResourceAttr("unifi_account.test", "name", "00B0D06FC226"), 39 | resource.TestCheckResourceAttr("unifi_account.test", "password", "00B0D06FC226"), 40 | ), 41 | }, 42 | importStep("unifi_account.test"), 43 | }, 44 | }) 45 | } 46 | 47 | func testAccAccountConfig(name, password string) string { 48 | return fmt.Sprintf(` 49 | resource "unifi_account" "test" { 50 | name = "%s" 51 | password = "%s" 52 | } 53 | `, name, password) 54 | } 55 | -------------------------------------------------------------------------------- /internal/provider/resource_dns_record.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "slices" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 9 | "github.com/ubiquiti-community/go-unifi/unifi" 10 | ) 11 | 12 | func resourceDNSRecord() *schema.Resource { 13 | return &schema.Resource{ 14 | Description: "`unifi_dns_record` manages DNS record settings for different providers.", 15 | 16 | CreateContext: resourceDNSRecordCreate, 17 | ReadContext: resourceDNSRecordRead, 18 | UpdateContext: resourceDNSRecordUpdate, 19 | DeleteContext: resourceDNSRecordDelete, 20 | Importer: &schema.ResourceImporter{ 21 | StateContext: importSiteAndID, 22 | }, 23 | 24 | Schema: map[string]*schema.Schema{ 25 | "id": { 26 | Description: "The ID of the DNS record.", 27 | Type: schema.TypeString, 28 | Computed: true, 29 | }, 30 | "site": { 31 | Description: "The name of the site to associate the DNS record with.", 32 | Type: schema.TypeString, 33 | Computed: true, 34 | Optional: true, 35 | ForceNew: true, 36 | }, 37 | "name": { 38 | Description: "The key of the DNS record.", 39 | Type: schema.TypeString, 40 | Required: true, 41 | ForceNew: true, 42 | }, 43 | "enabled": { 44 | Description: "Whether the DNS record is enabled.", 45 | Type: schema.TypeBool, 46 | Optional: true, 47 | Default: true, 48 | ForceNew: false, 49 | }, 50 | "port": { 51 | Description: "The port of the DNS record.", 52 | Type: schema.TypeInt, 53 | Required: true, 54 | }, 55 | "priority": { 56 | Description: "The priority of the DNS record.", 57 | Type: schema.TypeInt, 58 | Optional: true, 59 | }, 60 | "record_type": { 61 | Description: "The type of the DNS record.", 62 | Type: schema.TypeString, 63 | Optional: true, 64 | }, 65 | "ttl": { 66 | Description: "The TTL of the DNS record.", 67 | Type: schema.TypeInt, 68 | Optional: true, 69 | }, 70 | "value": { 71 | Description: "The value of the DNS record.", 72 | Type: schema.TypeString, 73 | Required: true, 74 | }, 75 | "weight": { 76 | Description: "The weight of the DNS record.", 77 | Type: schema.TypeInt, 78 | Optional: true, 79 | }, 80 | }, 81 | } 82 | } 83 | 84 | func resourceDNSRecordCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 85 | c := meta.(*client) 86 | 87 | req, err := resourceDNSRecordGetResourceData(d) 88 | if err != nil { 89 | return diag.FromErr(err) 90 | } 91 | 92 | site := d.Get("site").(string) 93 | if site == "" { 94 | site = c.site 95 | } 96 | 97 | resp, err := c.c.CreateDNSRecord(ctx, site, req) 98 | if err != nil { 99 | return diag.FromErr(err) 100 | } 101 | 102 | d.SetId(resp.ID) 103 | 104 | return resourceDNSRecordSetResourceData(resp, d, site) 105 | } 106 | 107 | func resourceDNSRecordGetResourceData(d *schema.ResourceData) (*unifi.DNSRecord, error) { 108 | r := &unifi.DNSRecord{ 109 | Enabled: d.Get("enabled").(bool), 110 | Key: d.Get("name").(string), 111 | Port: d.Get("port").(int), 112 | Priority: d.Get("priority").(int), 113 | RecordType: d.Get("record_type").(string), 114 | Ttl: d.Get("ttl").(int), 115 | Value: d.Get("value").(string), 116 | Weight: d.Get("weight").(int), 117 | } 118 | 119 | return r, nil 120 | } 121 | 122 | func resourceDNSRecordSetResourceData(resp *unifi.DNSRecord, d *schema.ResourceData, site string) diag.Diagnostics { 123 | d.Set("enabled", resp.Enabled) 124 | d.Set("name", resp.Key) 125 | d.Set("port", resp.Port) 126 | d.Set("priority", resp.Priority) 127 | d.Set("record_type", resp.RecordType) 128 | d.Set("ttl", resp.Ttl) 129 | d.Set("value", resp.Value) 130 | d.Set("weight", resp.Weight) 131 | d.Set("site", site) 132 | 133 | return nil 134 | } 135 | 136 | func resourceDNSRecordRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 137 | c := meta.(*client) 138 | 139 | id := d.Id() 140 | 141 | site := d.Get("site").(string) 142 | if site == "" { 143 | site = c.site 144 | } 145 | 146 | resp, err := c.c.ListDNSRecord(ctx, site) 147 | 148 | if err != nil { 149 | return diag.FromErr(err) 150 | } 151 | 152 | i := slices.IndexFunc(resp, func(r unifi.DNSRecord) bool { 153 | return r.ID == id 154 | }) 155 | 156 | if i == -1 { 157 | d.SetId("") 158 | return nil 159 | } 160 | 161 | rec := resp[i] 162 | 163 | return resourceDNSRecordSetResourceData(&rec, d, site) 164 | } 165 | 166 | func resourceDNSRecordUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 167 | c := meta.(*client) 168 | 169 | req, err := resourceDNSRecordGetResourceData(d) 170 | if err != nil { 171 | return diag.FromErr(err) 172 | } 173 | 174 | req.ID = d.Id() 175 | 176 | site := d.Get("site").(string) 177 | if site == "" { 178 | site = c.site 179 | } 180 | req.SiteID = site 181 | 182 | resp, err := c.c.UpdateDNSRecord(ctx, site, req) 183 | if err != nil { 184 | return diag.FromErr(err) 185 | } 186 | 187 | return resourceDNSRecordSetResourceData(resp, d, site) 188 | } 189 | 190 | func resourceDNSRecordDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 191 | c := meta.(*client) 192 | 193 | id := d.Id() 194 | 195 | site := d.Get("site").(string) 196 | if site == "" { 197 | site = c.site 198 | } 199 | err := c.c.DeleteDNSRecord(ctx, site, id) 200 | if _, ok := err.(*unifi.NotFoundError); ok { 201 | return nil 202 | } 203 | return diag.FromErr(err) 204 | } 205 | -------------------------------------------------------------------------------- /internal/provider/resource_dns_record_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 7 | ) 8 | 9 | func TestAccDNSRecord_default(t *testing.T) { 10 | resource.ParallelTest(t, resource.TestCase{ 11 | PreCheck: func() { preCheck(t) }, 12 | ProviderFactories: providerFactories, 13 | // TODO: CheckDestroy: , 14 | Steps: []resource.TestStep{ 15 | { 16 | Config: testAccDNSRecordConfig, 17 | // Check: resource.ComposeTestCheckFunc( 18 | // // testCheckFirewallGroupExists(t, "name"), 19 | // ), 20 | }, 21 | importStep("unifi_dns_record.test"), 22 | }, 23 | }) 24 | } 25 | 26 | const testAccDNSRecordConfig = ` 27 | resource "unifi_dns_record" "test" { 28 | service = "default" 29 | 30 | host_name = "test.example.com" 31 | 32 | server = "default.example.com" 33 | login = "testuser" 34 | password = "password" 35 | } 36 | ` 37 | -------------------------------------------------------------------------------- /internal/provider/resource_dynamic_dns.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | "github.com/ubiquiti-community/go-unifi/unifi" 9 | ) 10 | 11 | func resourceDynamicDNS() *schema.Resource { 12 | return &schema.Resource{ 13 | Description: "`unifi_dynamic_dns` manages dynamic DNS settings for different providers.", 14 | 15 | CreateContext: resourceDynamicDNSCreate, 16 | ReadContext: resourceDynamicDNSRead, 17 | UpdateContext: resourceDynamicDNSUpdate, 18 | DeleteContext: resourceDynamicDNSDelete, 19 | Importer: &schema.ResourceImporter{ 20 | StateContext: importSiteAndID, 21 | }, 22 | 23 | Schema: map[string]*schema.Schema{ 24 | "id": { 25 | Description: "The ID of the dynamic DNS.", 26 | Type: schema.TypeString, 27 | Computed: true, 28 | }, 29 | "site": { 30 | Description: "The name of the site to associate the dynamic DNS with.", 31 | Type: schema.TypeString, 32 | Computed: true, 33 | Optional: true, 34 | ForceNew: true, 35 | }, 36 | "interface": { 37 | Description: "The interface for the dynamic DNS. Can be `wan` or `wan2`.", 38 | Type: schema.TypeString, 39 | Optional: true, 40 | Default: "wan", 41 | ForceNew: true, 42 | }, 43 | "service": { 44 | Description: "The Dynamic DNS service provider, various values are supported (for example `dyndns`, etc.).", 45 | Type: schema.TypeString, 46 | Required: true, 47 | ForceNew: true, 48 | }, 49 | "host_name": { 50 | Description: "The host name to update in the dynamic DNS service.", 51 | Type: schema.TypeString, 52 | Required: true, 53 | }, 54 | "server": { 55 | Description: "The server for the dynamic DNS service.", 56 | Type: schema.TypeString, 57 | Optional: true, 58 | }, 59 | "login": { 60 | Description: "The server for the dynamic DNS service.", 61 | Type: schema.TypeString, 62 | Optional: true, 63 | }, 64 | "password": { 65 | Description: "The server for the dynamic DNS service.", 66 | Type: schema.TypeString, 67 | Optional: true, 68 | Sensitive: true, 69 | }, 70 | 71 | //TODO: options support? 72 | }, 73 | } 74 | } 75 | 76 | func resourceDynamicDNSCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 77 | c := meta.(*client) 78 | 79 | req, err := resourceDynamicDNSGetResourceData(d) 80 | if err != nil { 81 | return diag.FromErr(err) 82 | } 83 | 84 | site := d.Get("site").(string) 85 | if site == "" { 86 | site = c.site 87 | } 88 | 89 | resp, err := c.c.CreateDynamicDNS(ctx, site, req) 90 | if err != nil { 91 | return diag.FromErr(err) 92 | } 93 | 94 | d.SetId(resp.ID) 95 | 96 | return resourceDynamicDNSSetResourceData(resp, d, site) 97 | } 98 | 99 | func resourceDynamicDNSGetResourceData(d *schema.ResourceData) (*unifi.DynamicDNS, error) { 100 | r := &unifi.DynamicDNS{ 101 | Interface: d.Get("interface").(string), 102 | Service: d.Get("service").(string), 103 | 104 | HostName: d.Get("host_name").(string), 105 | 106 | Server: d.Get("server").(string), 107 | Login: d.Get("login").(string), 108 | XPassword: d.Get("password").(string), 109 | } 110 | 111 | return r, nil 112 | } 113 | 114 | func resourceDynamicDNSSetResourceData(resp *unifi.DynamicDNS, d *schema.ResourceData, site string) diag.Diagnostics { 115 | d.Set("interface", resp.Interface) 116 | d.Set("service", resp.Service) 117 | 118 | d.Set("host_name", resp.HostName) 119 | 120 | d.Set("server", resp.Server) 121 | d.Set("login", resp.Login) 122 | d.Set("password", resp.XPassword) 123 | 124 | return nil 125 | } 126 | 127 | func resourceDynamicDNSRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 128 | c := meta.(*client) 129 | 130 | id := d.Id() 131 | 132 | site := d.Get("site").(string) 133 | if site == "" { 134 | site = c.site 135 | } 136 | 137 | resp, err := c.c.GetDynamicDNS(ctx, site, id) 138 | if _, ok := err.(*unifi.NotFoundError); ok { 139 | d.SetId("") 140 | return nil 141 | } 142 | if err != nil { 143 | return diag.FromErr(err) 144 | } 145 | 146 | return resourceDynamicDNSSetResourceData(resp, d, site) 147 | } 148 | 149 | func resourceDynamicDNSUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 150 | c := meta.(*client) 151 | 152 | req, err := resourceDynamicDNSGetResourceData(d) 153 | if err != nil { 154 | return diag.FromErr(err) 155 | } 156 | 157 | req.ID = d.Id() 158 | 159 | site := d.Get("site").(string) 160 | if site == "" { 161 | site = c.site 162 | } 163 | req.SiteID = site 164 | 165 | resp, err := c.c.UpdateDynamicDNS(ctx, site, req) 166 | if err != nil { 167 | return diag.FromErr(err) 168 | } 169 | 170 | return resourceDynamicDNSSetResourceData(resp, d, site) 171 | } 172 | 173 | func resourceDynamicDNSDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 174 | c := meta.(*client) 175 | 176 | id := d.Id() 177 | 178 | site := d.Get("site").(string) 179 | if site == "" { 180 | site = c.site 181 | } 182 | err := c.c.DeleteDynamicDNS(ctx, site, id) 183 | if _, ok := err.(*unifi.NotFoundError); ok { 184 | return nil 185 | } 186 | return diag.FromErr(err) 187 | } 188 | -------------------------------------------------------------------------------- /internal/provider/resource_dynamic_dns_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 7 | ) 8 | 9 | func TestAccDynamicDNS_dyndns(t *testing.T) { 10 | resource.ParallelTest(t, resource.TestCase{ 11 | PreCheck: func() { preCheck(t) }, 12 | ProviderFactories: providerFactories, 13 | // TODO: CheckDestroy: , 14 | Steps: []resource.TestStep{ 15 | { 16 | Config: testAccDynamicDNSConfig, 17 | // Check: resource.ComposeTestCheckFunc( 18 | // // testCheckFirewallGroupExists(t, "name"), 19 | // ), 20 | }, 21 | importStep("unifi_dynamic_dns.test"), 22 | }, 23 | }) 24 | } 25 | 26 | const testAccDynamicDNSConfig = ` 27 | resource "unifi_dynamic_dns" "test" { 28 | service = "dyndns" 29 | 30 | host_name = "test.example.com" 31 | 32 | server = "dyndns.example.com" 33 | login = "testuser" 34 | password = "password" 35 | } 36 | ` 37 | -------------------------------------------------------------------------------- /internal/provider/resource_firewall_group.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" 10 | "github.com/ubiquiti-community/go-unifi/unifi" 11 | ) 12 | 13 | func resourceFirewallGroup() *schema.Resource { 14 | return &schema.Resource{ 15 | Description: "`unifi_firewall_group` manages groups of addresses or ports for use in firewall rules (`unifi_firewall_rule`).", 16 | 17 | CreateContext: resourceFirewallGroupCreate, 18 | ReadContext: resourceFirewallGroupRead, 19 | UpdateContext: resourceFirewallGroupUpdate, 20 | DeleteContext: resourceFirewallGroupDelete, 21 | Importer: &schema.ResourceImporter{ 22 | StateContext: importSiteAndID, 23 | }, 24 | 25 | Schema: map[string]*schema.Schema{ 26 | "id": { 27 | Description: "The ID of the firewall group.", 28 | Type: schema.TypeString, 29 | Computed: true, 30 | }, 31 | "site": { 32 | Description: "The name of the site to associate the firewall group with.", 33 | Type: schema.TypeString, 34 | Computed: true, 35 | Optional: true, 36 | ForceNew: true, 37 | }, 38 | "name": { 39 | Description: "The name of the firewall group.", 40 | Type: schema.TypeString, 41 | Required: true, 42 | }, 43 | "type": { 44 | Description: "The type of the firewall group. Must be one of: `address-group`, `port-group`, or `ipv6-address-group`.", 45 | Type: schema.TypeString, 46 | Required: true, 47 | ValidateFunc: validation.StringInSlice([]string{"address-group", "port-group", "ipv6-address-group"}, false), 48 | }, 49 | "members": { 50 | Description: "The members of the firewall group.", 51 | Type: schema.TypeSet, 52 | Required: true, 53 | Elem: &schema.Schema{Type: schema.TypeString}, 54 | }, 55 | }, 56 | } 57 | } 58 | 59 | func resourceFirewallGroupCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 60 | c := meta.(*client) 61 | 62 | req, err := resourceFirewallGroupGetResourceData(d) 63 | if err != nil { 64 | return diag.FromErr(err) 65 | } 66 | 67 | site := d.Get("site").(string) 68 | if site == "" { 69 | site = c.site 70 | } 71 | 72 | resp, err := c.c.CreateFirewallGroup(ctx, site, req) 73 | if err != nil { 74 | var apiErr *unifi.APIError 75 | if errors.As(err, &apiErr) && apiErr.Message == "api.err.FirewallGroupExisted" { 76 | return diag.Errorf("firewall groups must have unique names: %s", err) 77 | } 78 | 79 | return diag.FromErr(err) 80 | } 81 | 82 | d.SetId(resp.ID) 83 | 84 | return resourceFirewallGroupSetResourceData(resp, d, site) 85 | } 86 | 87 | func resourceFirewallGroupGetResourceData(d *schema.ResourceData) (*unifi.FirewallGroup, error) { 88 | members, err := setToStringSlice(d.Get("members").(*schema.Set)) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | return &unifi.FirewallGroup{ 94 | Name: d.Get("name").(string), 95 | GroupType: d.Get("type").(string), 96 | GroupMembers: members, 97 | }, nil 98 | } 99 | 100 | func resourceFirewallGroupSetResourceData(resp *unifi.FirewallGroup, d *schema.ResourceData, site string) diag.Diagnostics { 101 | d.Set("site", site) 102 | d.Set("name", resp.Name) 103 | d.Set("type", resp.GroupType) 104 | d.Set("members", stringSliceToSet(resp.GroupMembers)) 105 | 106 | return nil 107 | } 108 | 109 | func resourceFirewallGroupRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 110 | c := meta.(*client) 111 | 112 | id := d.Id() 113 | 114 | site := d.Get("site").(string) 115 | if site == "" { 116 | site = c.site 117 | } 118 | 119 | resp, err := c.c.GetFirewallGroup(ctx, site, id) 120 | if _, ok := err.(*unifi.NotFoundError); ok { 121 | d.SetId("") 122 | return nil 123 | } 124 | if err != nil { 125 | return diag.FromErr(err) 126 | } 127 | 128 | return resourceFirewallGroupSetResourceData(resp, d, site) 129 | } 130 | 131 | func resourceFirewallGroupUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 132 | c := meta.(*client) 133 | 134 | req, err := resourceFirewallGroupGetResourceData(d) 135 | if err != nil { 136 | return diag.FromErr(err) 137 | } 138 | 139 | req.ID = d.Id() 140 | 141 | site := d.Get("site").(string) 142 | if site == "" { 143 | site = c.site 144 | } 145 | req.SiteID = site 146 | 147 | resp, err := c.c.UpdateFirewallGroup(ctx, site, req) 148 | if err != nil { 149 | return diag.FromErr(err) 150 | } 151 | 152 | return resourceFirewallGroupSetResourceData(resp, d, site) 153 | } 154 | 155 | func resourceFirewallGroupDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 156 | c := meta.(*client) 157 | 158 | id := d.Id() 159 | 160 | site := d.Get("site").(string) 161 | if site == "" { 162 | site = c.site 163 | } 164 | 165 | err := c.c.DeleteFirewallGroup(ctx, site, id) 166 | if _, ok := err.(*unifi.NotFoundError); ok { 167 | return nil 168 | } 169 | return diag.FromErr(err) 170 | } 171 | -------------------------------------------------------------------------------- /internal/provider/resource_firewall_group_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 10 | ) 11 | 12 | func TestAccFirewallGroup_port_group(t *testing.T) { 13 | resource.ParallelTest(t, resource.TestCase{ 14 | PreCheck: func() { preCheck(t) }, 15 | ProviderFactories: providerFactories, 16 | // TODO: CheckDestroy: , 17 | Steps: []resource.TestStep{ 18 | { 19 | Config: testAccFirewallGroupConfig("testpg", "port-group", nil), 20 | // Check: resource.ComposeTestCheckFunc( 21 | // // testCheckFirewallGroupExists(t, "name"), 22 | // ), 23 | }, 24 | importStep("unifi_firewall_group.test"), 25 | { 26 | Config: testAccFirewallGroupConfig("testpg", "port-group", []string{"80", "443"}), 27 | }, 28 | importStep("unifi_firewall_group.test"), 29 | }, 30 | }) 31 | } 32 | 33 | func TestAccFirewallGroup_address_group(t *testing.T) { 34 | resource.ParallelTest(t, resource.TestCase{ 35 | PreCheck: func() { preCheck(t) }, 36 | ProviderFactories: providerFactories, 37 | // TODO: CheckDestroy: , 38 | Steps: []resource.TestStep{ 39 | { 40 | Config: testAccFirewallGroupConfig("testag", "address-group", nil), 41 | // Check: resource.ComposeTestCheckFunc( 42 | // // testCheckFirewallGroupExists(t, "name"), 43 | // ), 44 | }, 45 | importStep("unifi_firewall_group.test"), 46 | { 47 | Config: testAccFirewallGroupConfig("testag", "address-group", []string{"10.0.0.1", "10.0.0.2"}), 48 | }, 49 | importStep("unifi_firewall_group.test"), 50 | { 51 | Config: testAccFirewallGroupConfig("testag", "address-group", []string{"10.0.0.0/24"}), 52 | }, 53 | importStep("unifi_firewall_group.test"), 54 | }, 55 | }) 56 | } 57 | 58 | func TestAccFirewallGroup_same_name(t *testing.T) { 59 | resource.ParallelTest(t, resource.TestCase{ 60 | PreCheck: func() { preCheck(t) }, 61 | ProviderFactories: providerFactories, 62 | // TODO: CheckDestroy: , 63 | Steps: []resource.TestStep{ 64 | { 65 | Config: testAccFirewallGroupConfig_same_name, 66 | ExpectError: regexp.MustCompile("firewall groups must have unique names"), 67 | }, 68 | }, 69 | }) 70 | } 71 | 72 | func testAccFirewallGroupConfig(name, ty string, members []string) string { 73 | joined := strings.Join(members, "\",\"") 74 | if len(joined) > 0 { 75 | joined = "\"" + joined + "\"" 76 | } 77 | 78 | return fmt.Sprintf(` 79 | resource "unifi_firewall_group" "test" { 80 | name = "%s" 81 | type = "%s" 82 | 83 | members = [%s] 84 | } 85 | `, name, ty, joined) 86 | } 87 | 88 | const testAccFirewallGroupConfig_same_name = ` 89 | resource "unifi_firewall_group" "test_a" { 90 | name = "tf-acc fg" 91 | type = "address-group" 92 | 93 | members = [] 94 | } 95 | 96 | resource "unifi_firewall_group" "test_b" { 97 | name = "tf-acc fg" 98 | type = "address-group" 99 | 100 | members = [] 101 | } 102 | ` 103 | -------------------------------------------------------------------------------- /internal/provider/resource_port_forward.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" 9 | "github.com/ubiquiti-community/go-unifi/unifi" 10 | ) 11 | 12 | func resourcePortForward() *schema.Resource { 13 | return &schema.Resource{ 14 | Description: "`unifi_port_forward` manages a port forwarding rule on the gateway.", 15 | 16 | CreateContext: resourcePortForwardCreate, 17 | ReadContext: resourcePortForwardRead, 18 | UpdateContext: resourcePortForwardUpdate, 19 | DeleteContext: resourcePortForwardDelete, 20 | Importer: &schema.ResourceImporter{ 21 | StateContext: importSiteAndID, 22 | }, 23 | 24 | Schema: map[string]*schema.Schema{ 25 | "id": { 26 | Description: "The ID of the port forwarding rule.", 27 | Type: schema.TypeString, 28 | Computed: true, 29 | }, 30 | "site": { 31 | Description: "The name of the site to associate the port forwarding rule with.", 32 | Type: schema.TypeString, 33 | Computed: true, 34 | Optional: true, 35 | ForceNew: true, 36 | }, 37 | "dst_port": { 38 | Description: "The destination port for the forwarding.", 39 | Type: schema.TypeString, 40 | Optional: true, 41 | ValidateFunc: validatePortRange, 42 | }, 43 | // TODO: remove this, disabled rules should just be deleted. 44 | "enabled": { 45 | Description: "Specifies whether the port forwarding rule is enabled or not.", 46 | Type: schema.TypeBool, 47 | Default: true, 48 | Optional: true, 49 | Deprecated: "This will attribute will be removed in a future release. Instead of disabling a " + 50 | "port forwarding rule you can remove it from your configuration.", 51 | }, 52 | "fwd_ip": { 53 | Description: "The IPv4 address to forward traffic to.", 54 | Type: schema.TypeString, 55 | Optional: true, 56 | ValidateFunc: validation.IsIPv4Address, 57 | }, 58 | "fwd_port": { 59 | Description: "The port to forward traffic to.", 60 | Type: schema.TypeString, 61 | Optional: true, 62 | ValidateFunc: validatePortRange, 63 | }, 64 | "log": { 65 | Description: "Specifies whether to log forwarded traffic or not.", 66 | Type: schema.TypeBool, 67 | Default: false, 68 | Optional: true, 69 | }, 70 | "name": { 71 | Description: "The name of the port forwarding rule.", 72 | Type: schema.TypeString, 73 | Optional: true, 74 | }, 75 | "port_forward_interface": { 76 | Description: "The port forwarding interface. Can be `wan`, `wan2`, or `both`.", 77 | Type: schema.TypeString, 78 | Optional: true, 79 | ValidateFunc: validation.StringInSlice([]string{"wan", "wan2", "both"}, false), 80 | }, 81 | "protocol": { 82 | Description: "The protocol for the port forwarding rule. Can be `tcp`, `udp`, or `tcp_udp`.", 83 | Type: schema.TypeString, 84 | Optional: true, 85 | Default: "tcp_udp", 86 | ValidateFunc: validation.StringInSlice([]string{"tcp_udp", "tcp", "udp"}, false), 87 | }, 88 | "src_ip": { 89 | Description: "The source IPv4 address (or CIDR) of the port forwarding rule. For all traffic, specify `any`.", 90 | Type: schema.TypeString, 91 | Optional: true, 92 | Default: "any", 93 | ValidateFunc: validation.Any( 94 | validation.StringInSlice([]string{"any"}, false), 95 | validation.IsIPv4Address, 96 | cidrValidate, 97 | ), 98 | }, 99 | }, 100 | } 101 | } 102 | 103 | func resourcePortForwardCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 104 | c := meta.(*client) 105 | 106 | req, err := resourcePortForwardGetResourceData(d) 107 | if err != nil { 108 | return diag.FromErr(err) 109 | } 110 | 111 | site := d.Get("site").(string) 112 | if site == "" { 113 | site = c.site 114 | } 115 | resp, err := c.c.CreatePortForward(ctx, site, req) 116 | if err != nil { 117 | return diag.FromErr(err) 118 | } 119 | 120 | d.SetId(resp.ID) 121 | 122 | return resourcePortForwardSetResourceData(resp, d, site) 123 | } 124 | 125 | func resourcePortForwardGetResourceData(d *schema.ResourceData) (*unifi.PortForward, error) { 126 | return &unifi.PortForward{ 127 | DstPort: d.Get("dst_port").(string), 128 | Enabled: d.Get("enabled").(bool), 129 | Fwd: d.Get("fwd_ip").(string), 130 | FwdPort: d.Get("fwd_port").(string), 131 | Log: d.Get("log").(bool), 132 | Name: d.Get("name").(string), 133 | PfwdInterface: d.Get("port_forward_interface").(string), 134 | Proto: d.Get("protocol").(string), 135 | Src: d.Get("src_ip").(string), 136 | }, nil 137 | } 138 | 139 | func resourcePortForwardSetResourceData(resp *unifi.PortForward, d *schema.ResourceData, site string) diag.Diagnostics { 140 | d.Set("site", site) 141 | d.Set("dst_port", resp.DstPort) 142 | d.Set("enabled", resp.Enabled) 143 | d.Set("fwd_ip", resp.Fwd) 144 | d.Set("fwd_port", resp.FwdPort) 145 | d.Set("log", resp.Log) 146 | d.Set("name", resp.Name) 147 | d.Set("port_forward_interface", resp.PfwdInterface) 148 | d.Set("protocol", resp.Proto) 149 | d.Set("src_ip", resp.Src) 150 | 151 | return nil 152 | } 153 | 154 | func resourcePortForwardRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 155 | c := meta.(*client) 156 | 157 | id := d.Id() 158 | 159 | site := d.Get("site").(string) 160 | if site == "" { 161 | site = c.site 162 | } 163 | resp, err := c.c.GetPortForward(ctx, site, id) 164 | if _, ok := err.(*unifi.NotFoundError); ok { 165 | d.SetId("") 166 | return nil 167 | } 168 | if err != nil { 169 | return diag.FromErr(err) 170 | } 171 | 172 | return resourcePortForwardSetResourceData(resp, d, site) 173 | } 174 | 175 | func resourcePortForwardUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 176 | c := meta.(*client) 177 | 178 | req, err := resourcePortForwardGetResourceData(d) 179 | if err != nil { 180 | return diag.FromErr(err) 181 | } 182 | 183 | req.ID = d.Id() 184 | 185 | site := d.Get("site").(string) 186 | if site == "" { 187 | site = c.site 188 | } 189 | req.SiteID = site 190 | 191 | resp, err := c.c.UpdatePortForward(ctx, site, req) 192 | if err != nil { 193 | return diag.FromErr(err) 194 | } 195 | 196 | return resourcePortForwardSetResourceData(resp, d, site) 197 | } 198 | 199 | func resourcePortForwardDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 200 | c := meta.(*client) 201 | 202 | id := d.Id() 203 | 204 | site := d.Get("site").(string) 205 | if site == "" { 206 | site = c.site 207 | } 208 | 209 | err := c.c.DeletePortForward(ctx, site, id) 210 | return diag.FromErr(err) 211 | } 212 | -------------------------------------------------------------------------------- /internal/provider/resource_port_forward_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 8 | ) 9 | 10 | func TestAccPortForward_basic(t *testing.T) { 11 | resource.ParallelTest(t, resource.TestCase{ 12 | PreCheck: func() { preCheck(t) }, 13 | ProviderFactories: providerFactories, 14 | // TODO: CheckDestroy: , 15 | Steps: []resource.TestStep{ 16 | { 17 | Config: testAccPortForwardConfig("22", false, "10.1.1.1", "22", "fwd name"), 18 | Check: resource.ComposeTestCheckFunc( 19 | // testCheckNetworkExists(t, "name"), 20 | resource.TestCheckResourceAttr("unifi_port_forward.test", "dst_port", "22"), 21 | ), 22 | }, 23 | importStep("unifi_port_forward.test"), 24 | { 25 | Config: testAccPortForwardConfig("22", false, "10.1.1.2", "8022", "fwd name"), 26 | Check: resource.ComposeTestCheckFunc( 27 | resource.TestCheckResourceAttr("unifi_port_forward.test", "fwd_port", "8022"), 28 | resource.TestCheckResourceAttr("unifi_port_forward.test", "fwd_ip", "10.1.1.2"), 29 | ), 30 | }, 31 | importStep("unifi_port_forward.test"), 32 | { 33 | Config: testAccPortForwardConfig("22", false, "10.1.1.1", "22", "fwd name 2"), 34 | Check: resource.ComposeTestCheckFunc( 35 | resource.TestCheckResourceAttr("unifi_port_forward.test", "name", "fwd name 2"), 36 | ), 37 | }, 38 | importStep("unifi_port_forward.test"), 39 | }, 40 | }) 41 | } 42 | 43 | func TestAccPortForward_src_ip(t *testing.T) { 44 | resource.ParallelTest(t, resource.TestCase{ 45 | PreCheck: func() { preCheck(t) }, 46 | ProviderFactories: providerFactories, 47 | // TODO: CheckDestroy: , 48 | Steps: []resource.TestStep{ 49 | { 50 | Config: testAccPortForwardConfigSrc("22", false, "10.1.1.1", "22", "fwd name", "192.168.1.0"), 51 | Check: resource.ComposeTestCheckFunc( 52 | // testCheckNetworkExists(t, "name"), 53 | resource.TestCheckResourceAttr("unifi_port_forward.test", "dst_port", "22"), 54 | ), 55 | }, 56 | importStep("unifi_port_forward.test"), 57 | }, 58 | }) 59 | } 60 | 61 | func TestAccPortForward_src_cidr(t *testing.T) { 62 | resource.ParallelTest(t, resource.TestCase{ 63 | PreCheck: func() { preCheck(t) }, 64 | ProviderFactories: providerFactories, 65 | // TODO: CheckDestroy: , 66 | Steps: []resource.TestStep{ 67 | { 68 | Config: testAccPortForwardConfigSrc("22", false, "10.1.1.1", "22", "fwd name", "192.168.1.0/20"), 69 | Check: resource.ComposeTestCheckFunc( 70 | // testCheckNetworkExists(t, "name"), 71 | resource.TestCheckResourceAttr("unifi_port_forward.test", "dst_port", "22"), 72 | ), 73 | }, 74 | importStep("unifi_port_forward.test"), 75 | }, 76 | }) 77 | } 78 | 79 | func testAccPortForwardConfig(dstPort string, enabled bool, fwdIP, fwdPort, name string) string { 80 | return fmt.Sprintf(` 81 | resource "unifi_port_forward" "test" { 82 | dst_port = %q 83 | enabled = %t 84 | fwd_ip = %q 85 | fwd_port = %q 86 | name = %q 87 | } 88 | `, dstPort, enabled, fwdIP, fwdPort, name) 89 | } 90 | 91 | func testAccPortForwardConfigSrc(dstPort string, enabled bool, fwdIP, fwdPort, name, src string) string { 92 | return fmt.Sprintf(` 93 | resource "unifi_port_forward" "test" { 94 | dst_port = %q 95 | enabled = %t 96 | fwd_ip = %q 97 | fwd_port = %q 98 | name = %q 99 | src_ip = %q 100 | } 101 | `, dstPort, enabled, fwdIP, fwdPort, name, src) 102 | } 103 | -------------------------------------------------------------------------------- /internal/provider/resource_port_profile_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 7 | ) 8 | 9 | func TestAccPortProfile_basic(t *testing.T) { 10 | resource.ParallelTest(t, resource.TestCase{ 11 | PreCheck: func() { preCheck(t) }, 12 | ProviderFactories: providerFactories, 13 | // TODO: CheckDestroy: , 14 | Steps: []resource.TestStep{ 15 | { 16 | Config: testAccPortProfileConfig, 17 | Check: resource.ComposeTestCheckFunc( 18 | resource.TestCheckResourceAttr("unifi_port_profile.test", "poe_mode", "off"), 19 | ), 20 | }, 21 | importStep("unifi_port_profile.test"), 22 | }, 23 | }) 24 | } 25 | 26 | const testAccPortProfileConfig = ` 27 | resource "unifi_port_profile" "test" { 28 | name = "provider created" 29 | 30 | poe_mode = "off" 31 | speed = 1000 32 | stp_port_mode = false 33 | } 34 | ` 35 | -------------------------------------------------------------------------------- /internal/provider/resource_radius_profile_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 8 | ) 9 | 10 | func TestAccRadiusProfile_basic(t *testing.T) { 11 | resource.ParallelTest(t, resource.TestCase{ 12 | PreCheck: func() { preCheck(t) }, 13 | ProviderFactories: providerFactories, 14 | // TODO: CheckDestroy: , 15 | Steps: []resource.TestStep{ 16 | { 17 | Config: testAccRadiusProfileConfig("test"), 18 | Check: resource.ComposeTestCheckFunc( 19 | resource.TestCheckResourceAttr("unifi_radius_profile.test", "name", "test"), 20 | ), 21 | }, 22 | importStep("unifi_radius_profile.test"), 23 | }, 24 | }) 25 | } 26 | 27 | func TestAccRadiusProfile_servers(t *testing.T) { 28 | resource.ParallelTest(t, resource.TestCase{ 29 | PreCheck: func() { preCheck(t) }, 30 | ProviderFactories: providerFactories, 31 | // TODO: CheckDestroy: , 32 | Steps: []resource.TestStep{ 33 | { 34 | Config: testAccRadiusProfileConfigServer(), 35 | Check: resource.ComposeTestCheckFunc( 36 | resource.TestCheckResourceAttr("unifi_radius_profile.test", "name", "test"), 37 | ), 38 | }, 39 | importStep("unifi_radius_profile.test"), 40 | }, 41 | }) 42 | } 43 | 44 | func TestAccRadiusProfile_importByName(t *testing.T) { 45 | resource.ParallelTest(t, resource.TestCase{ 46 | PreCheck: func() { preCheck(t) }, 47 | ProviderFactories: providerFactories, 48 | Steps: []resource.TestStep{ 49 | // Apply and import network by name. 50 | { 51 | Config: testAccRadiusProfileImport(), 52 | }, 53 | { 54 | Config: testAccRadiusProfileImport(), 55 | ResourceName: "unifi_radius_profile.test", 56 | ImportState: true, 57 | ImportStateVerify: true, 58 | ImportStateId: "name=imported", 59 | }, 60 | }, 61 | }) 62 | } 63 | 64 | func testAccRadiusProfileConfigServer() string { 65 | return ` 66 | resource "unifi_radius_profile" "test" { 67 | name = "test" 68 | auth_server { 69 | ip = "192.168.1.1" 70 | xsecret = "securepw1" 71 | } 72 | auth_server { 73 | ip = "192.168.10.1" 74 | port = 8888 75 | xsecret = "securepw2" 76 | } 77 | acct_server { 78 | ip = "192.168.1.1" 79 | xsecret = "securepw1" 80 | } 81 | acct_server { 82 | ip = "192.168.10.1" 83 | port = 9999 84 | xsecret = "securepw2" 85 | } 86 | use_usg_acct_server = false 87 | use_usg_auth_server = false 88 | } 89 | ` 90 | } 91 | 92 | func testAccRadiusProfileConfig(name string) string { 93 | return fmt.Sprintf(` 94 | resource "unifi_radius_profile" "test" { 95 | name = "%[1]s" 96 | } 97 | `, name) 98 | } 99 | 100 | func testAccRadiusProfileImport() string { 101 | return ` 102 | resource "unifi_radius_profile" "test" { 103 | name = "imported" 104 | auth_server { 105 | ip = "192.168.1.1" 106 | port = 1812 107 | xsecret = "securepw" 108 | } 109 | use_usg_auth_server = true 110 | vlan_enabled = true 111 | vlan_wlan_mode = "required" 112 | } 113 | ` 114 | } 115 | -------------------------------------------------------------------------------- /internal/provider/resource_setting_mgmt.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 9 | "github.com/ubiquiti-community/go-unifi/unifi" 10 | ) 11 | 12 | // TODO: probably need to update this to be more like setting_usg, 13 | // using locking, and upsert, more computed, etc. 14 | 15 | func resourceSettingMgmt() *schema.Resource { 16 | return &schema.Resource{ 17 | Description: "`unifi_setting_mgmt` manages settings for a unifi site.", 18 | 19 | CreateContext: resourceSettingMgmtCreate, 20 | ReadContext: resourceSettingMgmtRead, 21 | UpdateContext: resourceSettingMgmtUpdate, 22 | DeleteContext: resourceSettingMgmtDelete, 23 | Importer: &schema.ResourceImporter{ 24 | StateContext: importSiteAndID, 25 | }, 26 | 27 | Schema: map[string]*schema.Schema{ 28 | "id": { 29 | Description: "The ID of the settings.", 30 | Type: schema.TypeString, 31 | Computed: true, 32 | }, 33 | "site": { 34 | Description: "The name of the site to associate the settings with.", 35 | Type: schema.TypeString, 36 | Computed: true, 37 | Optional: true, 38 | ForceNew: true, 39 | }, 40 | "auto_upgrade": { 41 | Description: "Automatically upgrade device firmware.", 42 | Type: schema.TypeBool, 43 | Optional: true, 44 | }, 45 | "ssh_enabled": { 46 | Description: "Enable SSH authentication.", 47 | Type: schema.TypeBool, 48 | Optional: true, 49 | }, 50 | "ssh_key": { 51 | Description: "SSH key.", 52 | Type: schema.TypeSet, 53 | Optional: true, 54 | Elem: &schema.Resource{ 55 | Schema: map[string]*schema.Schema{ 56 | "name": { 57 | Description: "Name of SSH key.", 58 | Type: schema.TypeString, 59 | Required: true, 60 | }, 61 | "type": { 62 | Description: "Type of SSH key, e.g. ssh-rsa.", 63 | Type: schema.TypeString, 64 | Required: true, 65 | }, 66 | "key": { 67 | Description: "Public SSH key.", 68 | Type: schema.TypeString, 69 | Optional: true, 70 | }, 71 | "comment": { 72 | Description: "Comment.", 73 | Type: schema.TypeString, 74 | Optional: true, 75 | }, 76 | }, 77 | }, 78 | }, 79 | }, 80 | } 81 | } 82 | 83 | func setToSshKeys(set *schema.Set) ([]unifi.SettingMgmtXSshKeys, error) { 84 | var sshKeys []unifi.SettingMgmtXSshKeys 85 | for _, item := range set.List() { 86 | data, ok := item.(map[string]any) 87 | if !ok { 88 | return nil, fmt.Errorf("unexpected data in block") 89 | } 90 | sshKey, err := toSshKey(data) 91 | if err != nil { 92 | return nil, fmt.Errorf("unable to create port override: %w", err) 93 | } 94 | sshKeys = append(sshKeys, sshKey) 95 | } 96 | return sshKeys, nil 97 | } 98 | 99 | func toSshKey(data map[string]any) (unifi.SettingMgmtXSshKeys, error) { 100 | return unifi.SettingMgmtXSshKeys{ 101 | Name: data["name"].(string), 102 | KeyType: data["type"].(string), 103 | Key: data["key"].(string), 104 | Comment: data["comment"].(string), 105 | }, nil 106 | } 107 | 108 | func setFromSshKeys(sshKeys []unifi.SettingMgmtXSshKeys) ([]map[string]any, error) { 109 | list := make([]map[string]any, 0, len(sshKeys)) 110 | for _, sshKey := range sshKeys { 111 | v, err := fromSshKey(sshKey) 112 | if err != nil { 113 | return nil, fmt.Errorf("unable to parse ssh key: %w", err) 114 | } 115 | list = append(list, v) 116 | } 117 | return list, nil 118 | } 119 | 120 | func fromSshKey(sshKey unifi.SettingMgmtXSshKeys) (map[string]any, error) { 121 | return map[string]any{ 122 | "name": sshKey.Name, 123 | "type": sshKey.KeyType, 124 | "key": sshKey.Key, 125 | "comment": sshKey.Comment, 126 | }, nil 127 | } 128 | 129 | func resourceSettingMgmtGetResourceData(d *schema.ResourceData, meta any) (*unifi.SettingMgmt, error) { 130 | sshKeys, err := setToSshKeys(d.Get("ssh_key").(*schema.Set)) 131 | if err != nil { 132 | return nil, fmt.Errorf("unable to process ssh_key block: %w", err) 133 | } 134 | 135 | return &unifi.SettingMgmt{ 136 | AutoUpgrade: d.Get("auto_upgrade").(bool), 137 | XSshEnabled: d.Get("ssh_enabled").(bool), 138 | XSshKeys: sshKeys, 139 | }, nil 140 | } 141 | 142 | func resourceSettingMgmtCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 143 | c := meta.(*client) 144 | 145 | req, err := resourceSettingMgmtGetResourceData(d, meta) 146 | if err != nil { 147 | return diag.FromErr(err) 148 | } 149 | 150 | site := d.Get("site").(string) 151 | if site == "" { 152 | site = c.site 153 | } 154 | 155 | resp, err := c.c.UpdateSettingMgmt(ctx, site, req) 156 | if err != nil { 157 | return diag.FromErr(err) 158 | } 159 | 160 | d.SetId(resp.ID) 161 | 162 | return resourceSettingMgmtSetResourceData(resp, d, meta, site) 163 | } 164 | 165 | func resourceSettingMgmtSetResourceData(resp *unifi.SettingMgmt, d *schema.ResourceData, meta any, site string) diag.Diagnostics { 166 | sshKeys, err := setFromSshKeys(resp.XSshKeys) 167 | if err != nil { 168 | return diag.FromErr(err) 169 | } 170 | 171 | d.Set("site", site) 172 | d.Set("auto_upgrade", resp.AutoUpgrade) 173 | d.Set("ssh_enabled", resp.XSshEnabled) 174 | d.Set("ssh_key", sshKeys) 175 | return nil 176 | } 177 | 178 | func resourceSettingMgmtRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 179 | c := meta.(*client) 180 | 181 | site := d.Get("site").(string) 182 | if site == "" { 183 | site = c.site 184 | } 185 | 186 | resp, err := c.c.GetSettingMgmt(ctx, site) 187 | if _, ok := err.(*unifi.NotFoundError); ok { 188 | d.SetId("") 189 | return nil 190 | } 191 | if err != nil { 192 | return diag.FromErr(err) 193 | } 194 | 195 | return resourceSettingMgmtSetResourceData(resp, d, meta, site) 196 | } 197 | 198 | func resourceSettingMgmtUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 199 | c := meta.(*client) 200 | 201 | req, err := resourceSettingMgmtGetResourceData(d, meta) 202 | if err != nil { 203 | return diag.FromErr(err) 204 | } 205 | 206 | req.ID = d.Id() 207 | site := d.Get("site").(string) 208 | if site == "" { 209 | site = c.site 210 | } 211 | 212 | resp, err := c.c.UpdateSettingMgmt(ctx, site, req) 213 | if err != nil { 214 | return diag.FromErr(err) 215 | } 216 | 217 | return resourceSettingMgmtSetResourceData(resp, d, meta, site) 218 | } 219 | 220 | func resourceSettingMgmtDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 221 | return nil 222 | } 223 | -------------------------------------------------------------------------------- /internal/provider/resource_setting_mgmt_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 8 | ) 9 | 10 | var settingMgmtLock = sync.Mutex{} 11 | 12 | func TestAccSettingMgmt_basic(t *testing.T) { 13 | resource.ParallelTest(t, resource.TestCase{ 14 | PreCheck: func() { 15 | preCheck(t) 16 | settingMgmtLock.Lock() 17 | t.Cleanup(func() { 18 | settingMgmtLock.Unlock() 19 | }) 20 | }, 21 | ProviderFactories: providerFactories, 22 | Steps: []resource.TestStep{ 23 | { 24 | Config: testAccSettingMgmtConfig_basic(), 25 | Check: resource.ComposeTestCheckFunc(), 26 | }, 27 | importStep("unifi_setting_mgmt.test"), 28 | }, 29 | }) 30 | } 31 | 32 | func TestAccSettingMgmt_site(t *testing.T) { 33 | resource.ParallelTest(t, resource.TestCase{ 34 | PreCheck: func() { 35 | preCheck(t) 36 | settingMgmtLock.Lock() 37 | t.Cleanup(func() { 38 | settingMgmtLock.Unlock() 39 | }) 40 | }, 41 | ProviderFactories: providerFactories, 42 | Steps: []resource.TestStep{ 43 | { 44 | Config: testAccSettingMgmtConfig_site(), 45 | Check: resource.ComposeTestCheckFunc(), 46 | }, 47 | { 48 | ResourceName: "unifi_setting_mgmt.test", 49 | ImportState: true, 50 | ImportStateIdFunc: siteAndIDImportStateIDFunc("unifi_setting_mgmt.test"), 51 | ImportStateVerify: true, 52 | }, 53 | }, 54 | }) 55 | } 56 | 57 | func TestAccSettingMgmt_sshKeys(t *testing.T) { 58 | resource.ParallelTest(t, resource.TestCase{ 59 | PreCheck: func() { 60 | preCheck(t) 61 | settingMgmtLock.Lock() 62 | t.Cleanup(func() { 63 | settingMgmtLock.Unlock() 64 | }) 65 | }, 66 | ProviderFactories: providerFactories, 67 | Steps: []resource.TestStep{ 68 | { 69 | Config: testAccSettingMgmtConfig_sshKeys(), 70 | Check: resource.ComposeTestCheckFunc(), 71 | }, 72 | { 73 | ResourceName: "unifi_setting_mgmt.test", 74 | ImportState: true, 75 | ImportStateIdFunc: siteAndIDImportStateIDFunc("unifi_setting_mgmt.test"), 76 | ImportStateVerify: true, 77 | }, 78 | }, 79 | }) 80 | } 81 | 82 | func testAccSettingMgmtConfig_basic() string { 83 | return ` 84 | resource "unifi_setting_mgmt" "test" { 85 | auto_upgrade = true 86 | } 87 | ` 88 | } 89 | 90 | func testAccSettingMgmtConfig_site() string { 91 | return ` 92 | resource "unifi_site" "test" { 93 | description = "test" 94 | } 95 | 96 | resource "unifi_setting_mgmt" "test" { 97 | site = unifi_site.test.name 98 | auto_upgrade = true 99 | } 100 | ` 101 | } 102 | 103 | func testAccSettingMgmtConfig_sshKeys() string { 104 | return ` 105 | resource "unifi_site" "test" { 106 | description = "test" 107 | } 108 | 109 | resource "unifi_setting_mgmt" "test" { 110 | site = unifi_site.test.name 111 | ssh_enabled = true 112 | ssh_key { 113 | name = "Test key" 114 | type = "ssh-rsa" 115 | key = "AAAAB3NzaC1yc2EAAAADAQABAAACAQDNWqT8zvVtmaks7sLlP+hmWmJVmruyNU9uk8JpLTX0oE+r9hjePsXCThTrft7s+vlaj+bLr8Yf5//TT8KS7LB/YIp2O3jPomOz9A4hIsG5R6FLfSggzQP4a7QSlNLCm/6WjKHP9DhRb7trnFz+KkCNmCVKLZgiyeUm2LydVKJ2QncHopA5yomtSpmb6x66zaKr+DbwzHC13WIEms5Ros0N9pEOcAghsSEVL42bfGBfSH37R+Kaw0nhWei4Y25jO66xsbtyZKoiF1+XXXBuEi77Tv7iQGHHOFRqNKKfGI1QhYvwlcjdzh9wu7Gtzeyh/+jpF8mwCLtFKle+W/zSs+lHCuCihvQEQtCIpZL5FapvxfxPZQJWL5RgsL9jieUaoF8EsWAOM83BCSZa/FB1RyfKdy4f7BQtDCKIm3nD5paCJSfS6DSw1TMvaFPeJLG3PuyHRbNvbVLmHRl9lK03na6/R9JX06nBUuPdP+FLjIZsyZz1DOUSDjCWHFk0+Ne2uEinV7SkOoxC6E2NxqlY/SyMnWZS+p95Zx6yOlNqB9sQ+Q4/YLGY5mUmqJrHPlH6LjXfudybKHMZUuVRF1NX3ESue8NSKc0SlJDQUXtJ9wkjjX1wAWvXCDwI72jtC86r/wzw+mcIfpks3jHQrOhpwCRmQL4vAs5DztA3hKxkgElYaw==" 116 | comment = "test@example.com" 117 | } 118 | } 119 | ` 120 | } 121 | -------------------------------------------------------------------------------- /internal/provider/resource_setting_radius.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" 9 | "github.com/ubiquiti-community/go-unifi/unifi" 10 | ) 11 | 12 | func resourceSettingRadius() *schema.Resource { 13 | return &schema.Resource{ 14 | Description: "`unifi_setting_radius` manages settings for the built-in RADIUS server.", 15 | 16 | CreateContext: resourceSettingRadiusCreate, 17 | ReadContext: resourceSettingRadiusRead, 18 | UpdateContext: resourceSettingRadiusUpdate, 19 | DeleteContext: schema.NoopContext, 20 | Importer: &schema.ResourceImporter{ 21 | StateContext: importSiteAndID, 22 | }, 23 | 24 | Schema: map[string]*schema.Schema{ 25 | "id": { 26 | Description: "The ID of the settings.", 27 | Type: schema.TypeString, 28 | Computed: true, 29 | }, 30 | "site": { 31 | Description: "The name of the site to associate the settings with.", 32 | Type: schema.TypeString, 33 | Computed: true, 34 | Optional: true, 35 | ForceNew: true, 36 | }, 37 | "accounting_enabled": { 38 | Description: "Enable RADIUS accounting", 39 | Type: schema.TypeBool, 40 | Optional: true, 41 | Default: false, 42 | }, 43 | "accounting_port": { 44 | Description: "The port for accounting communications.", 45 | Type: schema.TypeInt, 46 | Optional: true, 47 | Default: 1813, 48 | ValidateFunc: validation.IsPortNumber, 49 | }, 50 | "auth_port": { 51 | Description: "The port for authentication communications.", 52 | Type: schema.TypeInt, 53 | Optional: true, 54 | Default: 1812, 55 | ValidateFunc: validation.IsPortNumber, 56 | }, 57 | "interim_update_interval": { 58 | Description: "Statistics will be collected from connected clients at this interval.", 59 | Type: schema.TypeInt, 60 | Optional: true, 61 | Default: 3600, 62 | }, 63 | "tunneled_reply": { 64 | Description: "Encrypt communication between the server and the client.", 65 | Type: schema.TypeBool, 66 | Optional: true, 67 | Default: true, 68 | }, 69 | "secret": { 70 | Description: "RAIDUS secret passphrase.", 71 | Type: schema.TypeString, 72 | Sensitive: true, 73 | Optional: true, 74 | Default: "", 75 | }, 76 | "enabled": { 77 | Description: "RAIDUS server enabled.", 78 | Type: schema.TypeBool, 79 | Default: true, 80 | Optional: true, 81 | }, 82 | }, 83 | } 84 | } 85 | 86 | func resourceSettingRadiusGetResourceData(d *schema.ResourceData, meta any) (*unifi.SettingRadius, error) { 87 | return &unifi.SettingRadius{ 88 | AccountingEnabled: d.Get("accounting_enabled").(bool), 89 | Enabled: d.Get("enabled").(bool), 90 | AcctPort: d.Get("accounting_port").(int), 91 | AuthPort: d.Get("auth_port").(int), 92 | ConfigureWholeNetwork: true, 93 | TunneledReply: d.Get("tunneled_reply").(bool), 94 | XSecret: d.Get("secret").(string), 95 | InterimUpdateInterval: d.Get("interim_update_interval").(int), 96 | }, nil 97 | } 98 | 99 | func resourceSettingRadiusCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 100 | c := meta.(*client) 101 | 102 | req, err := resourceSettingRadiusGetResourceData(d, meta) 103 | if err != nil { 104 | return diag.FromErr(err) 105 | } 106 | 107 | site := d.Get("site").(string) 108 | if site == "" { 109 | site = c.site 110 | } 111 | 112 | resp, err := c.c.UpdateSettingRadius(ctx, site, req) 113 | if err != nil { 114 | return diag.FromErr(err) 115 | } 116 | 117 | d.SetId(resp.ID) 118 | 119 | return resourceSettingRadiusSetResourceData(resp, d, meta, site) 120 | } 121 | 122 | func resourceSettingRadiusSetResourceData(resp *unifi.SettingRadius, d *schema.ResourceData, meta any, site string) diag.Diagnostics { 123 | d.Set("site", site) 124 | d.Set("enabled", resp.Enabled) 125 | d.Set("accounting_enabled", resp.AccountingEnabled) 126 | d.Set("accounting_port", resp.AcctPort) 127 | d.Set("auth_port", resp.AuthPort) 128 | d.Set("tunneled_reply", resp.TunneledReply) 129 | d.Set("secret", resp.XSecret) 130 | d.Set("interim_update_interval", resp.InterimUpdateInterval) 131 | return nil 132 | } 133 | 134 | func resourceSettingRadiusRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 135 | c := meta.(*client) 136 | 137 | site := d.Get("site").(string) 138 | if site == "" { 139 | site = c.site 140 | } 141 | 142 | resp, err := c.c.GetSettingRadius(ctx, site) 143 | if _, ok := err.(*unifi.NotFoundError); ok { 144 | d.SetId("") 145 | return nil 146 | } 147 | if err != nil { 148 | return diag.FromErr(err) 149 | } 150 | 151 | return resourceSettingRadiusSetResourceData(resp, d, meta, site) 152 | } 153 | 154 | func resourceSettingRadiusUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 155 | c := meta.(*client) 156 | 157 | req, err := resourceSettingRadiusGetResourceData(d, meta) 158 | if err != nil { 159 | return diag.FromErr(err) 160 | } 161 | 162 | req.ID = d.Id() 163 | site := d.Get("site").(string) 164 | if site == "" { 165 | site = c.site 166 | } 167 | 168 | resp, err := c.c.UpdateSettingRadius(ctx, site, req) 169 | if err != nil { 170 | return diag.FromErr(err) 171 | } 172 | 173 | return resourceSettingRadiusSetResourceData(resp, d, meta, site) 174 | } 175 | -------------------------------------------------------------------------------- /internal/provider/resource_setting_radius_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 8 | ) 9 | 10 | var settingRadiusLock = sync.Mutex{} 11 | 12 | func TestAccSettingRadius_basic(t *testing.T) { 13 | resource.ParallelTest(t, resource.TestCase{ 14 | PreCheck: func() { 15 | preCheck(t) 16 | settingRadiusLock.Lock() 17 | t.Cleanup(func() { 18 | settingRadiusLock.Unlock() 19 | }) 20 | }, 21 | ProviderFactories: providerFactories, 22 | Steps: []resource.TestStep{ 23 | { 24 | Config: testAccSettingRadiusConfig_basic(), 25 | Check: resource.ComposeTestCheckFunc(), 26 | }, 27 | importStep("unifi_setting_radius.test"), 28 | }, 29 | }) 30 | } 31 | 32 | func TestAccSettingRadius_site(t *testing.T) { 33 | resource.ParallelTest(t, resource.TestCase{ 34 | PreCheck: func() { 35 | preCheck(t) 36 | settingRadiusLock.Lock() 37 | t.Cleanup(func() { 38 | settingRadiusLock.Unlock() 39 | }) 40 | }, 41 | ProviderFactories: providerFactories, 42 | Steps: []resource.TestStep{ 43 | { 44 | Config: testAccSettingRadiusConfig_site(), 45 | Check: resource.ComposeTestCheckFunc(), 46 | }, 47 | { 48 | ResourceName: "unifi_setting_radius.test", 49 | ImportState: true, 50 | ImportStateIdFunc: siteAndIDImportStateIDFunc("unifi_setting_radius.test"), 51 | ImportStateVerify: true, 52 | }, 53 | }, 54 | }) 55 | } 56 | 57 | func TestAccSettingRadius_full(t *testing.T) { 58 | resource.ParallelTest(t, resource.TestCase{ 59 | PreCheck: func() { 60 | preCheck(t) 61 | settingRadiusLock.Lock() 62 | t.Cleanup(func() { 63 | settingRadiusLock.Unlock() 64 | }) 65 | }, 66 | ProviderFactories: providerFactories, 67 | Steps: []resource.TestStep{ 68 | { 69 | Config: testAccSettingRadiusConfig_full(), 70 | Check: resource.ComposeTestCheckFunc(), 71 | }, 72 | { 73 | ResourceName: "unifi_setting_radius.test", 74 | ImportState: true, 75 | ImportStateIdFunc: siteAndIDImportStateIDFunc("unifi_setting_radius.test"), 76 | ImportStateVerify: true, 77 | }, 78 | }, 79 | }) 80 | } 81 | 82 | func TestAccSettingRadius_vlan(t *testing.T) { 83 | resource.ParallelTest(t, resource.TestCase{ 84 | PreCheck: func() { 85 | preCheck(t) 86 | settingRadiusLock.Lock() 87 | t.Cleanup(func() { 88 | settingRadiusLock.Unlock() 89 | }) 90 | }, 91 | ProviderFactories: providerFactories, 92 | Steps: []resource.TestStep{ 93 | { 94 | Config: testAccSettingRadiusConfig_vlan(), 95 | Check: resource.ComposeTestCheckFunc(), 96 | }, 97 | importStep("unifi_setting_radius.test"), 98 | }, 99 | }) 100 | } 101 | 102 | func testAccSettingRadiusConfig_basic() string { 103 | return ` 104 | resource "unifi_setting_radius" "test" { 105 | enabled = true 106 | secret = "securepw" 107 | } 108 | ` 109 | } 110 | 111 | func testAccSettingRadiusConfig_site() string { 112 | return ` 113 | resource "unifi_site" "test" { 114 | description = "test" 115 | } 116 | 117 | resource "unifi_setting_radius" "test" { 118 | site = unifi_site.test.name 119 | enabled = true 120 | secret = "securepw" 121 | } 122 | ` 123 | } 124 | 125 | func testAccSettingRadiusConfig_full() string { 126 | return ` 127 | resource "unifi_setting_radius" "test" { 128 | enabled = true 129 | secret = "securepw" 130 | accounting_port = "9999" 131 | auth_port = "8888" 132 | } 133 | ` 134 | } 135 | 136 | func testAccSettingRadiusConfig_vlan() string { 137 | return ` 138 | resource "unifi_setting_radius" "test" { 139 | enabled = true 140 | secret = "securepw" 141 | accounting_enabled = true 142 | } 143 | ` 144 | } 145 | -------------------------------------------------------------------------------- /internal/provider/resource_setting_usg.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" 11 | "github.com/ubiquiti-community/go-unifi/unifi" 12 | ) 13 | 14 | var resourceSettingUsgLock = sync.Mutex{} 15 | 16 | func resourceSettingUsgLocker(f func(context.Context, *schema.ResourceData, any) diag.Diagnostics) func(context.Context, *schema.ResourceData, any) diag.Diagnostics { 17 | return func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 18 | resourceSettingUsgLock.Lock() 19 | defer resourceSettingUsgLock.Unlock() 20 | return f(ctx, d, meta) 21 | } 22 | } 23 | 24 | func resourceSettingUsg() *schema.Resource { 25 | return &schema.Resource{ 26 | Description: "`unifi_setting_usg` manages settings for a Unifi Security Gateway.", 27 | 28 | CreateContext: resourceSettingUsgLocker(resourceSettingUsgUpsert), 29 | ReadContext: resourceSettingUsgLocker(resourceSettingUsgRead), 30 | UpdateContext: resourceSettingUsgLocker(resourceSettingUsgUpsert), 31 | DeleteContext: schema.NoopContext, 32 | Importer: &schema.ResourceImporter{ 33 | StateContext: importSiteAndID, 34 | }, 35 | 36 | Schema: map[string]*schema.Schema{ 37 | "id": { 38 | Description: "The ID of the settings.", 39 | Type: schema.TypeString, 40 | Computed: true, 41 | }, 42 | "site": { 43 | Description: "The name of the site to associate the settings with.", 44 | Type: schema.TypeString, 45 | Computed: true, 46 | Optional: true, 47 | ForceNew: true, 48 | }, 49 | "multicast_dns_enabled": { 50 | Description: "Whether multicast DNS is enabled.", 51 | Type: schema.TypeBool, 52 | Optional: true, 53 | Computed: true, 54 | }, 55 | "firewall_guest_default_log": { 56 | Description: "Whether the guest firewall log is enabled.", 57 | Type: schema.TypeBool, 58 | Optional: true, 59 | Computed: true, 60 | }, 61 | "firewall_lan_default_log": { 62 | Description: "Whether the LAN firewall log is enabled.", 63 | Type: schema.TypeBool, 64 | Optional: true, 65 | Computed: true, 66 | }, 67 | "firewall_wan_default_log": { 68 | Description: "Whether the WAN firewall log is enabled.", 69 | Type: schema.TypeBool, 70 | Optional: true, 71 | Computed: true, 72 | }, 73 | "dhcp_relay_servers": { 74 | Description: "The DHCP relay servers.", 75 | Type: schema.TypeList, 76 | Optional: true, 77 | Computed: true, 78 | MaxItems: 5, 79 | Elem: &schema.Schema{ 80 | Type: schema.TypeString, 81 | ValidateFunc: validation.All( 82 | validation.IsIPv4Address, 83 | // this doesn't let blank through 84 | validation.StringLenBetween(1, 50), 85 | ), 86 | }, 87 | }, 88 | }, 89 | } 90 | } 91 | 92 | func resourceSettingUsgUpdateResourceData(d *schema.ResourceData, meta any, setting *unifi.SettingUsg) error { 93 | c := meta.(*client) 94 | 95 | //nolint // GetOkExists is deprecated, but using here: 96 | if mdns, hasMdns := d.GetOkExists("multicast_dns_enabled"); hasMdns { 97 | if v := c.ControllerVersion(); v.GreaterThanOrEqual(controllerV7) { 98 | return fmt.Errorf("multicast_dns_enabled is not supported on controller version %v", c.ControllerVersion()) 99 | } 100 | 101 | setting.MdnsEnabled = mdns.(bool) 102 | } 103 | 104 | setting.FirewallGuestDefaultLog = d.Get("firewall_guest_default_log").(bool) 105 | setting.FirewallLanDefaultLog = d.Get("firewall_lan_default_log").(bool) 106 | setting.FirewallWANDefaultLog = d.Get("firewall_wan_default_log").(bool) 107 | 108 | dhcpRelay, err := listToStringSlice(d.Get("dhcp_relay_servers").([]any)) 109 | if err != nil { 110 | return fmt.Errorf("unable to convert dhcp_relay_servers to string slice: %w", err) 111 | } 112 | setting.DHCPRelayServer1 = append(dhcpRelay, "")[0] 113 | setting.DHCPRelayServer2 = append(dhcpRelay, "", "")[1] 114 | setting.DHCPRelayServer3 = append(dhcpRelay, "", "", "")[2] 115 | setting.DHCPRelayServer4 = append(dhcpRelay, "", "", "", "")[3] 116 | setting.DHCPRelayServer5 = append(dhcpRelay, "", "", "", "", "")[4] 117 | 118 | return nil 119 | } 120 | 121 | func resourceSettingUsgUpsert(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 122 | c := meta.(*client) 123 | 124 | site := d.Get("site").(string) 125 | if site == "" { 126 | site = c.site 127 | } 128 | 129 | req, err := c.c.GetSettingUsg(ctx, c.site) 130 | if err != nil { 131 | return diag.FromErr(err) 132 | } 133 | 134 | err = resourceSettingUsgUpdateResourceData(d, meta, req) 135 | if err != nil { 136 | return diag.FromErr(err) 137 | } 138 | 139 | resp, err := c.c.UpdateSettingUsg(ctx, site, req) 140 | if err != nil { 141 | return diag.FromErr(err) 142 | } 143 | 144 | d.SetId(resp.ID) 145 | return resourceSettingUsgSetResourceData(resp, d, meta, site) 146 | } 147 | 148 | func resourceSettingUsgSetResourceData(resp *unifi.SettingUsg, d *schema.ResourceData, meta any, site string) diag.Diagnostics { 149 | d.Set("site", site) 150 | d.Set("multicast_dns_enabled", resp.MdnsEnabled) 151 | d.Set("firewall_guest_default_log", resp.FirewallGuestDefaultLog) 152 | d.Set("firewall_lan_default_log", resp.FirewallLanDefaultLog) 153 | d.Set("firewall_wan_default_log", resp.FirewallWANDefaultLog) 154 | 155 | dhcpRelay := []string{} 156 | for _, s := range []string{ 157 | resp.DHCPRelayServer1, 158 | resp.DHCPRelayServer2, 159 | resp.DHCPRelayServer3, 160 | resp.DHCPRelayServer4, 161 | resp.DHCPRelayServer5, 162 | } { 163 | if s == "" { 164 | continue 165 | } 166 | dhcpRelay = append(dhcpRelay, s) 167 | } 168 | d.Set("dhcp_relay_servers", dhcpRelay) 169 | 170 | return nil 171 | } 172 | 173 | func resourceSettingUsgRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 174 | c := meta.(*client) 175 | 176 | site := d.Get("site").(string) 177 | if site == "" { 178 | site = c.site 179 | } 180 | 181 | resp, err := c.c.GetSettingUsg(ctx, site) 182 | if _, ok := err.(*unifi.NotFoundError); ok { 183 | d.SetId("") 184 | return nil 185 | } 186 | if err != nil { 187 | return diag.FromErr(err) 188 | } 189 | 190 | return resourceSettingUsgSetResourceData(resp, d, meta, site) 191 | } 192 | -------------------------------------------------------------------------------- /internal/provider/resource_setting_usg_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "sync" 7 | "testing" 8 | 9 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 10 | ) 11 | 12 | // using an additional lock to the one around the resource to avoid deadlocking accidentally 13 | var settingUsgLock = sync.Mutex{} 14 | 15 | func TestAccSettingUsg_mdns_v6(t *testing.T) { 16 | resource.ParallelTest(t, resource.TestCase{ 17 | PreCheck: func() { 18 | preCheck(t) 19 | preCheckVersionConstraint(t, "< 7") 20 | settingUsgLock.Lock() 21 | t.Cleanup(func() { 22 | settingUsgLock.Unlock() 23 | }) 24 | }, 25 | ProviderFactories: providerFactories, 26 | Steps: []resource.TestStep{ 27 | { 28 | Config: testAccSettingUsgConfig_mdns(true), 29 | Check: resource.ComposeTestCheckFunc(), 30 | }, 31 | importStep("unifi_setting_usg.test"), 32 | { 33 | Config: testAccSettingUsgConfig_mdns(false), 34 | Check: resource.ComposeTestCheckFunc(), 35 | }, 36 | importStep("unifi_setting_usg.test"), 37 | { 38 | Config: testAccSettingUsgConfig_mdns(true), 39 | Check: resource.ComposeTestCheckFunc(), 40 | }, 41 | importStep("unifi_setting_usg.test"), 42 | }, 43 | }) 44 | } 45 | 46 | func TestAccSettingUsg_mdns_v7(t *testing.T) { 47 | resource.ParallelTest(t, resource.TestCase{ 48 | PreCheck: func() { 49 | preCheck(t) 50 | preCheckVersionConstraint(t, ">= 7") 51 | settingUsgLock.Lock() 52 | t.Cleanup(func() { 53 | settingUsgLock.Unlock() 54 | }) 55 | }, 56 | ProviderFactories: providerFactories, 57 | Steps: []resource.TestStep{ 58 | { 59 | Config: testAccSettingUsgConfig_mdns(true), 60 | ExpectError: regexp.MustCompile("multicast_dns_enabled is not supported"), 61 | }, 62 | }, 63 | }) 64 | } 65 | 66 | func TestAccSettingUsg_dhcpRelay(t *testing.T) { 67 | resource.ParallelTest(t, resource.TestCase{ 68 | PreCheck: func() { 69 | preCheck(t) 70 | settingUsgLock.Lock() 71 | t.Cleanup(func() { 72 | settingUsgLock.Unlock() 73 | }) 74 | }, 75 | ProviderFactories: providerFactories, 76 | Steps: []resource.TestStep{ 77 | { 78 | Config: testAccSettingUsgConfig_dhcpRelay(), 79 | Check: resource.ComposeTestCheckFunc(), 80 | }, 81 | importStep("unifi_setting_usg.test"), 82 | }, 83 | }) 84 | } 85 | 86 | func TestAccSettingUsg_site(t *testing.T) { 87 | resource.ParallelTest(t, resource.TestCase{ 88 | PreCheck: func() { 89 | preCheck(t) 90 | settingUsgLock.Lock() 91 | t.Cleanup(func() { 92 | settingUsgLock.Unlock() 93 | }) 94 | }, 95 | ProviderFactories: providerFactories, 96 | Steps: []resource.TestStep{ 97 | { 98 | Config: testAccSettingUsgConfig_site(), 99 | Check: resource.ComposeTestCheckFunc(), 100 | }, 101 | { 102 | ResourceName: "unifi_setting_usg.test", 103 | ImportState: true, 104 | ImportStateIdFunc: siteAndIDImportStateIDFunc("unifi_setting_usg.test"), 105 | ImportStateVerify: true, 106 | }, 107 | }, 108 | }) 109 | } 110 | 111 | func testAccSettingUsgConfig_mdns(mdns bool) string { 112 | return fmt.Sprintf(` 113 | resource "unifi_setting_usg" "test" { 114 | multicast_dns_enabled = %t 115 | } 116 | `, mdns) 117 | } 118 | 119 | func testAccSettingUsgConfig_dhcpRelay() string { 120 | return ` 121 | resource "unifi_setting_usg" "test" { 122 | dhcp_relay_servers = [ 123 | "10.1.2.3", 124 | "10.1.2.4", 125 | ] 126 | } 127 | ` 128 | } 129 | 130 | func testAccSettingUsgConfig_site() string { 131 | return ` 132 | resource "unifi_site" "test" { 133 | description = "test" 134 | } 135 | 136 | resource "unifi_setting_usg" "test" { 137 | site = unifi_site.test.name 138 | } 139 | ` 140 | } 141 | -------------------------------------------------------------------------------- /internal/provider/resource_site.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | "github.com/ubiquiti-community/go-unifi/unifi" 11 | ) 12 | 13 | func resourceSite() *schema.Resource { 14 | return &schema.Resource{ 15 | Description: "`unifi_site` manages Unifi sites", 16 | 17 | CreateContext: resourceSiteCreate, 18 | ReadContext: resourceSiteRead, 19 | UpdateContext: resourceSiteUpdate, 20 | DeleteContext: resourceSiteDelete, 21 | Importer: &schema.ResourceImporter{ 22 | StateContext: resourceSiteImport, 23 | }, 24 | 25 | Schema: map[string]*schema.Schema{ 26 | "id": { 27 | Description: "The ID of the site.", 28 | Type: schema.TypeString, 29 | Computed: true, 30 | }, 31 | "description": { 32 | Description: "The description of the site.", 33 | Type: schema.TypeString, 34 | Required: true, 35 | }, 36 | "name": { 37 | Description: "The name of the site.", 38 | Type: schema.TypeString, 39 | Computed: true, 40 | }, 41 | }, 42 | } 43 | } 44 | 45 | func resourceSiteImport(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { 46 | c := meta.(*client) 47 | 48 | id := d.Id() 49 | _, err := c.c.GetSite(ctx, id) 50 | if err != nil { 51 | var nf *unifi.NotFoundError 52 | if !errors.As(err, &nf) { 53 | return nil, err 54 | } 55 | } else { 56 | // id is a valid site 57 | return []*schema.ResourceData{d}, nil 58 | } 59 | 60 | // lookup site by name 61 | sites, err := c.c.ListSites(ctx) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | for _, s := range sites { 67 | if s.Name == id { 68 | d.SetId(s.ID) 69 | return []*schema.ResourceData{d}, nil 70 | } 71 | } 72 | 73 | return nil, fmt.Errorf("unable to find site %q on controller", id) 74 | } 75 | 76 | func resourceSiteCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 77 | c := meta.(*client) 78 | 79 | description := d.Get("description").(string) 80 | 81 | resp, err := c.c.CreateSite(ctx, description) 82 | if err != nil { 83 | return diag.FromErr(err) 84 | } 85 | 86 | site := resp[0] 87 | d.SetId(site.ID) 88 | 89 | return resourceSiteSetResourceData(&site, d) 90 | } 91 | 92 | func resourceSiteSetResourceData(resp *unifi.Site, d *schema.ResourceData) diag.Diagnostics { 93 | d.Set("name", resp.Name) 94 | d.Set("description", resp.Description) 95 | return nil 96 | } 97 | 98 | func resourceSiteRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 99 | c := meta.(*client) 100 | 101 | id := d.Id() 102 | 103 | site, err := c.c.GetSite(ctx, id) 104 | if _, ok := err.(*unifi.NotFoundError); ok { 105 | d.SetId("") 106 | return nil 107 | } 108 | if err != nil { 109 | return diag.FromErr(err) 110 | } 111 | 112 | return resourceSiteSetResourceData(site, d) 113 | } 114 | 115 | func resourceSiteUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 116 | c := meta.(*client) 117 | 118 | site := &unifi.Site{ 119 | ID: d.Id(), 120 | Name: d.Get("name").(string), 121 | Description: d.Get("description").(string), 122 | } 123 | 124 | resp, err := c.c.UpdateSite(ctx, site.Name, site.Description) 125 | if err != nil { 126 | return diag.FromErr(err) 127 | } 128 | 129 | return resourceSiteSetResourceData(&resp[0], d) 130 | } 131 | 132 | func resourceSiteDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 133 | c := meta.(*client) 134 | id := d.Id() 135 | _, err := c.c.DeleteSite(ctx, id) 136 | return diag.FromErr(err) 137 | } 138 | -------------------------------------------------------------------------------- /internal/provider/resource_site_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 10 | "github.com/hashicorp/terraform-plugin-testing/terraform" 11 | ) 12 | 13 | func TestAccSite_basic(t *testing.T) { 14 | var siteName string 15 | 16 | resource.ParallelTest(t, resource.TestCase{ 17 | PreCheck: func() { preCheck(t) }, 18 | ProviderFactories: providerFactories, 19 | CheckDestroy: testAccCheckSiteResourceDestroy, 20 | Steps: []resource.TestStep{ 21 | { 22 | Config: testAccSiteConfig("tfacc-desc1"), 23 | Check: resource.ComposeTestCheckFunc( 24 | resource.TestCheckResourceAttr("unifi_site.test", "description", "tfacc-desc1"), 25 | 26 | // extract siteName for future use 27 | func(s *terraform.State) error { 28 | siteName = s.RootModule().Resources["unifi_site.test"].Primary.Attributes["name"] 29 | return nil 30 | }, 31 | ), 32 | }, 33 | importStep("unifi_site.test"), 34 | { 35 | Config: testAccSiteConfig("tfacc-desc2"), 36 | Check: resource.ComposeTestCheckFunc( 37 | resource.TestCheckResourceAttr("unifi_site.test", "description", "tfacc-desc2"), 38 | ), 39 | }, 40 | importStep("unifi_site.test"), 41 | 42 | // test importing from name, not id 43 | { 44 | ResourceName: "unifi_site.test", 45 | ImportStateIdFunc: func(*terraform.State) (string, error) { 46 | return siteName, nil 47 | }, 48 | ImportState: true, 49 | ImportStateVerify: true, 50 | }, 51 | }, 52 | }) 53 | } 54 | 55 | func testAccCheckSiteResourceDestroy(s *terraform.State) error { 56 | sites, err := testClient.ListSites(context.Background()) 57 | if err != nil { 58 | return err 59 | } 60 | for _, site := range sites { 61 | if strings.HasPrefix(site.Description, "tfacc-") { 62 | return fmt.Errorf("site not destroyed") 63 | } 64 | } 65 | return nil 66 | } 67 | 68 | func testAccSiteConfig(desc string) string { 69 | return fmt.Sprintf(` 70 | resource "unifi_site" "test" { 71 | description = %q 72 | } 73 | `, desc) 74 | } 75 | -------------------------------------------------------------------------------- /internal/provider/resource_static_route.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" 10 | "github.com/ubiquiti-community/go-unifi/unifi" 11 | ) 12 | 13 | func resourceStaticRoute() *schema.Resource { 14 | return &schema.Resource{ 15 | Description: "`unifi_static_route` manages a static route.", 16 | 17 | CreateContext: resourceStaticRouteCreate, 18 | ReadContext: resourceStaticRouteRead, 19 | UpdateContext: resourceStaticRouteUpdate, 20 | DeleteContext: resourceStaticRouteDelete, 21 | Importer: &schema.ResourceImporter{ 22 | StateContext: importSiteAndID, 23 | }, 24 | 25 | Schema: map[string]*schema.Schema{ 26 | "id": { 27 | Description: "The ID of the static route.", 28 | Type: schema.TypeString, 29 | Computed: true, 30 | }, 31 | "site": { 32 | Description: "The name of the site to associate the static route with.", 33 | Type: schema.TypeString, 34 | Computed: true, 35 | Optional: true, 36 | ForceNew: true, 37 | }, 38 | "name": { 39 | Description: "The name of the static route.", 40 | Type: schema.TypeString, 41 | Required: true, 42 | }, 43 | 44 | "network": { 45 | Description: "The network subnet address.", 46 | Type: schema.TypeString, 47 | Required: true, 48 | ValidateFunc: cidrValidate, 49 | DiffSuppressFunc: cidrDiffSuppress, 50 | }, 51 | "type": { 52 | Description: "The type of static route. Can be `interface-route`, `nexthop-route`, or `blackhole`.", 53 | Type: schema.TypeString, 54 | Required: true, 55 | ValidateFunc: validation.StringInSlice([]string{"interface-route", "nexthop-route", "blackhole"}, false), 56 | }, 57 | "distance": { 58 | Description: "The distance of the static route.", 59 | Type: schema.TypeInt, 60 | Required: true, 61 | }, 62 | 63 | "next_hop": { 64 | Description: "The next hop of the static route (only valid for `nexthop-route` type).", 65 | Type: schema.TypeString, 66 | Optional: true, 67 | ValidateFunc: validation.IsIPAddress, 68 | }, 69 | "interface": { 70 | Description: "The interface of the static route (only valid for `interface-route` type). This can be `WAN1`, `WAN2`, or a network ID.", 71 | Type: schema.TypeString, 72 | Optional: true, 73 | }, 74 | }, 75 | } 76 | } 77 | 78 | func resourceStaticRouteCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 79 | c := meta.(*client) 80 | 81 | req, err := resourceStaticRouteGetResourceData(d) 82 | if err != nil { 83 | return diag.FromErr(err) 84 | } 85 | 86 | site := d.Get("site").(string) 87 | if site == "" { 88 | site = c.site 89 | } 90 | 91 | resp, err := c.c.CreateRouting(ctx, site, req) 92 | if err != nil { 93 | return diag.FromErr(err) 94 | } 95 | 96 | d.SetId(resp.ID) 97 | 98 | return resourceStaticRouteSetResourceData(resp, d, site) 99 | } 100 | 101 | func resourceStaticRouteGetResourceData(d *schema.ResourceData) (*unifi.Routing, error) { 102 | t := d.Get("type").(string) 103 | 104 | r := &unifi.Routing{ 105 | Enabled: true, 106 | Type: "static-route", 107 | 108 | Name: d.Get("name").(string), 109 | StaticRouteNetwork: cidrZeroBased(d.Get("network").(string)), 110 | StaticRouteDistance: d.Get("distance").(int), 111 | StaticRouteType: t, 112 | } 113 | 114 | switch t { 115 | case "interface-route": 116 | r.StaticRouteInterface = d.Get("interface").(string) 117 | case "nexthop-route": 118 | r.StaticRouteNexthop = d.Get("next_hop").(string) 119 | case "blackhole": 120 | default: 121 | return nil, fmt.Errorf("unexpected route type: %q", t) 122 | } 123 | 124 | return r, nil 125 | } 126 | 127 | func resourceStaticRouteSetResourceData(resp *unifi.Routing, d *schema.ResourceData, site string) diag.Diagnostics { 128 | d.Set("site", site) 129 | d.Set("name", resp.Name) 130 | d.Set("network", cidrZeroBased(resp.StaticRouteNetwork)) 131 | d.Set("distance", resp.StaticRouteDistance) 132 | 133 | t := resp.StaticRouteType 134 | d.Set("type", t) 135 | 136 | d.Set("next_hop", "") 137 | d.Set("interface", "") 138 | 139 | switch t { 140 | case "interface-route": 141 | d.Set("interface", resp.StaticRouteInterface) 142 | case "nexthop-route": 143 | d.Set("next_hop", resp.StaticRouteNexthop) 144 | case "blackhole": 145 | // no additional attributes 146 | default: 147 | return diag.Errorf("unexpected static route type: %q", t) 148 | } 149 | 150 | return nil 151 | } 152 | 153 | func resourceStaticRouteRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 154 | c := meta.(*client) 155 | 156 | id := d.Id() 157 | 158 | site := d.Get("site").(string) 159 | if site == "" { 160 | site = c.site 161 | } 162 | 163 | resp, err := c.c.GetRouting(ctx, site, id) 164 | if _, ok := err.(*unifi.NotFoundError); ok { 165 | d.SetId("") 166 | return nil 167 | } 168 | if err != nil { 169 | return diag.FromErr(err) 170 | } 171 | 172 | return resourceStaticRouteSetResourceData(resp, d, site) 173 | } 174 | 175 | func resourceStaticRouteUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 176 | c := meta.(*client) 177 | 178 | req, err := resourceStaticRouteGetResourceData(d) 179 | if err != nil { 180 | return diag.FromErr(err) 181 | } 182 | 183 | req.ID = d.Id() 184 | 185 | site := d.Get("site").(string) 186 | if site == "" { 187 | site = c.site 188 | } 189 | req.SiteID = site 190 | 191 | resp, err := c.c.UpdateRouting(ctx, site, req) 192 | if err != nil { 193 | return diag.FromErr(err) 194 | } 195 | 196 | return resourceStaticRouteSetResourceData(resp, d, site) 197 | } 198 | 199 | func resourceStaticRouteDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 200 | c := meta.(*client) 201 | 202 | id := d.Id() 203 | 204 | site := d.Get("site").(string) 205 | if site == "" { 206 | site = c.site 207 | } 208 | err := c.c.DeleteRouting(ctx, site, id) 209 | if _, ok := err.(*unifi.NotFoundError); ok { 210 | return nil 211 | } 212 | return diag.FromErr(err) 213 | } 214 | -------------------------------------------------------------------------------- /internal/provider/resource_user_group.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | "github.com/ubiquiti-community/go-unifi/unifi" 9 | ) 10 | 11 | func resourceUserGroup() *schema.Resource { 12 | return &schema.Resource{ 13 | Description: "`unifi_user_group` manages a user group (called \"client group\" in the UI), which can be used " + 14 | "to limit bandwidth for groups of users.", 15 | 16 | CreateContext: resourceUserGroupCreate, 17 | ReadContext: resourceUserGroupRead, 18 | UpdateContext: resourceUserGroupUpdate, 19 | DeleteContext: resourceUserGroupDelete, 20 | Importer: &schema.ResourceImporter{ 21 | StateContext: importSiteAndID, 22 | }, 23 | 24 | Schema: map[string]*schema.Schema{ 25 | "id": { 26 | Description: "The ID of the user group.", 27 | Type: schema.TypeString, 28 | Computed: true, 29 | }, 30 | "site": { 31 | Description: "The name of the site to associate the user group with.", 32 | Type: schema.TypeString, 33 | Computed: true, 34 | Optional: true, 35 | ForceNew: true, 36 | }, 37 | "name": { 38 | Description: "The name of the user group.", 39 | Type: schema.TypeString, 40 | Required: true, 41 | }, 42 | "qos_rate_max_down": { 43 | Description: "The QOS maximum download rate.", 44 | Type: schema.TypeInt, 45 | Optional: true, 46 | Default: -1, 47 | // TODO: validate does not equal 0,1 48 | }, 49 | "qos_rate_max_up": { 50 | Description: "The QOS maximum upload rate.", 51 | Type: schema.TypeInt, 52 | Optional: true, 53 | Default: -1, 54 | // TODO: validate does not equal 0,1 55 | }, 56 | }, 57 | } 58 | } 59 | 60 | func resourceUserGroupCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 61 | c := meta.(*client) 62 | 63 | req, err := resourceUserGroupGetResourceData(d) 64 | if err != nil { 65 | return diag.FromErr(err) 66 | } 67 | 68 | site := d.Get("site").(string) 69 | if site == "" { 70 | site = c.site 71 | } 72 | 73 | resp, err := c.c.CreateUserGroup(context.TODO(), site, req) 74 | if err != nil { 75 | return diag.FromErr(err) 76 | } 77 | 78 | d.SetId(resp.ID) 79 | 80 | return resourceUserGroupSetResourceData(resp, d) 81 | } 82 | 83 | func resourceUserGroupGetResourceData(d *schema.ResourceData) (*unifi.UserGroup, error) { 84 | return &unifi.UserGroup{ 85 | Name: d.Get("name").(string), 86 | QOSRateMaxDown: d.Get("qos_rate_max_down").(int), 87 | QOSRateMaxUp: d.Get("qos_rate_max_up").(int), 88 | }, nil 89 | } 90 | 91 | func resourceUserGroupSetResourceData(resp *unifi.UserGroup, d *schema.ResourceData) diag.Diagnostics { 92 | d.Set("name", resp.Name) 93 | d.Set("qos_rate_max_down", resp.QOSRateMaxDown) 94 | d.Set("qos_rate_max_up", resp.QOSRateMaxUp) 95 | 96 | return nil 97 | } 98 | 99 | func resourceUserGroupRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 100 | c := meta.(*client) 101 | 102 | id := d.Id() 103 | 104 | site := d.Get("site").(string) 105 | if site == "" { 106 | site = c.site 107 | } 108 | 109 | resp, err := c.c.GetUserGroup(context.TODO(), site, id) 110 | if _, ok := err.(*unifi.NotFoundError); ok { 111 | d.SetId("") 112 | return nil 113 | } 114 | if err != nil { 115 | return diag.FromErr(err) 116 | } 117 | 118 | return resourceUserGroupSetResourceData(resp, d) 119 | } 120 | 121 | func resourceUserGroupUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 122 | c := meta.(*client) 123 | 124 | req, err := resourceUserGroupGetResourceData(d) 125 | if err != nil { 126 | return diag.FromErr(err) 127 | } 128 | 129 | req.ID = d.Id() 130 | 131 | site := d.Get("site").(string) 132 | if site == "" { 133 | site = c.site 134 | } 135 | req.SiteID = site 136 | 137 | resp, err := c.c.UpdateUserGroup(context.TODO(), site, req) 138 | if err != nil { 139 | return diag.FromErr(err) 140 | } 141 | 142 | return resourceUserGroupSetResourceData(resp, d) 143 | } 144 | 145 | func resourceUserGroupDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { 146 | c := meta.(*client) 147 | 148 | id := d.Id() 149 | 150 | site := d.Get("site").(string) 151 | if site == "" { 152 | site = c.site 153 | } 154 | err := c.c.DeleteUserGroup(context.TODO(), site, id) 155 | if _, ok := err.(*unifi.NotFoundError); ok { 156 | return nil 157 | } 158 | return diag.FromErr(err) 159 | } 160 | -------------------------------------------------------------------------------- /internal/provider/resource_user_group_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 7 | ) 8 | 9 | func TestAccUserGroup_basic(t *testing.T) { 10 | resource.ParallelTest(t, resource.TestCase{ 11 | PreCheck: func() { preCheck(t) }, 12 | ProviderFactories: providerFactories, 13 | // TODO: CheckDestroy: , 14 | Steps: []resource.TestStep{ 15 | { 16 | Config: testAccUserGroupConfig, 17 | // Check: resource.ComposeTestCheckFunc( 18 | // // testCheckUserGroupExists(t, "name"), 19 | // ), 20 | }, 21 | { 22 | Config: testAccUserGroupConfig_qos, 23 | }, 24 | importStep("unifi_user_group.test"), 25 | { 26 | Config: testAccUserGroupConfig, 27 | }, 28 | importStep("unifi_user_group.test"), 29 | }, 30 | }) 31 | } 32 | 33 | const testAccUserGroupConfig = ` 34 | resource "unifi_user_group" "test" { 35 | name = "tfacc" 36 | } 37 | ` 38 | 39 | const testAccUserGroupConfig_qos = ` 40 | resource "unifi_user_group" "test" { 41 | name = "tfacc" 42 | 43 | qos_rate_max_up = 2000 44 | qos_rate_max_down = 50 45 | } 46 | ` 47 | -------------------------------------------------------------------------------- /internal/provider/strings.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 7 | ) 8 | 9 | func listToStringSlice(src []any) ([]string, error) { 10 | dst := make([]string, 0, len(src)) 11 | for _, s := range src { 12 | d, ok := s.(string) 13 | if !ok { 14 | return nil, fmt.Errorf("unale to convert %v (%T) to string", s, s) 15 | } 16 | dst = append(dst, d) 17 | } 18 | return dst, nil 19 | } 20 | 21 | func setToStringSlice(src *schema.Set) ([]string, error) { 22 | return listToStringSlice(src.List()) 23 | } 24 | 25 | func stringSliceToList(list []string) []any { 26 | vs := make([]any, 0, len(list)) 27 | for _, v := range list { 28 | vs = append(vs, v) 29 | } 30 | return vs 31 | } 32 | 33 | func stringSliceToSet(src []string) *schema.Set { 34 | return schema.NewSet(schema.HashString, stringSliceToList(src)) 35 | } 36 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main // import "github.com/ubiquiti-community/terraform-provider-unifi" 2 | 3 | import ( 4 | "flag" 5 | 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" 7 | 8 | "github.com/ubiquiti-community/terraform-provider-unifi/internal/provider" 9 | ) 10 | 11 | // Generate docs for website 12 | //go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs 13 | 14 | var ( 15 | // these will be set by the goreleaser configuration 16 | // to appropriate values for the compiled binary 17 | version string = "dev" 18 | 19 | // goreleaser can also pass the specific commit if you want 20 | // commit string = "" 21 | ) 22 | 23 | func main() { 24 | var debugMode bool 25 | 26 | flag.BoolVar(&debugMode, "debug", false, "set to true to run the provider with support for debuggers like delve") 27 | flag.Parse() 28 | 29 | opts := &plugin.ServeOpts{ProviderFunc: provider.New(version)} 30 | 31 | if debugMode { 32 | opts.Debug = true 33 | opts.ProviderAddr = "registry.terraform.io/paultyng/unifi" 34 | } 35 | 36 | plugin.Serve(opts) 37 | } 38 | -------------------------------------------------------------------------------- /scripts/init.d/demo-mode: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | write_config() { 4 | echo "${1}=${2}" >> /usr/lib/unifi/data/system.properties 5 | } 6 | 7 | write_config is_simulation true 8 | 9 | # Increase the number of demo devices to allow more concurrent tests to be executed simultaneously. 10 | write_config demo.num_uap 0 11 | write_config demo.num_ugw 1 12 | write_config demo.num_usw 20 13 | -------------------------------------------------------------------------------- /templates/guides/csv-users.md.tmpl: -------------------------------------------------------------------------------- 1 | --- 2 | subcategory: "" 3 | page_title: "Manage Users/Clients in a CSV - Unifi Provider" 4 | description: |- 5 | An example of using a CSV to manage all of your users of your network. 6 | --- 7 | 8 | # Manage Users in a CSV 9 | 10 | Given a CSV file with the following content: 11 | 12 | {{ codefile "csv" "examples/csv_users/users.csv" }} 13 | 14 | You could create/manage a `unifi_user` for every row/MAC address in the CSV with the following config: 15 | 16 | {{ tffile "examples/csv_users/users.tf" }} 17 | -------------------------------------------------------------------------------- /templates/guides/multiple-site-firewall.md.tmpl: -------------------------------------------------------------------------------- 1 | --- 2 | subcategory: "" 3 | page_title: "Manage Firewall Rules for Multiple Sites - Unifi Provider" 4 | description: |- 5 | An example of applying firewall rules to multiple sites. 6 | --- 7 | 8 | # Manage Firewall Rules for Multiple Sites 9 | 10 | The provider takes a default site value but all resources in the provider should allow overriding of the 11 | site you are managing. In order to apply and manage a firewall rule across multiple sites, you simply 12 | need to provide different values for the `site` attribute to `unifi_firewall_rule`: 13 | 14 | {{ tffile "examples/multiple_site_firewall/firewall.tf" }} 15 | 16 | You could optionally load lists of sites from JSON/CSV, variables, or other sources. 17 | 18 | When you apply this configuration it will create the same firewall rule on every site in the list. 19 | If you need to update the rule, you simply make an update to the rule definition and Terraform will 20 | apply/update it across all the sites. If you add / or remove a site from the list, Terraform will also 21 | handle creating or removing the rule on the subsequent `terraform apply`. 22 | -------------------------------------------------------------------------------- /templates/index.md.tmpl: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "" 3 | page_title: "Provider: Unifi" 4 | description: |- 5 | The Unifi provider provides resources to interact with a Unifi controller API. 6 | --- 7 | 8 | # Unifi Provider 9 | 10 | The Unifi provider provides resources to interact with a Unifi controller API. 11 | 12 | It is not recommended to use your own account for management of your controller. A user specific to 13 | Terraform is recommended. You can create a **Limited Admin** with **Local Access Only** and 14 | provide that information for authentication. Two-factor authentication is not supported in the provider. 15 | 16 | ## Example Usage 17 | 18 | {{tffile "examples/provider/provider.tf"}} 19 | 20 | {{ .SchemaMarkdown | trimspace }} 21 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package tools 5 | 6 | import ( 7 | _ "github.com/golangci/golangci-lint/cmd/golangci-lint" 8 | _ "github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs" 9 | ) 10 | --------------------------------------------------------------------------------