├── .gitattributes ├── .github ├── dependabot.yml ├── release.yml └── workflows │ ├── continuous-integration.yml │ ├── release.yml │ └── tfplugindocs.yml ├── .gitignore ├── .goreleaser.yml ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── bin └── wait-for-routeros.sh ├── client ├── bgp_instance.go ├── bgp_instance_test.go ├── bgp_peer.go ├── bgp_peer_test.go ├── bridge.go ├── bridge_port.go ├── bridge_port_test.go ├── bridge_test.go ├── bridge_vlan.go ├── bridge_vlan_test.go ├── client.go ├── client_crud.go ├── client_test.go ├── console-inspected │ ├── parse.go │ ├── parse_test.go │ ├── split_strategy.go │ └── types.go ├── console_inspect.go ├── dhcp_server.go ├── dhcp_server_network.go ├── dhcp_server_network_test.go ├── dhcp_server_test.go ├── dns.go ├── dns_test.go ├── errors.go ├── errors_test.go ├── firewall_filter.go ├── firewall_filter_test.go ├── go.mod ├── go.sum ├── helpers.go ├── interface_list.go ├── interface_list_member.go ├── interface_list_member_test.go ├── interface_list_test.go ├── interface_wireguard.go ├── interface_wireguard_peer.go ├── interface_wireguard_peer_test.go ├── interface_wireguard_test.go ├── ip_addr.go ├── ip_addr_test.go ├── ipv6_addr.go ├── ipv6_addr_test.go ├── lease.go ├── lease_test.go ├── pool.go ├── pool_test.go ├── resource_wrappers.go ├── scheduler.go ├── scheduler_test.go ├── script.go ├── script_test.go ├── setup.go ├── setup_test.go ├── system_resources.go ├── system_resources_test.go ├── types │ ├── duration.go │ ├── duration_test.go │ ├── list.go │ └── list_test.go ├── vlan_interface.go ├── vlan_interface_test.go ├── wireless_interface.go ├── wireless_interface_test.go ├── wireless_security_profile.go └── wireless_security_profile_test.go ├── cmd └── mikrotik-codegen │ ├── internal │ ├── codegen │ │ ├── README.md │ │ ├── formatter.go │ │ ├── generator_mikrotik.go │ │ ├── generator_terraform.go │ │ ├── parser.go │ │ ├── parser_test.go │ │ ├── templates.go │ │ ├── types.go │ │ └── types_test.go │ └── utils │ │ ├── utils.go │ │ └── utils_test.go │ └── main.go ├── docker └── docker-compose.yml ├── docs ├── index.md └── resources │ ├── bgp_instance.md │ ├── bgp_peer.md │ ├── bridge.md │ ├── bridge_port.md │ ├── bridge_vlan.md │ ├── dhcp_lease.md │ ├── dhcp_server.md │ ├── dhcp_server_network.md │ ├── dns_record.md │ ├── firewall_filter_rule.md │ ├── interface_list.md │ ├── interface_list_member.md │ ├── interface_wireguard.md │ ├── interface_wireguard_peer.md │ ├── ip_address.md │ ├── ipv6_address.md │ ├── pool.md │ ├── scheduler.md │ ├── script.md │ ├── vlan_interface.md │ └── wireless_interface.md ├── examples ├── provider │ └── provider.tf └── resources │ ├── mikrotik_bgp_instance │ ├── import.sh │ └── resource.tf │ ├── mikrotik_bgp_peer │ ├── import.sh │ └── resource.tf │ ├── mikrotik_bridge │ ├── import.sh │ └── resource.tf │ ├── mikrotik_bridge_port │ ├── import.sh │ └── resource.tf │ ├── mikrotik_bridge_vlan │ ├── import.sh │ └── resource.tf │ ├── mikrotik_dhcp_lease │ ├── import.sh │ └── resource.tf │ ├── mikrotik_dhcp_server │ ├── import.sh │ └── resource.tf │ ├── mikrotik_dhcp_server_network │ ├── import.sh │ └── resource.tf │ ├── mikrotik_dns_record │ ├── import.sh │ └── resource.tf │ ├── mikrotik_firewall_filter_rule │ ├── import.sh │ └── resource.tf │ ├── mikrotik_interface_list │ ├── import.sh │ └── resource.tf │ ├── mikrotik_interface_list_member │ ├── import.sh │ └── resource.tf │ ├── mikrotik_interface_wireguard │ ├── import.sh │ └── resource.tf │ ├── mikrotik_interface_wireguard_peer │ ├── import.sh │ └── resource.tf │ ├── mikrotik_ip_address │ ├── import.sh │ └── resource.tf │ ├── mikrotik_ipv6_address │ ├── import.sh │ └── resource.tf │ ├── mikrotik_pool │ ├── import.sh │ └── resource.tf │ ├── mikrotik_scheduler │ ├── import.sh │ └── resource.tf │ ├── mikrotik_script │ ├── import.sh │ └── resource.tf │ └── mikrotik_vlan_interface │ ├── import.sh │ └── resource.tf ├── go.mod ├── go.sum ├── go.work ├── go.work.sum ├── main.go ├── mikrotik ├── acc_setup_test.go ├── internal │ ├── test_helpers.go │ ├── types │ │ └── defaultaware │ │ │ ├── bool.go │ │ │ ├── bool_test.go │ │ │ ├── int64.go │ │ │ ├── int64_test.go │ │ │ ├── resource_wrapper.go │ │ │ ├── resource_wrapper_test.go │ │ │ ├── string.go │ │ │ └── string_test.go │ └── utils │ │ ├── provider.go │ │ ├── provider_test.go │ │ ├── string.go │ │ ├── struct_copy.go │ │ └── struct_copy_test.go ├── provider.go ├── provider_framework.go ├── provider_test.go ├── resource_bgp_instance.go ├── resource_bgp_instance_test.go ├── resource_bgp_peer.go ├── resource_bgp_peer_test.go ├── resource_bridge.go ├── resource_bridge_port.go ├── resource_bridge_port_test.go ├── resource_bridge_test.go ├── resource_bridge_vlan.go ├── resource_bridge_vlan_test.go ├── resource_dhcp_lease.go ├── resource_dhcp_lease_test.go ├── resource_dhcp_server.go ├── resource_dhcp_server_network.go ├── resource_dhcp_server_network_test.go ├── resource_dhcp_server_test.go ├── resource_dns_record.go ├── resource_dns_record_test.go ├── resource_firewall_filter.go ├── resource_firewall_filter_rule_test.go ├── resource_generic_crud_operations.go ├── resource_interface_list.go ├── resource_interface_list_member.go ├── resource_interface_list_member_test.go ├── resource_interface_list_test.go ├── resource_interface_wireguard.go ├── resource_interface_wireguard_peer.go ├── resource_interface_wireguard_peer_test.go ├── resource_interface_wireguard_test.go ├── resource_ip_address.go ├── resource_ip_address_test.go ├── resource_ipv6_address.go ├── resource_ipv6_address_test.go ├── resource_pool.go ├── resource_pool_test.go ├── resource_scheduler.go ├── resource_scheduler_test.go ├── resource_script.go ├── resource_script_test.go ├── resource_vlan_interface.go ├── resource_vlan_interface_test.go ├── resource_wireless_interface.go └── resource_wireless_interface_test.go ├── templates ├── index.md.tmpl ├── resources.md.tmpl └── resources │ ├── bgp_instance.md.tmpl │ └── bgp_peer.md.tmpl └── tools └── tools.go /.gitattributes: -------------------------------------------------------------------------------- 1 | # do not expand these files by default in diff viewer 2 | go.sum linguist-generated 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | open-pull-requests-limit: 0 5 | - package-ecosystem: "gomod" 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | open-pull-requests-limit: 0 10 | - package-ecosystem: github-actions 11 | directory: / 12 | schedule: 13 | interval: daily 14 | open-pull-requests-limit: 0 15 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: Breaking Changes 🛠 4 | labels: 5 | - breaking-change 6 | - title: New Features 🎉 7 | labels: 8 | - enhancement 9 | - title: Bug fixes 10 | labels: 11 | - bug 12 | - title: Other Changes 13 | labels: 14 | - "*" 15 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ${{ matrix.os }} 13 | continue-on-error: ${{ matrix.experimental }} 14 | strategy: 15 | matrix: 16 | experimental: [false] 17 | go: ["1.18"] 18 | os: [ubuntu-latest] 19 | # Test against latest stable 6.x and 7.x and "latest" stable 20 | routeros: ["6.49.15", "7.14.3"] 21 | include: 22 | - experimental: true 23 | go: 1.18 24 | os: ubuntu-latest 25 | routeros: "latest" 26 | 27 | steps: 28 | - name: Set up Go 29 | uses: actions/setup-go@v4 30 | with: 31 | go-version: ${{ matrix.go }} 32 | id: go 33 | 34 | - name: Check out code into the Go module directory 35 | uses: actions/checkout@v3 36 | 37 | - name: Get dependencies 38 | run: go mod download 39 | 40 | - name: Build 41 | run: go build -v . 42 | 43 | - name: Run linters 44 | run: make lint 45 | 46 | - name: Wait until RouterOS container is ready 47 | run: ./bin/wait-for-routeros.sh 127.0.0.1 8080 48 | 49 | - name: Run provider tests 50 | run: make testacc 51 | env: 52 | MIKROTIK_HOST: 127.0.0.1:8728 53 | MIKROTIK_USER: admin 54 | MIKROTIK_PASSWORD: '' 55 | TF_ACC: 1 56 | 57 | - name: Run client tests 58 | run: make testclient 59 | env: 60 | MIKROTIK_HOST: 127.0.0.1:8728 61 | MIKROTIK_USER: admin 62 | MIKROTIK_PASSWORD: '' 63 | TF_ACC: 1 64 | 65 | services: 66 | routeros: 67 | image: mnazarenko/docker-routeros:${{ matrix.routeros }} 68 | ports: 69 | - 8728:8728 70 | - 8080:80 71 | options: >- 72 | --cap-add=NET_ADMIN 73 | --device=/dev/net/tun 74 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This GitHub action can publish assets for release when a tag is created. 2 | # Currently its setup to run on any tag that matches the pattern "v*" (ie. v0.1.0). 3 | # 4 | # This uses an action (paultyng/ghaction-import-gpg) that assumes you set your 5 | # private key in the `GPG_PRIVATE_KEY` secret and passphrase in the `PASSPHRASE` 6 | # secret. If you would rather own your own GPG handling, please fork this action 7 | # or use an alternative one for key handling. 8 | # 9 | # You will need to pass the `--batch` flag to `gpg` in your signing step 10 | # in `goreleaser` to indicate this is being used in a non-interactive mode. 11 | # 12 | name: release 13 | on: 14 | push: 15 | tags: 16 | - 'v*' 17 | jobs: 18 | goreleaser: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - 22 | name: Checkout 23 | uses: actions/checkout@v3 24 | - 25 | name: Unshallow 26 | run: git fetch --prune --unshallow 27 | - 28 | name: Set up Go 29 | uses: actions/setup-go@v4 30 | with: 31 | go-version: 1.18 32 | - 33 | name: Import GPG key 34 | id: import_gpg 35 | # TODO: move this to HashiCorp namespace or find alternative that is just simple gpg commands 36 | # see https://github.com/hashicorp/terraform-provider-scaffolding/issues/22 37 | uses: paultyng/ghaction-import-gpg@v2.1.0 38 | env: 39 | # These secrets will need to be configured for the repository: 40 | GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} 41 | PASSPHRASE: ${{ secrets.PASSPHRASE }} 42 | - 43 | name: Run GoReleaser 44 | uses: goreleaser/goreleaser-action@v5 45 | with: 46 | version: latest 47 | args: release --rm-dist 48 | env: 49 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} 50 | # GitHub sets this automatically 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | -------------------------------------------------------------------------------- /.github/workflows/tfplugindocs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'tfplugindocs' 3 | on: 4 | pull_request: 5 | permissions: 6 | contents: read 7 | jobs: 8 | tfplugindocs: 9 | permissions: 10 | contents: read 11 | pull-requests: read 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Set up Go 15 | uses: actions/setup-go@v4 16 | with: 17 | go-version: 1.18 18 | - name: Check out code into the Go module directory 19 | uses: actions/checkout@v3 20 | - name: Get dependencies 21 | run: go mod download 22 | - name: Run tfplugindocs 23 | run: go generate ./... 24 | - name: Fail if any files changed 25 | shell: bash 26 | run: | 27 | if [[ $(git status --porcelain=v1 docs/ | wc -l) -ne 0 ]]; then 28 | echo "Please ensure tfplugindocs changes are committed to docs/" 29 | echo "Changed files:" 30 | git diff --name-only docs/ 31 | git status docs/ 32 | exit 1 33 | fi 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | terraform-provider-mikrotik 2 | dist/ 3 | vendor 4 | .vscode/ 5 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Visit https://goreleaser.com for documentation on how to customize this 2 | # behavior. 3 | before: 4 | hooks: 5 | # this is just an example and not a requirement for provider building/publishing 6 | - go mod tidy 7 | builds: 8 | - env: 9 | # goreleaser does not work with CGO, it could also complicate 10 | # usage by users in CI/CD systems like Terraform Cloud where 11 | # they are unable to install libraries. 12 | - CGO_ENABLED=0 13 | mod_timestamp: '{{ .CommitTimestamp }}' 14 | flags: 15 | - -trimpath 16 | ldflags: 17 | - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' 18 | goos: 19 | - freebsd 20 | - windows 21 | - linux 22 | - darwin 23 | goarch: 24 | - amd64 25 | - '386' 26 | - arm 27 | - arm64 28 | ignore: 29 | - goos: darwin 30 | goarch: '386' 31 | binary: '{{ .ProjectName }}_v{{ .Version }}' 32 | archives: 33 | - format: zip 34 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' 35 | checksum: 36 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' 37 | algorithm: sha256 38 | signs: 39 | - artifacts: checksum 40 | args: 41 | # if you are using this is a GitHub action or some other automated pipeline, you 42 | # need to pass the batch flag to indicate its not interactive. 43 | - "--batch" 44 | - "--local-user" 45 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key 46 | - "--output" 47 | - "${signature}" 48 | - "--detach-sign" 49 | - "${artifact}" 50 | release: 51 | draft: false 52 | changelog: 53 | use: github-native 54 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.15.x 4 | script: 5 | - make build 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019-2022 Dom Del Nano and others 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build generate clean plan apply lint-client lint-provider lint testacc testclient test 2 | 3 | TIMEOUT ?= 40m 4 | ROUTEROS_VERSION ?= "" 5 | ifdef TEST 6 | override TEST := ./... -run $(TEST) 7 | else 8 | override TEST := ./... 9 | endif 10 | 11 | ifdef TF_LOG 12 | override TF_LOG := TF_LOG=$(TF_LOG) 13 | endif 14 | 15 | compose := docker compose -f docker/docker-compose.yml 16 | 17 | build: 18 | go build -o terraform-provider-mikrotik 19 | 20 | generate: 21 | go generate ./... 22 | 23 | clean: 24 | rm dist/* 25 | 26 | plan: build 27 | terraform init 28 | terraform plan 29 | 30 | apply: 31 | terraform apply 32 | 33 | lint-client: 34 | go vet ./client/... 35 | 36 | lint-provider: 37 | go vet ./mikrotik/... 38 | 39 | lint: lint-client lint-provider 40 | 41 | test: lint testclient testacc 42 | 43 | testclient: 44 | cd client; go test $(TEST) -race -v -count 1 45 | 46 | testacc: 47 | TF_ACC=1 $(TF_LOG) go test $(TEST) -v -count 1 -timeout $(TIMEOUT) 48 | 49 | routeros: routeros-clean 50 | ROUTEROS_VERSION=$(ROUTEROS_VERSION) ${compose} up -d --build --remove-orphans routeros 51 | 52 | routeros-stop: 53 | ${compose} stop routeros 54 | 55 | routeros-logs: 56 | ${compose} logs -f routeros 57 | 58 | routeros-clean: 59 | ${compose} rm -sfv routeros 60 | -------------------------------------------------------------------------------- /bin/wait-for-routeros.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | routeros_host=${1:-127.0.0.1} 4 | routeros_port=${2:-8080} 5 | 6 | echo "waiting for RouterOS (${routeros_host}:${routeros_port}) to be up and running" 7 | for i in $(seq 1 60); do 8 | if curl -s --connect-timeout 1 -o /dev/null ${routeros_host}:${routeros_port}; then 9 | exit 0; 10 | else 11 | printf "." 12 | sleep 1 13 | fi 14 | done; 15 | 16 | exit 1 17 | -------------------------------------------------------------------------------- /client/bridge.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/go-routeros/routeros" 5 | ) 6 | 7 | // Bridge defines /bridge resource 8 | type Bridge struct { 9 | Id string `mikrotik:".id" codegen:"id,mikrotikID"` 10 | Name string `mikrotik:"name" codegen:"name,required,terraformID"` 11 | FastForward bool `mikrotik:"fast-forward" codegen:"fast_forward"` 12 | VlanFiltering bool `mikrotik:"vlan-filtering" codegen:"vlan_filtering"` 13 | Comment string `mikrotik:"comment" codegen:"comment"` 14 | } 15 | 16 | var _ Resource = (*Bridge)(nil) 17 | 18 | func (b *Bridge) ActionToCommand(a Action) string { 19 | return map[Action]string{ 20 | Add: "/interface/bridge/add", 21 | Find: "/interface/bridge/print", 22 | Update: "/interface/bridge/set", 23 | Delete: "/interface/bridge/remove", 24 | }[a] 25 | } 26 | 27 | func (b *Bridge) IDField() string { 28 | return ".id" 29 | } 30 | 31 | func (b *Bridge) ID() string { 32 | return b.Id 33 | } 34 | 35 | func (b *Bridge) SetID(id string) { 36 | b.Id = id 37 | } 38 | 39 | func (b *Bridge) AfterAddHook(r *routeros.Reply) { 40 | b.Id = r.Done.Map["ret"] 41 | } 42 | 43 | func (b *Bridge) FindField() string { 44 | return "name" 45 | } 46 | 47 | func (b *Bridge) FindFieldValue() string { 48 | return b.Name 49 | } 50 | 51 | func (b *Bridge) DeleteField() string { 52 | return "numbers" 53 | } 54 | 55 | func (b *Bridge) DeleteFieldValue() string { 56 | return b.Name 57 | } 58 | 59 | // Typed wrappers 60 | func (c Mikrotik) AddBridge(r *Bridge) (*Bridge, error) { 61 | res, err := c.Add(r) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | return res.(*Bridge), nil 67 | } 68 | 69 | func (c Mikrotik) UpdateBridge(r *Bridge) (*Bridge, error) { 70 | res, err := c.Update(r) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | return res.(*Bridge), nil 76 | } 77 | 78 | func (c Mikrotik) FindBridge(name string) (*Bridge, error) { 79 | res, err := c.Find(&Bridge{Name: name}) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | return res.(*Bridge), nil 85 | } 86 | 87 | func (c Mikrotik) DeleteBridge(name string) error { 88 | return c.Delete(&Bridge{Name: name}) 89 | } 90 | -------------------------------------------------------------------------------- /client/bridge_port.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/go-routeros/routeros" 5 | ) 6 | 7 | // BridgePort defines port-in-bridge association 8 | type BridgePort struct { 9 | Id string `mikrotik:".id" codegen:"id,mikrotikID"` 10 | Bridge string `mikrotik:"bridge" codegen:"bridge"` 11 | Interface string `mikrotik:"interface" codegen:"interface"` 12 | PVId int `mikrotik:"pvid" codegen:"pvid"` 13 | Comment string `mikrotik:"comment" codegen:"comment"` 14 | } 15 | 16 | var _ Resource = (*BridgePort)(nil) 17 | 18 | func (b *BridgePort) ActionToCommand(a Action) string { 19 | return map[Action]string{ 20 | Add: "/interface/bridge/port/add", 21 | Find: "/interface/bridge/port/print", 22 | Update: "/interface/bridge/port/set", 23 | Delete: "/interface/bridge/port/remove", 24 | }[a] 25 | } 26 | 27 | func (b *BridgePort) IDField() string { 28 | return ".id" 29 | } 30 | 31 | func (b *BridgePort) ID() string { 32 | return b.Id 33 | } 34 | 35 | func (b *BridgePort) SetID(id string) { 36 | b.Id = id 37 | } 38 | 39 | func (b *BridgePort) AfterAddHook(r *routeros.Reply) { 40 | b.Id = r.Done.Map["ret"] 41 | } 42 | 43 | func (b *BridgePort) DeleteField() string { 44 | return "numbers" 45 | } 46 | 47 | func (b *BridgePort) DeleteFieldValue() string { 48 | return b.Id 49 | } 50 | 51 | // Typed wrappers 52 | func (c Mikrotik) AddBridgePort(r *BridgePort) (*BridgePort, error) { 53 | res, err := c.Add(r) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | return res.(*BridgePort), nil 59 | } 60 | 61 | func (c Mikrotik) UpdateBridgePort(r *BridgePort) (*BridgePort, error) { 62 | res, err := c.Update(r) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | return res.(*BridgePort), nil 68 | } 69 | 70 | func (c Mikrotik) FindBridgePort(id string) (*BridgePort, error) { 71 | res, err := c.Find(&BridgePort{Id: id}) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | return res.(*BridgePort), nil 77 | } 78 | 79 | func (c Mikrotik) DeleteBridgePort(id string) error { 80 | return c.Delete(&BridgePort{Id: id}) 81 | } 82 | -------------------------------------------------------------------------------- /client/bridge_port_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestBridgePort_basic(t *testing.T) { 11 | c := NewClient(GetConfigFromEnv()) 12 | bridge, err := c.AddBridge(&Bridge{ 13 | Name: "test_bridge", 14 | }) 15 | if err != nil { 16 | t.Fatal(err) 17 | return 18 | } 19 | defer func() { 20 | if err := c.DeleteBridge(bridge.Name); err != nil { 21 | t.Error(err) 22 | } 23 | }() 24 | 25 | bridgePort, err := c.AddBridgePort(&BridgePort{ 26 | Bridge: bridge.Name, 27 | Interface: "*0", 28 | }) 29 | require.NoError(t, err) 30 | 31 | defer func() { 32 | c.DeleteBridgePort(bridgePort.Id) 33 | require.NoError(t, err) 34 | 35 | _, err = c.FindBridgePort(bridgePort.Id) 36 | require.True(t, IsNotFoundError(err), "expected to get NotFound error") 37 | }() 38 | 39 | expected := &BridgePort{ 40 | Id: bridgePort.Id, 41 | Bridge: "test_bridge", 42 | Interface: "*0", 43 | PVId: 1, 44 | Comment: bridgePort.Comment, 45 | } 46 | if !reflect.DeepEqual(expected, bridgePort) { 47 | t.Errorf(`expected and actual bridge port objects are not equal: 48 | want: %+v, 49 | got: %+v 50 | `, expected, bridgePort) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /client/bridge_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestBridgeBasic(t *testing.T) { 9 | c := NewClient(GetConfigFromEnv()) 10 | 11 | name := "test_bridge" 12 | bridge := &Bridge{ 13 | Name: name, 14 | FastForward: false, 15 | VlanFiltering: false, 16 | Comment: "a test bridge", 17 | } 18 | _, err := c.AddBridge(bridge) 19 | if err != nil { 20 | t.Fatalf("expected no error, got %v", err) 21 | } 22 | 23 | found, err := c.FindBridge(name) 24 | if err != nil { 25 | t.Fatalf("expected no error, got %v", err) 26 | } 27 | bridge.Id = found.Id 28 | if !reflect.DeepEqual(bridge, found) { 29 | t.Fatalf("expected found resource to have pre-defined fields but it didn't") 30 | } 31 | 32 | updatedResource := &Bridge{ 33 | Id: found.Id, 34 | Name: found.Name + "_updated", 35 | FastForward: true, 36 | VlanFiltering: true, 37 | Comment: "updated comment", 38 | } 39 | _, err = c.UpdateBridge(updatedResource) 40 | if err != nil { 41 | t.Fatalf("expected no error, got %v", err) 42 | } 43 | foundAfterUpdate, err := c.FindBridge(updatedResource.Name) 44 | if err != nil { 45 | t.Fatalf("expected no error, got %v", err) 46 | } 47 | if !reflect.DeepEqual(updatedResource, foundAfterUpdate) { 48 | t.Fatalf("expected found resource to have pre-defined fields but it didn't") 49 | } 50 | 51 | if err = c.DeleteBridge(name); err != nil { 52 | t.Fatalf("expected no error, got %v", err) 53 | } 54 | 55 | if err = c.DeleteBridge(name); err == nil { 56 | t.Fatal("expected notfound error, got nothing") 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /client/bridge_vlan.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/ddelnano/terraform-provider-mikrotik/client/types" 5 | "github.com/go-routeros/routeros" 6 | ) 7 | 8 | // BridgeVlan defines vlan filtering in bridge resource 9 | type BridgeVlan struct { 10 | Id string `mikrotik:".id" codegen:"id,mikrotikID"` 11 | Bridge string `mikrotik:"bridge" codegen:"bridge,required"` 12 | Tagged types.MikrotikList `mikrotik:"tagged" codegen:"tagged,elemType=String"` 13 | Untagged types.MikrotikList `mikrotik:"untagged" codegen:"untagged,elemType=String"` 14 | VlanIds types.MikrotikIntList `mikrotik:"vlan-ids" codegen:"vlan_ids,elemType=Int64"` 15 | } 16 | 17 | var _ Resource = (*BridgeVlan)(nil) 18 | 19 | func (b *BridgeVlan) ActionToCommand(a Action) string { 20 | return map[Action]string{ 21 | Add: "/interface/bridge/vlan/add", 22 | Find: "/interface/bridge/vlan/print", 23 | Update: "/interface/bridge/vlan/set", 24 | Delete: "/interface/bridge/vlan/remove", 25 | }[a] 26 | } 27 | 28 | func (b *BridgeVlan) IDField() string { 29 | return ".id" 30 | } 31 | 32 | func (b *BridgeVlan) ID() string { 33 | return b.Id 34 | } 35 | 36 | func (b *BridgeVlan) SetID(id string) { 37 | b.Id = id 38 | } 39 | 40 | func (b *BridgeVlan) AfterAddHook(r *routeros.Reply) { 41 | b.Id = r.Done.Map["ret"] 42 | } 43 | 44 | func (c Mikrotik) AddBridgeVlan(r *BridgeVlan) (*BridgeVlan, error) { 45 | res, err := c.Add(r) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return res.(*BridgeVlan), nil 51 | } 52 | 53 | func (c Mikrotik) UpdateBridgeVlan(r *BridgeVlan) (*BridgeVlan, error) { 54 | res, err := c.Update(r) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | return res.(*BridgeVlan), nil 60 | } 61 | 62 | func (c Mikrotik) FindBridgeVlan(id string) (*BridgeVlan, error) { 63 | res, err := c.Find(&BridgeVlan{Id: id}) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | return res.(*BridgeVlan), nil 69 | } 70 | 71 | func (c Mikrotik) DeleteBridgeVlan(id string) error { 72 | return c.Delete(&BridgeVlan{Id: id}) 73 | } 74 | -------------------------------------------------------------------------------- /client/bridge_vlan_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestBridgeVlanBasic(t *testing.T) { 11 | c := NewClient(GetConfigFromEnv()) 12 | 13 | bridge1Name := "test_bridge1_" + RandomString() 14 | bridge1 := &Bridge{ 15 | Name: bridge1Name, 16 | FastForward: false, 17 | VlanFiltering: false, 18 | Comment: "a test bridge", 19 | } 20 | _, err := c.AddBridge(bridge1) 21 | if err != nil { 22 | t.Fatalf("expected no error, got %v", err) 23 | } 24 | defer func() { 25 | if err = c.DeleteBridge(bridge1Name); err != nil { 26 | t.Fatalf("expected no error, got %v", err) 27 | } 28 | }() 29 | 30 | bridge2Name := "test_bridge2_" + RandomString() 31 | bridge2 := &Bridge{ 32 | Name: bridge2Name, 33 | FastForward: false, 34 | VlanFiltering: false, 35 | Comment: "a test bridge", 36 | } 37 | _, err = c.AddBridge(bridge2) 38 | if err != nil { 39 | t.Fatalf("expected no error, got %v", err) 40 | } 41 | defer func() { 42 | if err = c.DeleteBridge(bridge2Name); err != nil { 43 | t.Fatalf("expected no error, got %v", err) 44 | } 45 | }() 46 | 47 | bridgeVlan := &BridgeVlan{ 48 | Bridge: bridge1.Name, 49 | VlanIds: []int{10, 20}, 50 | } 51 | 52 | createdBridgeVlan, err := c.AddBridgeVlan(bridgeVlan) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | 57 | expectedBridgeVlan := &BridgeVlan{ 58 | Id: createdBridgeVlan.Id, 59 | Bridge: bridge1Name, 60 | VlanIds: []int{10, 20}, 61 | Tagged: []string{}, 62 | Untagged: []string{}, 63 | } 64 | assert.Equal(t, expectedBridgeVlan, createdBridgeVlan) 65 | 66 | createdBridgeVlan.Bridge = bridge2Name 67 | updatedBridgeVlan, err := c.UpdateBridgeVlan(createdBridgeVlan) 68 | require.NoError(t, err) 69 | 70 | expectedBridgeVlan = &BridgeVlan{ 71 | Id: createdBridgeVlan.Id, 72 | Bridge: bridge2Name, 73 | VlanIds: []int{10, 20}, 74 | Tagged: []string{}, 75 | Untagged: []string{}, 76 | } 77 | assert.Equal(t, expectedBridgeVlan, updatedBridgeVlan) 78 | } 79 | -------------------------------------------------------------------------------- /client/console-inspected/parse.go: -------------------------------------------------------------------------------- 1 | package consoleinspected 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Parse parses definition of inspected console item and extracts items using splitStrategy. 9 | // 10 | // It returns console item struct with its subcommands, commands, arguments, etc. 11 | func Parse(input string, splitStrategy ItemsDefinitionSplitStrategy) (ConsoleItem, error) { 12 | chunks, err := splitStrategy.Split(input) 13 | if err != nil { 14 | return ConsoleItem{}, err 15 | } 16 | 17 | var result ConsoleItem 18 | result.Self = Item{} 19 | 20 | for _, v := range chunks { 21 | item, err := parseItem(v) 22 | if err != nil { 23 | return ConsoleItem{}, err 24 | } 25 | if item.Type == TypeSelf { 26 | result.Self = item 27 | continue 28 | } 29 | switch t := item.NodeType; t { 30 | case NodeTypeDir: 31 | result.Subcommands = append(result.Subcommands, item.Name) 32 | case NodeTypeArg: 33 | result.Arguments = append(result.Arguments, item) 34 | case NodeTypeCommand: 35 | result.Commands = append(result.Commands, item.Name) 36 | default: 37 | return ConsoleItem{}, fmt.Errorf("unknown node type %q", t) 38 | } 39 | } 40 | 41 | return result, nil 42 | } 43 | 44 | func parseItem(input string) (Item, error) { 45 | result := Item{} 46 | for _, v := range strings.Split(input, ";") { 47 | if strings.TrimSpace(v) == "" { 48 | continue 49 | } 50 | if strings.HasPrefix(v, "name=") { 51 | result.Name = strings.TrimPrefix(v, "name=") 52 | } 53 | if strings.HasPrefix(v, "node-type=") { 54 | result.NodeType = NodeType(strings.TrimPrefix(v, "node-type=")) 55 | } 56 | if strings.HasPrefix(v, "type=") { 57 | result.Type = Type(strings.TrimPrefix(v, "type=")) 58 | } 59 | } 60 | 61 | return result, nil 62 | } 63 | -------------------------------------------------------------------------------- /client/console-inspected/parse_test.go: -------------------------------------------------------------------------------- 1 | package consoleinspected 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestParse(t *testing.T) { 10 | testCases := []struct { 11 | name string 12 | input string 13 | expected ConsoleItem 14 | expectedError bool 15 | }{ 16 | { 17 | name: "simple command", 18 | input: "name=add;node-type=cmd;type=self;name=comment;node-type=arg;type=child;name=copy-from;node-type=arg;type=child;", 19 | expected: ConsoleItem{ 20 | Self: Item{ 21 | Name: "add", 22 | NodeType: NodeTypeCommand, 23 | Type: TypeSelf, 24 | }, 25 | Arguments: []Item{ 26 | {Name: "comment", NodeType: NodeTypeArg, Type: TypeChild}, 27 | {Name: "copy-from", NodeType: NodeTypeArg, Type: TypeChild}, 28 | }, 29 | }, 30 | }, 31 | { 32 | name: "command with subcommands", 33 | input: "name=list;node-type=dir;type=self;name=add;node-type=cmd;type=child;name=comment;node-type=cmd;type=child;name=edit;node-type=cmd;type=child;name=export;node-type=cmd;type=child;name=find;node-type=cmd;type=child;name=get;node-type=cmd;type=child;name=member;node-type=dir;type=child;name=print;node-type=cmd;type=child;name=remove;node-type=cmd;type=child;name=reset;node-type=cmd;type=child;name=set;node-type=cmd;type=child", 34 | expected: ConsoleItem{ 35 | Self: Item{ 36 | Name: "list", 37 | NodeType: NodeTypeDir, 38 | Type: TypeSelf, 39 | }, 40 | Commands: []string{ 41 | "add", 42 | "comment", 43 | "edit", 44 | "export", 45 | "find", 46 | "get", 47 | "print", 48 | "remove", 49 | "reset", 50 | "set", 51 | }, 52 | Subcommands: []string{"member"}, 53 | }, 54 | }, 55 | } 56 | for _, tc := range testCases { 57 | t.Run(tc.name, func(t *testing.T) { 58 | item, err := Parse(tc.input, DefaultSplitStrategy) 59 | if !assert.Equal(t, !tc.expectedError, err == nil) || tc.expectedError { 60 | return 61 | } 62 | assert.Equal(t, tc.expected, item) 63 | }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /client/console-inspected/split_strategy.go: -------------------------------------------------------------------------------- 1 | package consoleinspected 2 | 3 | import "strings" 4 | 5 | var DefaultSplitStrategy = splitStrategyFunc(orderedSplit) 6 | 7 | type splitStrategyFunc func(string) ([]string, error) 8 | 9 | func (f splitStrategyFunc) Split(in string) ([]string, error) { 10 | return f(in) 11 | } 12 | 13 | // orderedSplit splits items definition using order of fields. 14 | // 15 | // Each 'name=' key starts a new item definition. 16 | func orderedSplit(in string) ([]string, error) { 17 | result := []string{} 18 | 19 | buf := strings.Builder{} 20 | for _, v := range strings.Split(in, ";") { 21 | if strings.TrimSpace(v) == "" { 22 | continue 23 | } 24 | if strings.HasPrefix(v, "name=") { 25 | if buf.Len() > 0 { 26 | result = append(result, buf.String()) 27 | } 28 | buf.Reset() 29 | } 30 | buf.WriteString(v) 31 | buf.WriteString(";") 32 | } 33 | if buf.Len() > 0 { 34 | result = append(result, buf.String()) 35 | } 36 | 37 | return result, nil 38 | } 39 | -------------------------------------------------------------------------------- /client/console-inspected/types.go: -------------------------------------------------------------------------------- 1 | package consoleinspected 2 | 3 | const ( 4 | // NodeTypeDir represents console menu level. 5 | NodeTypeDir NodeType = "dir" 6 | 7 | // NodeTypeCommand represents console command that can be called. 8 | NodeTypeCommand NodeType = "cmd" 9 | 10 | // NodeTypeArg represents console item that is argument to a command. 11 | NodeTypeArg NodeType = "arg" 12 | 13 | // TypeSelf is console item type for currently inspected item. 14 | TypeSelf Type = "self" 15 | 16 | // TypeChild is console item type of all items within inspected container. 17 | TypeChild Type = "child" 18 | ) 19 | 20 | type ( 21 | // NodeType is dedicated type that holds values of "node-type" field of console item. 22 | NodeType string 23 | 24 | // Type is dedicated type that holds values of "type" field of console item. 25 | Type string 26 | 27 | // Item represents inspected console items. 28 | Item struct { 29 | NodeType NodeType `mikrotik:"node-type"` 30 | Type Type `mikrotik:"type"` 31 | Name string `mikrotik:"name"` 32 | } 33 | 34 | // ConsoleItem represents inspected console item with extracted commands, arguments, etc. 35 | ConsoleItem struct { 36 | // Self holds information about current console item. 37 | Self Item 38 | 39 | // Commands holds a list of commands available for this menu level. 40 | // Valid only for ConsoleItem of type NodeTypeDir. 41 | Commands []string 42 | 43 | // Subcommands holds a list of commands for the nested menu level. 44 | // Valid only for ConsoleItem of type NodeTypeDir. 45 | Subcommands []string 46 | 47 | // Arguments holds a list of argument items for a command. 48 | // Valid only for ConsoleItem of type NodeItemCommand. 49 | Arguments []Item 50 | } 51 | ) 52 | 53 | type ( 54 | ItemsDefinitionSplitStrategy interface { 55 | // Split splits set of items definition represented by a single string into chunks of separate item definitions. 56 | Split(string) ([]string, error) 57 | } 58 | ) 59 | -------------------------------------------------------------------------------- /client/console_inspect.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "strings" 5 | 6 | consoleinspected "github.com/ddelnano/terraform-provider-mikrotik/client/console-inspected" 7 | ) 8 | 9 | func (c Mikrotik) InspectConsoleCommand(command string) (consoleinspected.ConsoleItem, error) { 10 | client, err := c.getMikrotikClient() 11 | if err != nil { 12 | return consoleinspected.ConsoleItem{}, err 13 | } 14 | normalizedCommand := strings.ReplaceAll(command[1:], "/", ",") 15 | cmd := []string{"/console/inspect", "as-value", "=path=" + normalizedCommand, "=request=child"} 16 | reply, err := client.RunArgs(cmd) 17 | if err != nil { 18 | return consoleinspected.ConsoleItem{}, err 19 | } 20 | var items []consoleinspected.Item 21 | var result consoleinspected.ConsoleItem 22 | if err := Unmarshal(*reply, &items); err != nil { 23 | return consoleinspected.ConsoleItem{}, err 24 | } 25 | 26 | for _, v := range items { 27 | if v.Type == consoleinspected.TypeSelf { 28 | result.Self = v 29 | continue 30 | } 31 | switch v.NodeType { 32 | case consoleinspected.NodeTypeArg: 33 | result.Arguments = append(result.Arguments, v) 34 | case consoleinspected.NodeTypeCommand: 35 | result.Commands = append(result.Commands, v.Name) 36 | case consoleinspected.NodeTypeDir: 37 | result.Subcommands = append(result.Subcommands, v.Name) 38 | } 39 | } 40 | 41 | return result, nil 42 | } 43 | -------------------------------------------------------------------------------- /client/dhcp_server.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/go-routeros/routeros" 5 | ) 6 | 7 | // DhcpServer represents DHCP server resource 8 | type DhcpServer struct { 9 | Id string `mikrotik:".id" codegen:"id,mikrotikID"` 10 | Name string `mikrotik:"name" codegen:"name,terraformID,required"` 11 | Disabled bool `mikrotik:"disabled" codegen:"disabled"` 12 | AddArp bool `mikrotik:"add-arp" codegen:"add_arp"` 13 | AddressPool string `mikrotik:"address-pool" codegen:"address_pool"` 14 | Authoritative string `mikrotik:"authoritative" codegen:"authoritative"` 15 | Interface string `mikrotik:"interface" codegen:"interface"` 16 | LeaseScript string `mikrotik:"lease-script" codegen:"lease_script"` 17 | } 18 | 19 | var _ Resource = (*DhcpServer)(nil) 20 | 21 | func (b *DhcpServer) ActionToCommand(a Action) string { 22 | return map[Action]string{ 23 | Add: "/ip/dhcp-server/add", 24 | Find: "/ip/dhcp-server/print", 25 | Update: "/ip/dhcp-server/set", 26 | Delete: "/ip/dhcp-server/remove", 27 | }[a] 28 | } 29 | 30 | func (b *DhcpServer) IDField() string { 31 | return ".id" 32 | } 33 | 34 | func (b *DhcpServer) ID() string { 35 | return b.Id 36 | } 37 | 38 | func (b *DhcpServer) SetID(id string) { 39 | b.Id = id 40 | } 41 | 42 | func (b *DhcpServer) AfterAddHook(r *routeros.Reply) { 43 | b.Id = r.Done.Map["ret"] 44 | } 45 | 46 | func (b *DhcpServer) FindField() string { 47 | return "name" 48 | } 49 | 50 | func (b *DhcpServer) Normalize(r *routeros.Reply) { 51 | if len(r.Re) < 1 || len(r.Re[0].Map) < 1 { 52 | return 53 | } 54 | 55 | if _, ok := r.Re[0].Map["authoritative"]; !ok { 56 | b.Authoritative = "yes" 57 | } 58 | } 59 | 60 | func (b *DhcpServer) FindFieldValue() string { 61 | return b.Name 62 | } 63 | 64 | func (b *DhcpServer) DeleteField() string { 65 | return "numbers" 66 | } 67 | 68 | func (b *DhcpServer) DeleteFieldValue() string { 69 | return b.Name 70 | } 71 | 72 | // Typed wrappers 73 | func (c Mikrotik) AddDhcpServer(r *DhcpServer) (*DhcpServer, error) { 74 | res, err := c.Add(r) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | return res.(*DhcpServer), nil 80 | } 81 | 82 | func (c Mikrotik) UpdateDhcpServer(r *DhcpServer) (*DhcpServer, error) { 83 | res, err := c.Update(r) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | return res.(*DhcpServer), nil 89 | } 90 | 91 | func (c Mikrotik) FindDhcpServer(name string) (*DhcpServer, error) { 92 | res, err := c.Find(&DhcpServer{Name: name}) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | return res.(*DhcpServer), nil 98 | } 99 | 100 | func (c Mikrotik) DeleteDhcpServer(name string) error { 101 | return c.Delete(&DhcpServer{Name: name}) 102 | } 103 | -------------------------------------------------------------------------------- /client/dhcp_server_network.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "github.com/go-routeros/routeros" 4 | 5 | // DhcpServerNetwork describes network configuration for DHCP server 6 | type DhcpServerNetwork struct { 7 | Id string `mikrotik:".id" codegen:"id,mikrotikID"` 8 | Comment string `mikrotik:"comment" codegen:"comment"` 9 | Address string `mikrotik:"address" codegen:"address"` 10 | Netmask string `mikrotik:"netmask" codegen:"netmask"` 11 | Gateway string `mikrotik:"gateway" codegen:"gateway"` 12 | DnsServer string `mikrotik:"dns-server" codegen:"dns_server"` 13 | } 14 | 15 | var _ Resource = (*DhcpServerNetwork)(nil) 16 | 17 | func (b *DhcpServerNetwork) ActionToCommand(a Action) string { 18 | return map[Action]string{ 19 | Add: "/ip/dhcp-server/network/add", 20 | Find: "/ip/dhcp-server/network/print", 21 | Update: "/ip/dhcp-server/network/set", 22 | Delete: "/ip/dhcp-server/network/remove", 23 | }[a] 24 | } 25 | 26 | func (b *DhcpServerNetwork) IDField() string { 27 | return ".id" 28 | } 29 | 30 | func (b *DhcpServerNetwork) ID() string { 31 | return b.Id 32 | } 33 | 34 | func (b *DhcpServerNetwork) SetID(id string) { 35 | b.Id = id 36 | } 37 | 38 | func (b *DhcpServerNetwork) AfterAddHook(r *routeros.Reply) { 39 | b.Id = r.Done.Map["ret"] 40 | } 41 | 42 | // Typed wrappers 43 | func (c Mikrotik) AddDhcpServerNetwork(r *DhcpServerNetwork) (*DhcpServerNetwork, error) { 44 | res, err := c.Add(r) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | return res.(*DhcpServerNetwork), nil 50 | } 51 | 52 | func (c Mikrotik) UpdateDhcpServerNetwork(r *DhcpServerNetwork) (*DhcpServerNetwork, error) { 53 | res, err := c.Update(r) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | return res.(*DhcpServerNetwork), nil 59 | } 60 | 61 | func (c Mikrotik) FindDhcpServerNetwork(id string) (*DhcpServerNetwork, error) { 62 | res, err := c.Find(&DhcpServerNetwork{Id: id}) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | return res.(*DhcpServerNetwork), nil 68 | } 69 | 70 | func (c Mikrotik) DeleteDhcpServerNetwork(id string) error { 71 | return c.Delete(&DhcpServerNetwork{Id: id}) 72 | } 73 | -------------------------------------------------------------------------------- /client/dhcp_server_network_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "testing" 4 | 5 | func TestAddDhcpServerNetworkUpdateAndDelete(t *testing.T) { 6 | c := NewClient(GetConfigFromEnv()) 7 | 8 | netmask := "255.255.255.0" 9 | network := "192.168.99.0" 10 | dhcpServerNetwork, err := c.AddDhcpServerNetwork(&DhcpServerNetwork{ 11 | Address: network + "/" + netmask, 12 | Netmask: netmask, 13 | Comment: "Created by terraform", 14 | }) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | found, err := c.FindDhcpServerNetwork(dhcpServerNetwork.Id) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | if found.Address != dhcpServerNetwork.Address { 25 | t.Errorf("expected network address to be %q, got %q", dhcpServerNetwork.Address, found.Address) 26 | } 27 | 28 | dhcpServerNetwork.Comment = "updated network" 29 | updated, err := c.UpdateDhcpServerNetwork(dhcpServerNetwork) 30 | if err != nil { 31 | t.Error(err) 32 | } 33 | 34 | if updated.Comment != "updated network" { 35 | t.Errorf("expected comment to be %q, got %q", dhcpServerNetwork.Comment, updated.Comment) 36 | } 37 | 38 | // cleanup 39 | if err := c.DeleteDhcpServerNetwork(dhcpServerNetwork.Id); err != nil { 40 | t.Error(err) 41 | } 42 | 43 | _, err = c.FindDhcpServerNetwork(dhcpServerNetwork.Id) 44 | if err == nil { 45 | t.Error("expected error, got nil") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /client/dhcp_server_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "testing" 4 | 5 | func TestAddDhcpServerUpdateAndDelete(t *testing.T) { 6 | c := NewClient(GetConfigFromEnv()) 7 | 8 | name := "myserver" 9 | disabled := true 10 | dhcpServer, err := c.AddDhcpServer(&DhcpServer{ 11 | Name: name, 12 | Disabled: disabled, 13 | Interface: "*0", 14 | }) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | foundServer, err := c.FindDhcpServer(name) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | if foundServer.Name != name { 25 | t.Errorf("expected server name to be %q, got %q", name, foundServer.Name) 26 | } 27 | 28 | dhcpServer.Name = dhcpServer.Name + "updated" 29 | updatedServer, err := c.UpdateDhcpServer(dhcpServer) 30 | if err != nil { 31 | t.Error(err) 32 | } 33 | 34 | if updatedServer.Name != dhcpServer.Name { 35 | t.Errorf("expected name to be %q, got %q", dhcpServer.Name, updatedServer.Name) 36 | } 37 | 38 | // cleanup 39 | if err := c.DeleteDhcpServer(dhcpServer.Id); err != nil { 40 | t.Error(err) 41 | } 42 | 43 | _, err = c.FindDhcpServer(name) 44 | if err == nil { 45 | t.Error("expected error, got nil") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /client/dns.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/ddelnano/terraform-provider-mikrotik/client/types" 5 | "github.com/go-routeros/routeros" 6 | ) 7 | 8 | type DnsRecord struct { 9 | Id string `mikrotik:".id" codegen:"id,mikrotikID"` 10 | Name string `mikrotik:"name" codegen:"name,terraformID,required"` 11 | Address string `mikrotik:"address" codegen:"address,required"` 12 | Regexp string `mikrotik:"regexp" codegen:"regexp"` 13 | Ttl types.MikrotikDuration `mikrotik:"ttl" codegen:"ttl"` 14 | Comment string `mikrotik:"comment" codegen:"comment"` 15 | } 16 | 17 | func (d *DnsRecord) ActionToCommand(action Action) string { 18 | return map[Action]string{ 19 | Add: "/ip/dns/static/add", 20 | Find: "/ip/dns/static/print", 21 | List: "/ip/dns/static/print", 22 | Update: "/ip/dns/static/set", 23 | Delete: "/ip/dns/static/remove", 24 | }[action] 25 | } 26 | 27 | func (d *DnsRecord) IDField() string { 28 | return ".id" 29 | } 30 | 31 | func (d *DnsRecord) ID() string { 32 | return d.Id 33 | } 34 | 35 | func (d *DnsRecord) SetID(id string) { 36 | d.Id = id 37 | } 38 | 39 | func (d *DnsRecord) AfterAddHook(r *routeros.Reply) { 40 | d.Id = r.Done.Map["ret"] 41 | } 42 | 43 | func (d *DnsRecord) DeleteField() string { 44 | return "numbers" 45 | } 46 | 47 | func (d *DnsRecord) DeleteFieldValue() string { 48 | return d.Id 49 | } 50 | 51 | func (client Mikrotik) AddDnsRecord(d *DnsRecord) (*DnsRecord, error) { 52 | res, err := client.Add(d) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | return res.(*DnsRecord), nil 58 | } 59 | 60 | func (client Mikrotik) FindDnsRecord(name string) (*DnsRecord, error) { 61 | res, err := client.Find(&FindByFieldWrapper{ 62 | Resource: &DnsRecord{Name: name}, 63 | field: "name", 64 | fieldValueFunc: func() string { return name }, 65 | }) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | return res.(*DnsRecord), nil 71 | } 72 | 73 | func (client Mikrotik) UpdateDnsRecord(d *DnsRecord) (*DnsRecord, error) { 74 | res, err := client.Update(d) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | return res.(*DnsRecord), nil 80 | } 81 | 82 | func (client Mikrotik) DeleteDnsRecord(id string) error { 83 | return client.Delete(&DnsRecord{Id: id}) 84 | } 85 | -------------------------------------------------------------------------------- /client/dns_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestFindDnsRecord_onNonExistantDnsRecord(t *testing.T) { 12 | c := NewClient(GetConfigFromEnv()) 13 | 14 | name := "dns record does not exist" 15 | _, err := c.FindDnsRecord(name) 16 | 17 | require.Truef(t, IsNotFoundError(err), 18 | "Expecting to receive NotFound error for dns record %q", name) 19 | } 20 | 21 | func TestDnsRecord_basic(t *testing.T) { 22 | c := NewClient(GetConfigFromEnv()) 23 | 24 | recordName := "new_record" 25 | record := &DnsRecord{ 26 | Name: recordName, 27 | Address: "10.10.10.200", 28 | Ttl: 300, 29 | Comment: "new record from test", 30 | } 31 | 32 | created, err := c.Add(record) 33 | if err != nil { 34 | t.Errorf("expected no error, got %v", err) 35 | return 36 | } 37 | 38 | found, err := c.Find(&DnsRecord{Id: created.ID()}) 39 | require.NoError(t, err) 40 | 41 | if !reflect.DeepEqual(created, found) { 42 | t.Error("expected created and found resources to be equal, but they don't") 43 | } 44 | 45 | created.(*DnsRecord).Comment = "updated comment" 46 | _, err = c.Update(created) 47 | require.NoError(t, err) 48 | found, err = c.Find(&DnsRecord{Id: created.ID()}) 49 | require.NoError(t, err) 50 | assert.Equal(t, created, found) 51 | 52 | err = c.Delete(found) 53 | assert.NoError(t, err) 54 | 55 | _, err = c.Find(&DnsRecord{Id: created.ID()}) 56 | require.Error(t, err) 57 | 58 | require.True(t, IsNotFoundError(err), 59 | "expected to get NotFound error") 60 | } 61 | 62 | func TestDns_Regexp(t *testing.T) { 63 | c := NewClient(GetConfigFromEnv()) 64 | 65 | recordName := "new_record" 66 | record := &DnsRecord{ 67 | Name: recordName, 68 | Regexp: ".*\\.domain\\.com", 69 | Address: "10.10.10.200", 70 | Ttl: 300, 71 | Comment: "new record from test", 72 | } 73 | 74 | _, err := c.Add(record) 75 | require.Error(t, err, "usage of 'name' and 'regexp' at the same type should result in error") 76 | 77 | regexRecord := &DnsRecord{ 78 | Address: "10.10.10.201", 79 | Ttl: 300, 80 | Regexp: ".+\\.domain\\.com", 81 | Comment: "new record from test", 82 | } 83 | regexCreated, err := c.Add(regexRecord) 84 | require.NoError(t, err) 85 | defer func() { 86 | _ = c.Delete(regexCreated) 87 | }() 88 | assert.Equal(t, regexRecord, regexCreated) 89 | } 90 | -------------------------------------------------------------------------------- /client/errors.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "errors" 4 | 5 | type NotFound struct { 6 | s string 7 | } 8 | 9 | func NewNotFound(text string) error { 10 | return NotFound{text} 11 | } 12 | 13 | func (e NotFound) Error() string { 14 | return e.s 15 | } 16 | 17 | func IsNotFoundError(err error) bool { 18 | var e NotFound 19 | var ePtr *NotFound 20 | 21 | return errors.As(err, &e) || errors.As(err, &ePtr) 22 | } 23 | -------------------------------------------------------------------------------- /client/errors_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestIsNotFoundError(t *testing.T) { 12 | 13 | testCases := []struct { 14 | name string 15 | err error 16 | expected bool 17 | }{ 18 | { 19 | name: "nil error", 20 | expected: false, 21 | }, 22 | { 23 | name: "created via NewNotFoundError()", 24 | err: NewNotFound("not found"), 25 | expected: true, 26 | }, 27 | { 28 | name: "created directly via struct initialization", 29 | err: NotFound{}, 30 | expected: true, 31 | }, 32 | { 33 | name: "chained with other errors", 34 | err: fmt.Errorf("cannot load object info: %w", NewNotFound("no such object")), 35 | expected: true, 36 | }, 37 | { 38 | name: "chain of non-matching errors", 39 | err: fmt.Errorf("cannot load object info: %w", errors.New("no such object")), 40 | expected: false, 41 | }, 42 | { 43 | name: "generic error", 44 | err: errors.New("no such object"), 45 | expected: false, 46 | }, 47 | } 48 | for _, tc := range testCases { 49 | t.Run(tc.name, func(t *testing.T) { 50 | result := IsNotFoundError(tc.err) 51 | require.Equal(t, tc.expected, result) 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /client/firewall_filter.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/ddelnano/terraform-provider-mikrotik/client/types" 5 | "github.com/go-routeros/routeros" 6 | ) 7 | 8 | // FirewallFilterRule defines /ip/firewall/filter rule 9 | type FirewallFilterRule struct { 10 | Id string `mikrotik:".id" codegen:"id,mikrotikID,terraformID"` 11 | Action string `mikrotik:"action" codegen:"action"` 12 | Chain string `mikrotik:"chain" codegen:"chain,required"` 13 | Comment string `mikrotik:"comment" codegen:"comment"` 14 | ConnectionState types.MikrotikList `mikrotik:"connection-state" codegen:"connection_state"` 15 | DestPort string `mikrotik:"dst-port" codegen:"dst_port"` 16 | InInterface string `mikrotik:"in-interface" codegen:"in_interface"` 17 | InInterfaceList string `mikrotik:"in-interface-list" codegen:"in_interface_list"` 18 | OutInterfaceList string `mikrotik:"out-interface-list" codegen:"out_interface_list"` 19 | Protocol string `mikrotik:"protocol" codegen:"protocol"` 20 | } 21 | 22 | var _ Resource = (*FirewallFilterRule)(nil) 23 | 24 | func (b *FirewallFilterRule) ActionToCommand(a Action) string { 25 | return map[Action]string{ 26 | Add: "/ip/firewall/filter/add", 27 | Find: "/ip/firewall/filter/print", 28 | Update: "/ip/firewall/filter/set", 29 | Delete: "/ip/firewall/filter/remove", 30 | }[a] 31 | } 32 | 33 | func (b *FirewallFilterRule) IDField() string { 34 | return ".id" 35 | } 36 | 37 | func (b *FirewallFilterRule) ID() string { 38 | return b.Id 39 | } 40 | 41 | func (b *FirewallFilterRule) SetID(id string) { 42 | b.Id = id 43 | } 44 | 45 | func (b *FirewallFilterRule) AfterAddHook(r *routeros.Reply) { 46 | b.Id = r.Done.Map["ret"] 47 | } 48 | 49 | func (c Mikrotik) AddFirewallFilterRule(r *FirewallFilterRule) (*FirewallFilterRule, error) { 50 | res, err := c.Add(r) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | return res.(*FirewallFilterRule), nil 56 | } 57 | 58 | func (c Mikrotik) UpdateFirewallFilterRule(r *FirewallFilterRule) (*FirewallFilterRule, error) { 59 | res, err := c.Update(r) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | return res.(*FirewallFilterRule), nil 65 | } 66 | 67 | func (c Mikrotik) FindFirewallFilterRule(id string) (*FirewallFilterRule, error) { 68 | res, err := c.Find(&FirewallFilterRule{Id: id}) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | return res.(*FirewallFilterRule), nil 74 | } 75 | 76 | func (c Mikrotik) DeleteFirewallFilterRule(id string) error { 77 | return c.Delete(&FirewallFilterRule{Id: id}) 78 | } 79 | -------------------------------------------------------------------------------- /client/firewall_filter_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ddelnano/terraform-provider-mikrotik/client/types" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestFirewallFilter_customChain(t *testing.T) { 12 | c := NewClient(GetConfigFromEnv()) 13 | 14 | rule := &FirewallFilterRule{ 15 | Chain: "mychain", 16 | Comment: "Test rule", 17 | DestPort: "1001", 18 | ConnectionState: types.MikrotikList{"new"}, 19 | Protocol: "tcp", 20 | } 21 | 22 | createdRule, err := c.AddFirewallFilterRule(rule) 23 | require.NoError(t, err) 24 | 25 | defer func(id string) { 26 | assert.NoError(t, c.DeleteFirewallFilterRule(id)) 27 | }(createdRule.Id) 28 | 29 | rule.Id = createdRule.Id 30 | 31 | foundRule, err := c.FindFirewallFilterRule(createdRule.Id) 32 | require.NoError(t, err) 33 | assert.Equal(t, rule, foundRule) 34 | } 35 | 36 | func TestFirewallFilter_builtinChain(t *testing.T) { 37 | c := NewClient(GetConfigFromEnv()) 38 | 39 | rule := &FirewallFilterRule{ 40 | Chain: "filter", 41 | Comment: "Test rule for builtin chain", 42 | DestPort: "1001", 43 | ConnectionState: types.MikrotikList{"established", "related"}, 44 | Protocol: "tcp", 45 | } 46 | 47 | createdRule, err := c.AddFirewallFilterRule(rule) 48 | require.NoError(t, err) 49 | 50 | defer func(id string) { 51 | assert.NoError(t, c.DeleteFirewallFilterRule(id)) 52 | }(createdRule.Id) 53 | 54 | rule.Id = createdRule.Id 55 | 56 | foundRule, err := c.FindFirewallFilterRule(rule.Id) 57 | require.NoError(t, err) 58 | assert.Equal(t, rule, foundRule) 59 | 60 | rule.Protocol = "udp" 61 | rule.Comment = "Updated protocol" 62 | _, err = c.UpdateFirewallFilterRule(rule) 63 | require.NoError(t, err) 64 | 65 | foundRule, err = c.FindFirewallFilterRule(rule.Id) 66 | require.NoError(t, err) 67 | assert.Equal(t, rule, foundRule) 68 | } 69 | -------------------------------------------------------------------------------- /client/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ddelnano/terraform-provider-mikrotik/client 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/go-routeros/routeros v0.0.0-20210123142807-2a44d57c6730 7 | github.com/stretchr/testify v1.8.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /client/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/go-routeros/routeros v0.0.0-20210123142807-2a44d57c6730 h1:EuqwWLv/LPPjhvFqkeD2bz+FOlvw2DjvDI7vK8GVeyY= 5 | github.com/go-routeros/routeros v0.0.0-20210123142807-2a44d57c6730/go.mod h1:em1mEqFKnoeQuQP9Sg7i26yaW8o05WwcNj7yLhrXxSQ= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 10 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 11 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 12 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 16 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 17 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 18 | -------------------------------------------------------------------------------- /client/helpers.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func getRouterOSMajorVersion(systemResources SystemResources) (majorVersion int, err error) { 11 | if len(systemResources.Version) == 0 { 12 | return 0, errors.New("RouterOS system resources returned empty string") 13 | } 14 | majorVersion, err = strconv.Atoi(string(systemResources.Version[0])) 15 | return 16 | } 17 | 18 | func SkipIfRouterOSV6OrEarlier(t *testing.T, systemResources SystemResources) { 19 | majorVersion, err := getRouterOSMajorVersion(systemResources) 20 | if err != nil { 21 | t.Errorf("failed to get the system resource major version: %v", err) 22 | } 23 | if majorVersion <= 6 { 24 | t.Skip() 25 | } 26 | } 27 | 28 | func SkipIfRouterOSV7OrLater(t *testing.T, systemResources SystemResources) { 29 | majorVersion, err := getRouterOSMajorVersion(systemResources) 30 | if err != nil { 31 | t.Errorf("failed to get the system resource major version: %v", err) 32 | } 33 | if majorVersion >= 7 { 34 | t.Skip() 35 | } 36 | } 37 | 38 | // RandomString returns a random string 39 | func RandomString() string { 40 | // a naive implementation with all-digits for now 41 | return strconv.FormatInt(time.Now().UTC().UnixNano(), 10) 42 | } 43 | -------------------------------------------------------------------------------- /client/interface_list.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/go-routeros/routeros" 5 | ) 6 | 7 | // InterfaceList manages a list of interfaces 8 | type InterfaceList struct { 9 | Id string `mikrotik:".id" codegen:"id,mikrotikID"` 10 | Comment string `mikrotik:"comment" codegen:"comment"` 11 | Name string `mikrotik:"name" codegen:"name,terraformID,required"` 12 | } 13 | 14 | var _ Resource = (*InterfaceList)(nil) 15 | 16 | func (b *InterfaceList) ActionToCommand(a Action) string { 17 | return map[Action]string{ 18 | Add: "/interface/list/add", 19 | Find: "/interface/list/print", 20 | Update: "/interface/list/set", 21 | Delete: "/interface/list/remove", 22 | }[a] 23 | } 24 | 25 | func (b *InterfaceList) IDField() string { 26 | return ".id" 27 | } 28 | 29 | func (b *InterfaceList) ID() string { 30 | return b.Id 31 | } 32 | 33 | func (b *InterfaceList) SetID(id string) { 34 | b.Id = id 35 | } 36 | 37 | // Uncomment extra methods to satisfy more interfaces 38 | 39 | func (b *InterfaceList) AfterAddHook(r *routeros.Reply) { 40 | b.Id = r.Done.Map["ret"] 41 | } 42 | 43 | func (b *InterfaceList) FindField() string { 44 | return "name" 45 | } 46 | 47 | func (b *InterfaceList) FindFieldValue() string { 48 | return b.Name 49 | } 50 | 51 | func (b *InterfaceList) DeleteField() string { 52 | return "numbers" 53 | } 54 | 55 | func (b *InterfaceList) DeleteFieldValue() string { 56 | return b.Name 57 | } 58 | 59 | // Typed wrappers 60 | func (c Mikrotik) AddInterfaceList(r *InterfaceList) (*InterfaceList, error) { 61 | res, err := c.Add(r) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | return res.(*InterfaceList), nil 67 | } 68 | 69 | func (c Mikrotik) UpdateInterfaceList(r *InterfaceList) (*InterfaceList, error) { 70 | res, err := c.Update(r) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | return res.(*InterfaceList), nil 76 | } 77 | 78 | func (c Mikrotik) FindInterfaceList(name string) (*InterfaceList, error) { 79 | res, err := c.Find(&InterfaceList{Name: name}) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | return res.(*InterfaceList), nil 85 | } 86 | 87 | func (c Mikrotik) DeleteInterfaceList(name string) error { 88 | return c.Delete(&InterfaceList{Name: name}) 89 | } 90 | -------------------------------------------------------------------------------- /client/interface_list_member.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/go-routeros/routeros" 5 | ) 6 | 7 | // InterfaceListMember manages an interface list's members 8 | type InterfaceListMember struct { 9 | Id string `mikrotik:".id" codegen:"id,mikrotikID"` 10 | Interface string `mikrotik:"interface" codegen:"interface,required"` 11 | List string `mikrotik:"list" codegen:"list,required"` 12 | } 13 | 14 | var _ Resource = (*InterfaceListMember)(nil) 15 | 16 | func (b *InterfaceListMember) ActionToCommand(a Action) string { 17 | return map[Action]string{ 18 | Add: "/interface/list/member/add", 19 | Find: "/interface/list/member/print", 20 | Update: "/interface/list/member/set", 21 | Delete: "/interface/list/member/remove", 22 | }[a] 23 | } 24 | 25 | func (b *InterfaceListMember) IDField() string { 26 | return ".id" 27 | } 28 | 29 | func (b *InterfaceListMember) ID() string { 30 | return b.Id 31 | } 32 | 33 | func (b *InterfaceListMember) SetID(id string) { 34 | b.Id = id 35 | } 36 | 37 | func (b *InterfaceListMember) AfterAddHook(r *routeros.Reply) { 38 | b.Id = r.Done.Map["ret"] 39 | } 40 | 41 | func (b *InterfaceListMember) DeleteField() string { 42 | return "numbers" 43 | } 44 | 45 | func (b *InterfaceListMember) DeleteFieldValue() string { 46 | return b.Id 47 | } 48 | 49 | // Typed wrappers 50 | func (c Mikrotik) AddInterfaceListMember(r *InterfaceListMember) (*InterfaceListMember, error) { 51 | res, err := c.Add(r) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | return res.(*InterfaceListMember), nil 57 | } 58 | 59 | func (c Mikrotik) UpdateInterfaceListMember(r *InterfaceListMember) (*InterfaceListMember, error) { 60 | res, err := c.Update(r) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | return res.(*InterfaceListMember), nil 66 | } 67 | 68 | func (c Mikrotik) FindInterfaceListMember(id string) (*InterfaceListMember, error) { 69 | res, err := c.Find(&InterfaceListMember{Id: id}) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | return res.(*InterfaceListMember), nil 75 | } 76 | 77 | func (c Mikrotik) DeleteInterfaceListMember(id string) error { 78 | return c.Delete(&InterfaceListMember{Id: id}) 79 | } 80 | -------------------------------------------------------------------------------- /client/interface_list_member_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "testing" 4 | 5 | func TestAddInterfaceListMemberUpdateAndDelete(t *testing.T) { 6 | c := NewClient(GetConfigFromEnv()) 7 | 8 | listName := "test_list" 9 | 10 | list, err := c.AddInterfaceList(&InterfaceList{ 11 | Name: listName, 12 | }) 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | defer func() { 17 | if err := c.DeleteInterfaceList(list.Id); err != nil { 18 | t.Error(err) 19 | } 20 | }() 21 | 22 | listMember, err := c.AddInterfaceListMember(&InterfaceListMember{ 23 | List: list.Name, 24 | Interface: "*0", 25 | }) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | defer func() { 30 | if err := c.DeleteInterfaceListMember(listMember.Id); err != nil { 31 | t.Error(err) 32 | } 33 | if m, err := c.FindInterfaceListMember(listMember.Id); err == nil || m != nil { 34 | t.Errorf("expected error to be present and list member to be nil") 35 | } 36 | }() 37 | 38 | found, err := c.FindInterfaceListMember(listMember.Id) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | if found.List != list.Name { 44 | t.Errorf("expected name to be %q, got %q", list.Name, found.List) 45 | } 46 | 47 | listMember.Interface = "ether1" 48 | updated, err := c.UpdateInterfaceListMember(listMember) 49 | if err != nil { 50 | t.Error(err) 51 | } 52 | 53 | if updated.Interface != "ether1" { 54 | t.Errorf("expected updated interface to be %q, got %q", listMember.Interface, updated.Interface) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /client/interface_list_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "testing" 4 | 5 | func TestAddInterfaceListUpdateAndDelete(t *testing.T) { 6 | c := NewClient(GetConfigFromEnv()) 7 | 8 | list, err := c.AddInterfaceList(&InterfaceList{ 9 | Name: "mylist", 10 | Comment: "Created by terraform", 11 | }) 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | 16 | found, err := c.FindInterfaceList(list.Name) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | if found.Name != list.Name { 22 | t.Errorf("expected name to be %q, got %q", list.Name, found.Name) 23 | } 24 | 25 | list.Comment = "updated list" 26 | updated, err := c.UpdateInterfaceList(list) 27 | if err != nil { 28 | t.Error(err) 29 | } 30 | 31 | if updated.Comment != "updated list" { 32 | t.Errorf("expected comment to be %q, got %q", list.Comment, updated.Comment) 33 | } 34 | 35 | // cleanup 36 | if err := c.DeleteInterfaceList(list.Name); err != nil { 37 | t.Error(err) 38 | } 39 | 40 | _, err = c.FindInterfaceList(list.Name) 41 | if err == nil { 42 | t.Error("expected error, got nil") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /client/interface_wireguard.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/go-routeros/routeros" 5 | ) 6 | 7 | type InterfaceWireguard struct { 8 | Id string `mikrotik:".id"` 9 | Name string `mikrotik:"name"` 10 | Comment string `mikrotik:"comment"` 11 | Disabled bool `mikrotik:"disabled"` 12 | ListenPort int `mikrotik:"listen-port"` 13 | Mtu int `mikrotik:"mtu"` 14 | PrivateKey string `mikrotik:"private-key"` 15 | PublicKey string `mikrotik:"public-key,readonly"` //read only property 16 | Running bool `mikrotik:"running,readonly"` //read only property 17 | } 18 | 19 | func (i *InterfaceWireguard) ActionToCommand(action Action) string { 20 | return map[Action]string{ 21 | Add: "/interface/wireguard/add", 22 | Find: "/interface/wireguard/print", 23 | List: "/interface/wireguard/print", 24 | Update: "/interface/wireguard/set", 25 | Delete: "/interface/wireguard/remove", 26 | }[action] 27 | } 28 | 29 | func (i *InterfaceWireguard) IDField() string { 30 | return ".id" 31 | } 32 | 33 | func (i *InterfaceWireguard) ID() string { 34 | return i.Id 35 | } 36 | 37 | func (i *InterfaceWireguard) SetID(id string) { 38 | i.Id = id 39 | } 40 | 41 | func (i *InterfaceWireguard) AfterAddHook(r *routeros.Reply) { 42 | i.Id = r.Done.Map["ret"] 43 | } 44 | 45 | func (i *InterfaceWireguard) FindField() string { 46 | return "name" 47 | } 48 | 49 | func (i *InterfaceWireguard) FindFieldValue() string { 50 | return i.Name 51 | } 52 | 53 | func (i *InterfaceWireguard) DeleteField() string { 54 | return "numbers" 55 | } 56 | 57 | func (i *InterfaceWireguard) DeleteFieldValue() string { 58 | return i.Name 59 | } 60 | 61 | func (client Mikrotik) AddInterfaceWireguard(i *InterfaceWireguard) (*InterfaceWireguard, error) { 62 | res, err := client.Add(i) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | return res.(*InterfaceWireguard), nil 68 | } 69 | 70 | func (client Mikrotik) FindInterfaceWireguard(name string) (*InterfaceWireguard, error) { 71 | res, err := client.Find(&InterfaceWireguard{Name: name}) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | return res.(*InterfaceWireguard), nil 77 | } 78 | 79 | func (client Mikrotik) UpdateInterfaceWireguard(i *InterfaceWireguard) (*InterfaceWireguard, error) { 80 | res, err := client.Update(i) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | return res.(*InterfaceWireguard), nil 86 | } 87 | 88 | func (client Mikrotik) DeleteInterfaceWireguard(name string) error { 89 | return client.Delete(&InterfaceWireguard{Name: name}) 90 | } 91 | -------------------------------------------------------------------------------- /client/interface_wireguard_peer.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/ddelnano/terraform-provider-mikrotik/client/types" 5 | "github.com/go-routeros/routeros" 6 | ) 7 | 8 | type InterfaceWireguardPeer struct { 9 | Id string `mikrotik:".id"` 10 | AllowedAddress string `mikrotik:"allowed-address"` 11 | Comment string `mikrotik:"comment"` 12 | Disabled bool `mikrotik:"disabled"` 13 | EndpointAddress string `mikrotik:"endpoint-address"` 14 | EndpointPort int64 `mikrotik:"endpoint-port"` 15 | Interface string `mikrotik:"interface"` 16 | PersistentKeepalive types.MikrotikDuration `mikrotik:"persistent-keepalive"` 17 | PresharedKey string `mikrotik:"preshared-key"` 18 | PublicKey string `mikrotik:"public-key"` 19 | } 20 | 21 | func (i *InterfaceWireguardPeer) ActionToCommand(action Action) string { 22 | return map[Action]string{ 23 | Add: "/interface/wireguard/peers/add", 24 | Find: "/interface/wireguard/peers/print", 25 | List: "/interface/wireguard/peers/print", 26 | Update: "/interface/wireguard/peers/set", 27 | Delete: "/interface/wireguard/peers/remove", 28 | }[action] 29 | } 30 | 31 | func (i *InterfaceWireguardPeer) IDField() string { 32 | return ".id" 33 | } 34 | 35 | func (i *InterfaceWireguardPeer) ID() string { 36 | return i.Id 37 | } 38 | 39 | func (i *InterfaceWireguardPeer) SetID(id string) { 40 | i.Id = id 41 | } 42 | 43 | func (i *InterfaceWireguardPeer) AfterAddHook(r *routeros.Reply) { 44 | i.Id = r.Done.Map["ret"] 45 | } 46 | 47 | func (i *InterfaceWireguardPeer) DeleteField() string { 48 | return "numbers" 49 | } 50 | 51 | func (i *InterfaceWireguardPeer) DeleteFieldValue() string { 52 | return i.Id 53 | } 54 | 55 | func (client Mikrotik) AddInterfaceWireguardPeer(i *InterfaceWireguardPeer) (*InterfaceWireguardPeer, error) { 56 | res, err := client.Add(i) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | return res.(*InterfaceWireguardPeer), nil 62 | } 63 | 64 | func (client Mikrotik) FindInterfaceWireguardPeer(id string) (*InterfaceWireguardPeer, error) { 65 | res, err := client.Find(&InterfaceWireguardPeer{Id: id}) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | return res.(*InterfaceWireguardPeer), nil 71 | } 72 | 73 | func (client Mikrotik) UpdateInterfaceWireguardPeer(i *InterfaceWireguardPeer) (*InterfaceWireguardPeer, error) { 74 | res, err := client.Update(i) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | return res.(*InterfaceWireguardPeer), nil 80 | } 81 | 82 | func (client Mikrotik) DeleteInterfaceWireguardPeer(id string) error { 83 | return client.Delete(&InterfaceWireguardPeer{Id: id}) 84 | } 85 | -------------------------------------------------------------------------------- /client/interface_wireguard_peer_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestFindInterfaceWireguardPeer_onNonExistantInterfacePeer(t *testing.T) { 11 | SkipIfRouterOSV6OrEarlier(t, sysResources) 12 | c := NewClient(GetConfigFromEnv()) 13 | 14 | peerID := "Interface peer does not exist" 15 | _, err := c.FindInterfaceWireguardPeer(peerID) 16 | 17 | require.Truef(t, IsNotFoundError(err), 18 | "Expecting to receive NotFound error for Interface peer `%q`, instead error was nil.", peerID) 19 | } 20 | 21 | func TestInterfaceWireguardPeer_Crud(t *testing.T) { 22 | SkipIfRouterOSV6OrEarlier(t, sysResources) 23 | c := NewClient(GetConfigFromEnv()) 24 | 25 | name := "new_interface_wireguard" 26 | interfaceWireguard := &InterfaceWireguard{ 27 | Name: name, 28 | Disabled: false, 29 | ListenPort: 10000, 30 | Mtu: 10001, 31 | PrivateKey: "YOi0P0lTTiN8hAQvuRET23Srb+U7C52iOZokj0CCSkM=", 32 | Comment: "new interface from test", 33 | } 34 | 35 | createdInterface, err := c.Add(interfaceWireguard) 36 | if err != nil { 37 | t.Errorf("expected no error, got %v", err) 38 | return 39 | } 40 | defer func() { 41 | err = c.Delete(interfaceWireguard) 42 | if err != nil { 43 | t.Errorf("expected no error, got %v", err) 44 | } 45 | }() 46 | 47 | interfaceWireguardPeer := &InterfaceWireguardPeer{ 48 | Interface: createdInterface.(*InterfaceWireguard).Name, 49 | Disabled: false, 50 | AllowedAddress: "0.0.0.0/0", 51 | EndpointPort: 13250, 52 | Comment: "new interface from test", 53 | PublicKey: "/yZWgiYAgNNSy7AIcxuEewYwOVPqJJRKG90s9ypwfiM=", 54 | } 55 | 56 | created, err := c.Add(interfaceWireguardPeer) 57 | require.NoError(t, err) 58 | defer func() { 59 | err = c.Delete(interfaceWireguardPeer) 60 | assert.NoError(t, err) 61 | }() 62 | 63 | findPeer := &InterfaceWireguardPeer{} 64 | findPeer.Id = created.(*InterfaceWireguardPeer).Id 65 | foundPeer, err := c.Find(findPeer) 66 | require.NoError(t, err) 67 | 68 | assert.Equal(t, created, foundPeer) 69 | } 70 | -------------------------------------------------------------------------------- /client/interface_wireguard_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestFindInterfaceWireguard_onNonExistantInterfaceWireguard(t *testing.T) { 11 | SkipIfRouterOSV6OrEarlier(t, sysResources) 12 | c := NewClient(GetConfigFromEnv()) 13 | 14 | name := "Interface wireguard does not exist" 15 | _, err := c.FindInterfaceWireguard(name) 16 | 17 | require.Truef(t, IsNotFoundError(err), 18 | "Expecting to receive NotFound error for Interface wireguard %q.", name) 19 | } 20 | 21 | func TestAddFindDeleteInterfaceWireguard(t *testing.T) { 22 | SkipIfRouterOSV6OrEarlier(t, sysResources) 23 | c := NewClient(GetConfigFromEnv()) 24 | 25 | name := "new_interface_wireguard" 26 | interfaceWireguard := &InterfaceWireguard{ 27 | Name: name, 28 | Disabled: false, 29 | ListenPort: 10000, 30 | Mtu: 10001, 31 | PrivateKey: "YOi0P0lTTiN8hAQvuRET23Srb+U7C52iOZokj0CCSkM=", 32 | Comment: "new interface from test", 33 | } 34 | 35 | created, err := c.Add(interfaceWireguard) 36 | if err != nil { 37 | t.Errorf("expected no error, got %v", err) 38 | return 39 | } 40 | defer func() { 41 | err = c.Delete(interfaceWireguard) 42 | require.NoError(t, err) 43 | 44 | _, err := c.Find(interfaceWireguard) 45 | require.True(t, IsNotFoundError(err), "expected to get NotFound error") 46 | }() 47 | 48 | findInterface := &InterfaceWireguard{} 49 | findInterface.Name = name 50 | found, err := c.Find(findInterface) 51 | if err != nil { 52 | t.Errorf("expected no error, got %v", err) 53 | return 54 | } 55 | 56 | if _, ok := found.(Resource); !ok { 57 | t.Error("expected found resource to implement Resource interface, but it doesn't") 58 | return 59 | } 60 | if !reflect.DeepEqual(created, found) { 61 | t.Error("expected created and found resources to be equal, but they don't") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /client/ip_addr.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/go-routeros/routeros" 5 | ) 6 | 7 | type IpAddress struct { 8 | Id string `mikrotik:".id" codegen:"id,mikrotikID"` 9 | Address string `mikrotik:"address" codegen:"address,required"` 10 | Comment string `mikrotik:"comment" codegen:"comment"` 11 | Disabled bool `mikrotik:"disabled" codegen:"disabled"` 12 | Interface string `mikrotik:"interface" codegen:"interface,required"` 13 | Network string `mikrotik:"network" codegen:"network,computed"` 14 | } 15 | 16 | var _ Resource = (*IpAddress)(nil) 17 | 18 | func (b *IpAddress) ActionToCommand(a Action) string { 19 | return map[Action]string{ 20 | Add: "/ip/address/add", 21 | Find: "/ip/address/print", 22 | Update: "/ip/address/set", 23 | Delete: "/ip/address/remove", 24 | }[a] 25 | } 26 | 27 | func (b *IpAddress) IDField() string { 28 | return ".id" 29 | } 30 | 31 | func (b *IpAddress) ID() string { 32 | return b.Id 33 | } 34 | 35 | func (b *IpAddress) SetID(id string) { 36 | b.Id = id 37 | } 38 | 39 | func (b *IpAddress) AfterAddHook(r *routeros.Reply) { 40 | b.Id = r.Done.Map["ret"] 41 | } 42 | 43 | // Typed wrappers 44 | func (c Mikrotik) AddIpAddress(r *IpAddress) (*IpAddress, error) { 45 | res, err := c.Add(r) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return res.(*IpAddress), nil 51 | } 52 | 53 | func (c Mikrotik) UpdateIpAddress(r *IpAddress) (*IpAddress, error) { 54 | res, err := c.Update(r) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | return res.(*IpAddress), nil 60 | } 61 | 62 | func (c Mikrotik) FindIpAddress(id string) (*IpAddress, error) { 63 | res, err := c.Find(&IpAddress{Id: id}) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | return res.(*IpAddress), nil 69 | } 70 | 71 | func (client Mikrotik) ListIpAddress() ([]IpAddress, error) { 72 | res, err := client.List(&IpAddress{}) 73 | if err != nil { 74 | return nil, err 75 | } 76 | returnSlice := make([]IpAddress, len(res)) 77 | for i, v := range res { 78 | returnSlice[i] = *(v.(*IpAddress)) 79 | } 80 | 81 | return returnSlice, nil 82 | } 83 | 84 | func (c Mikrotik) DeleteIpAddress(id string) error { 85 | return c.Delete(&IpAddress{Id: id}) 86 | } 87 | -------------------------------------------------------------------------------- /client/ip_addr_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestAddIpAddressAndDeleteIpAddress(t *testing.T) { 9 | c := NewClient(GetConfigFromEnv()) 10 | 11 | address := "1.1.1.1/24" 12 | comment := "terraform-acc-test" 13 | disabled := false 14 | network := "1.1.1.0" 15 | ifname := "ether1" 16 | updatedComment := "terraform acc test updated" 17 | 18 | expectedIpAddress := &IpAddress{ 19 | Address: address, 20 | Comment: comment, 21 | Disabled: disabled, 22 | Interface: ifname, 23 | Network: network, 24 | } 25 | 26 | ipaddr, err := c.AddIpAddress(expectedIpAddress) 27 | 28 | if err != nil { 29 | t.Errorf("Error creating an ip address with: %v", err) 30 | } 31 | 32 | expectedIpAddress.Id = ipaddr.Id 33 | 34 | if !reflect.DeepEqual(ipaddr, expectedIpAddress) { 35 | t.Errorf("The ip address does not match what we expected. actual: %v expected: %v", ipaddr, expectedIpAddress) 36 | } 37 | 38 | expectedIpAddress.Comment = updatedComment 39 | ipaddr, err = c.UpdateIpAddress(expectedIpAddress) 40 | 41 | if err != nil { 42 | t.Errorf("Error updating an ip address with: %v", err) 43 | } 44 | if !reflect.DeepEqual(ipaddr, expectedIpAddress) { 45 | t.Errorf("The ip address does not match what we expected. actual: %v expected: %v", ipaddr, expectedIpAddress) 46 | } 47 | 48 | foundIpAddress, err := c.FindIpAddress(ipaddr.Id) 49 | 50 | if err != nil { 51 | t.Errorf("Error getting ip address with: %v", err) 52 | } 53 | 54 | if !reflect.DeepEqual(ipaddr, foundIpAddress) { 55 | t.Errorf("Created ip address and found ip address do not match. actual: %v expected: %v", foundIpAddress, ipaddr) 56 | } 57 | 58 | err = c.DeleteIpAddress(ipaddr.Id) 59 | 60 | if err != nil { 61 | t.Errorf("Error deleting ip address with: %v", err) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /client/ipv6_addr.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/go-routeros/routeros" 5 | ) 6 | 7 | // Ipv6Address defines resource 8 | type Ipv6Address struct { 9 | Id string `mikrotik:".id" codegen:"id,mikrotikID,terraformID"` 10 | Address string `mikrotik:"address" codegen:"address,required"` 11 | Advertise bool `mikrotik:"advertise" codegen:"advertise"` 12 | Comment string `mikrotik:"comment" codegen:"comment"` 13 | Disabled bool `mikrotik:"disabled" codegen:"disabled"` 14 | Eui64 bool `mikrotik:"eui-64" codegen:"eui_64"` 15 | FromPool string `mikrotik:"from-pool" codegen:"from_pool"` 16 | Interface string `mikrotik:"interface" codegen:"interface,required"` 17 | NoDad bool `mikrotik:"no-dad" codegen:"no_dad"` 18 | } 19 | 20 | var _ Resource = (*Ipv6Address)(nil) 21 | 22 | func (b *Ipv6Address) ActionToCommand(a Action) string { 23 | return map[Action]string{ 24 | Add: "/ipv6/address/add", 25 | Find: "/ipv6/address/print", 26 | Update: "/ipv6/address/set", 27 | Delete: "/ipv6/address/remove", 28 | }[a] 29 | } 30 | 31 | func (b *Ipv6Address) IDField() string { 32 | return ".id" 33 | } 34 | 35 | func (b *Ipv6Address) ID() string { 36 | return b.Id 37 | } 38 | 39 | func (b *Ipv6Address) SetID(id string) { 40 | b.Id = id 41 | } 42 | 43 | func (b *Ipv6Address) AfterAddHook(r *routeros.Reply) { 44 | b.Id = r.Done.Map["ret"] 45 | } 46 | 47 | // Typed wrappers 48 | func (c Mikrotik) AddIpv6Address(r *Ipv6Address) (*Ipv6Address, error) { 49 | res, err := c.Add(r) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return res.(*Ipv6Address), nil 55 | } 56 | 57 | func (c Mikrotik) UpdateIpv6Address(r *Ipv6Address) (*Ipv6Address, error) { 58 | res, err := c.Update(r) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | return res.(*Ipv6Address), nil 64 | } 65 | 66 | func (c Mikrotik) ListIpv6Address() ([]Ipv6Address, error) { 67 | res, err := c.List(&Ipv6Address{}) 68 | if err != nil { 69 | return nil, err 70 | } 71 | returnSlice := make([]Ipv6Address, len(res)) 72 | for i, v := range res { 73 | returnSlice[i] = *(v.(*Ipv6Address)) 74 | } 75 | 76 | return returnSlice, nil 77 | } 78 | 79 | func (c Mikrotik) FindIpv6Address(id string) (*Ipv6Address, error) { 80 | res, err := c.Find(&Ipv6Address{Id: id}) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | return res.(*Ipv6Address), nil 86 | } 87 | 88 | func (c Mikrotik) DeleteIpv6Address(id string) error { 89 | return c.Delete(&Ipv6Address{Id: id}) 90 | } 91 | -------------------------------------------------------------------------------- /client/ipv6_addr_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestAddIpv6AddressAndDeleteIpv6Address(t *testing.T) { 11 | SkipIfRouterOSV6OrEarlier(t, sysResources) 12 | c := NewClient(GetConfigFromEnv()) 13 | 14 | address := "1:1:1:1:1:1:1:1/64" 15 | comment := "terraform-acc-test" 16 | disabled := false 17 | ifname := "ether1" 18 | updatedComment := "terraform acc test updated" 19 | 20 | expectedIpv6Address := &Ipv6Address{ 21 | Address: address, 22 | Comment: comment, 23 | Disabled: disabled, 24 | Interface: ifname, 25 | } 26 | 27 | ipv6addr, err := c.AddIpv6Address(expectedIpv6Address) 28 | require.NoError(t, err) 29 | 30 | expectedIpv6Address.Id = ipv6addr.Id 31 | assert.Equal(t, expectedIpv6Address, ipv6addr) 32 | 33 | expectedIpv6Address.Comment = updatedComment 34 | ipv6addr, err = c.UpdateIpv6Address(expectedIpv6Address) 35 | require.NoError(t, err) 36 | assert.Equal(t, expectedIpv6Address, ipv6addr) 37 | 38 | foundIpv6Address, err := c.FindIpv6Address(ipv6addr.Id) 39 | require.NoError(t, err) 40 | assert.Equal(t, ipv6addr, foundIpv6Address) 41 | 42 | err = c.DeleteIpv6Address(ipv6addr.Id) 43 | assert.NoError(t, err) 44 | } 45 | -------------------------------------------------------------------------------- /client/lease.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/go-routeros/routeros" 7 | ) 8 | 9 | type DhcpLease struct { 10 | Id string `mikrotik:".id" codegen:"id,mikrotikID,terraformID"` 11 | Address string `mikrotik:"address" codegen:"address,required"` 12 | MacAddress string `mikrotik:"mac-address" codegen:"macaddress,required"` 13 | Comment string `mikrotik:"comment" codegen:"comment"` 14 | BlockAccess bool `mikrotik:"block-access" codegen:"blocked"` 15 | Dynamic bool `mikrotik:"dynamic,readonly" codegen:"dynamic,computed"` // TODO: don't see this listed as a param https://wiki.mikrotik.com/wiki/Manual:IP/DHCP_Server, but our docs list it as one 16 | Hostname string `mikrotik:"host-name,readonly" codegen:"hostname,computed"` 17 | } 18 | 19 | func (client Mikrotik) ListDhcpLeases() ([]DhcpLease, error) { 20 | c, err := client.getMikrotikClient() 21 | 22 | if err != nil { 23 | return nil, err 24 | } 25 | cmd := []string{"/ip/dhcp-server/lease/print"} 26 | log.Printf("[INFO] Running the mikrotik command: `%s`", cmd) 27 | r, err := c.RunArgs(cmd) 28 | 29 | if err != nil { 30 | return nil, err 31 | } 32 | log.Printf("[DEBUG] Found dhcp leases: %v", r) 33 | 34 | leases := []DhcpLease{} 35 | 36 | err = Unmarshal(*r, &leases) 37 | 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return leases, nil 43 | } 44 | 45 | var _ Resource = (*DhcpLease)(nil) 46 | 47 | func (b *DhcpLease) ActionToCommand(a Action) string { 48 | return map[Action]string{ 49 | Add: "/ip/dhcp-server/lease/add", 50 | Find: "/ip/dhcp-server/lease/print", 51 | Update: "/ip/dhcp-server/lease/set", 52 | Delete: "/ip/dhcp-server/lease/remove", 53 | }[a] 54 | } 55 | 56 | func (b *DhcpLease) IDField() string { 57 | return ".id" 58 | } 59 | 60 | func (b *DhcpLease) ID() string { 61 | return b.Id 62 | } 63 | 64 | func (b *DhcpLease) SetID(id string) { 65 | b.Id = id 66 | } 67 | 68 | func (b *DhcpLease) AfterAddHook(r *routeros.Reply) { 69 | b.Id = r.Done.Map["ret"] 70 | } 71 | 72 | // Typed wrappers 73 | func (c Mikrotik) AddDhcpLease(r *DhcpLease) (*DhcpLease, error) { 74 | res, err := c.Add(r) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | return res.(*DhcpLease), nil 80 | } 81 | 82 | func (c Mikrotik) UpdateDhcpLease(r *DhcpLease) (*DhcpLease, error) { 83 | res, err := c.Update(r) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | return res.(*DhcpLease), nil 89 | } 90 | 91 | func (c Mikrotik) FindDhcpLease(id string) (*DhcpLease, error) { 92 | res, err := c.Find(&DhcpLease{Id: id}) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | return res.(*DhcpLease), nil 98 | } 99 | 100 | func (client Mikrotik) ListDhcpLease() ([]DhcpLease, error) { 101 | res, err := client.List(&DhcpLease{}) 102 | if err != nil { 103 | return nil, err 104 | } 105 | returnSlice := make([]DhcpLease, len(res)) 106 | for i, v := range res { 107 | returnSlice[i] = *(v.(*DhcpLease)) 108 | } 109 | 110 | return returnSlice, nil 111 | } 112 | 113 | func (c Mikrotik) DeleteDhcpLease(id string) error { 114 | return c.Delete(&DhcpLease{Id: id}) 115 | } 116 | -------------------------------------------------------------------------------- /client/lease_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestAddLeaseAndDeleteLease(t *testing.T) { 11 | c := NewClient(GetConfigFromEnv()) 12 | 13 | address := "1.1.1.1" 14 | macaddress := "11:11:11:11:11:11" 15 | comment := "terraform-acc-test" 16 | blocked := false 17 | updatedMacaddress := "11:11:11:11:11:12" 18 | updatedComment := "terraform acc test updated" 19 | 20 | expectedLease := &DhcpLease{ 21 | Address: address, 22 | MacAddress: macaddress, 23 | Comment: comment, 24 | BlockAccess: blocked, 25 | } 26 | lease, err := c.AddDhcpLease(expectedLease) 27 | require.NoError(t, err) 28 | 29 | expectedLease.Id = lease.Id 30 | assert.Equal(t, expectedLease, lease) 31 | 32 | expectedLease.Comment = updatedComment 33 | expectedLease.MacAddress = updatedMacaddress 34 | 35 | lease, err = c.UpdateDhcpLease(expectedLease) 36 | assert.NoError(t, err) 37 | assert.Equal(t, expectedLease, lease) 38 | 39 | foundLease, err := c.FindDhcpLease(lease.Id) 40 | assert.NoError(t, err) 41 | assert.Equal(t, lease, foundLease) 42 | 43 | err = c.DeleteDhcpLease(lease.Id) 44 | assert.NoError(t, err) 45 | } 46 | 47 | func TestFindDhcpLease_forNonExistantLease(t *testing.T) { 48 | c := NewClient(GetConfigFromEnv()) 49 | 50 | leaseId := "Invalid id" 51 | _, err := c.FindDhcpLease(leaseId) 52 | 53 | assert.Error(t, err) 54 | assert.True(t, IsNotFoundError(err), "expected error to be of NotFound type") 55 | } 56 | -------------------------------------------------------------------------------- /client/pool.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/go-routeros/routeros" 5 | ) 6 | 7 | type Pool struct { 8 | Id string `mikrotik:".id" codegen:"id,mikrotikID,terraformID"` 9 | Name string `mikrotik:"name" codegen:"name,required"` 10 | Ranges string `mikrotik:"ranges" codegen:"ranges,required"` 11 | NextPool string `mikrotik:"next-pool" codegen:"next_pool,optiona,computed"` 12 | Comment string `mikrotik:"comment" codegen:"comment,optional,computed"` 13 | } 14 | 15 | var _ Resource = (*Pool)(nil) 16 | 17 | func (b *Pool) ActionToCommand(a Action) string { 18 | return map[Action]string{ 19 | Add: "/ip/pool/add", 20 | Find: "/ip/pool/print", 21 | Update: "/ip/pool/set", 22 | Delete: "/ip/pool/remove", 23 | }[a] 24 | } 25 | 26 | func (b *Pool) IDField() string { 27 | return ".id" 28 | } 29 | 30 | func (b *Pool) ID() string { 31 | return b.Id 32 | } 33 | 34 | func (b *Pool) SetID(id string) { 35 | b.Id = id 36 | } 37 | 38 | func (b *Pool) AfterAddHook(r *routeros.Reply) { 39 | b.Id = r.Done.Map["ret"] 40 | } 41 | 42 | // Typed wrappers 43 | func (c Mikrotik) AddPool(r *Pool) (*Pool, error) { 44 | return r.processResourceErrorTuplePtr(c.Add(r)) 45 | } 46 | 47 | func (c Mikrotik) UpdatePool(r *Pool) (*Pool, error) { 48 | return r.processResourceErrorTuplePtr(c.Update(r)) 49 | } 50 | 51 | func (c Mikrotik) FindPool(id string) (*Pool, error) { 52 | return Pool{}.processResourceErrorTuplePtr(c.Find(&Pool{Id: id})) 53 | } 54 | 55 | func (c Mikrotik) FindPoolByName(name string) (*Pool, error) { 56 | return Pool{}.processResourceErrorTuplePtr(c.findByField(&Pool{}, "name", name)) 57 | } 58 | 59 | func (c Mikrotik) DeletePool(id string) error { 60 | return c.Delete(&Pool{Id: id}) 61 | } 62 | 63 | func (c Mikrotik) ListPools() ([]Pool, error) { 64 | res, err := c.List(&Pool{}) 65 | if err != nil { 66 | return nil, err 67 | } 68 | returnSlice := make([]Pool, len(res)) 69 | for i, v := range res { 70 | returnSlice[i] = *(v.(*Pool)) 71 | } 72 | return returnSlice, nil 73 | } 74 | 75 | func (b Pool) processResourceErrorTuplePtr(r Resource, err error) (*Pool, error) { 76 | if err != nil { 77 | return nil, err 78 | } 79 | return r.(*Pool), nil 80 | } 81 | -------------------------------------------------------------------------------- /client/pool_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestAddUpdateAndDeletePool(t *testing.T) { 11 | c := NewClient(GetConfigFromEnv()) 12 | 13 | expectedPool := &Pool{ 14 | Name: "pool-" + RandomString(), 15 | Ranges: "172.16.0.1-172.16.0.8,172.16.0.10", 16 | Comment: "pool comment", 17 | } 18 | pool, err := c.AddPool(expectedPool) 19 | 20 | if err != nil { 21 | t.Fatalf("Error creating a pool with: %v", err) 22 | } 23 | 24 | expectedPool.Id = pool.Id 25 | if !reflect.DeepEqual(pool, expectedPool) { 26 | t.Errorf("The pool does not match what we expected. actual: %v expected: %v", pool, expectedPool) 27 | } 28 | 29 | expectedPool.Comment = "updated comment" 30 | expectedPool.Ranges = "172.16.0.1-172.16.0.8,172.16.0.16" 31 | pool, err = c.UpdatePool(expectedPool) 32 | 33 | if err != nil { 34 | t.Errorf("Error updating pool with: %v", err) 35 | } 36 | 37 | if !reflect.DeepEqual(pool, expectedPool) { 38 | t.Errorf("Updated pool does not match the expected: %v expected: %v", expectedPool, pool) 39 | } 40 | 41 | err = c.DeletePool(pool.Id) 42 | 43 | if err != nil { 44 | t.Errorf("Error deleting pool with: %v", err) 45 | } 46 | } 47 | 48 | func TestFindPool_forNonExistingPool(t *testing.T) { 49 | c := NewClient(GetConfigFromEnv()) 50 | 51 | poolId := "Invalid id" 52 | _, err := c.FindPool(poolId) 53 | 54 | require.Truef(t, IsNotFoundError(err), "client should have NotFound error error but instead received") 55 | } 56 | 57 | func TestFindPoolByName_forExistingPool(t *testing.T) { 58 | c := NewClient(GetConfigFromEnv()) 59 | 60 | p := &Pool{ 61 | Name: "pool-" + RandomString(), 62 | Ranges: "172.16.0.1-172.16.0.8,172.16.0.10", 63 | Comment: "existing pool", 64 | } 65 | pool, err := c.AddPool(p) 66 | 67 | expectedPool, err := c.FindPoolByName(pool.Name) 68 | if err != nil { 69 | t.Fatalf("Error finding pool by name with: %v", err) 70 | } 71 | if pool.Name != expectedPool.Name { 72 | t.Errorf("The pool Name fields do not match. actual: %v expected: %v", pool.Name, expectedPool.Name) 73 | } 74 | c.DeletePool(pool.Id) 75 | } 76 | 77 | func TestFindPoolByName_forNonExistingPool(t *testing.T) { 78 | c := NewClient(GetConfigFromEnv()) 79 | 80 | poolName := "Invalid name" 81 | _, err := c.FindPoolByName(poolName) 82 | 83 | require.True(t, IsNotFoundError(err), 84 | "client should have NotFound error") 85 | } 86 | -------------------------------------------------------------------------------- /client/resource_wrappers.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "reflect" 4 | 5 | type ( 6 | // FindByFieldWrapper changes the fields used to find the remote resource. 7 | FindByFieldWrapper struct { 8 | Resource 9 | field string 10 | fieldValueFunc func() string 11 | } 12 | ) 13 | 14 | var ( 15 | _ Finder = (*FindByFieldWrapper)(nil) 16 | _ Resource = (*FindByFieldWrapper)(nil) 17 | ) 18 | 19 | func (fw FindByFieldWrapper) FindField() string { 20 | return fw.field 21 | } 22 | 23 | func (fw FindByFieldWrapper) FindFieldValue() string { 24 | return fw.fieldValueFunc() 25 | } 26 | 27 | // Create satisfies ResourceInstanceCreator interface and returns new object of the wrapped resource. 28 | func (fw FindByFieldWrapper) Create() Resource { 29 | reflectNew := reflect.New(reflect.Indirect(reflect.ValueOf(fw.Resource)).Type()) 30 | 31 | return reflectNew.Interface().(Resource) 32 | } 33 | -------------------------------------------------------------------------------- /client/scheduler.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/ddelnano/terraform-provider-mikrotik/client/types" 5 | "github.com/go-routeros/routeros" 6 | ) 7 | 8 | type Scheduler struct { 9 | Id string `mikrotik:".id"` 10 | Name string `mikrotik:"name"` 11 | OnEvent string `mikrotik:"on-event"` 12 | StartDate string `mikrotik:"start-date"` 13 | StartTime string `mikrotik:"start-time"` 14 | Interval types.MikrotikDuration `mikrotik:"interval"` 15 | } 16 | 17 | var _ Resource = (*Scheduler)(nil) 18 | 19 | func (b *Scheduler) ActionToCommand(a Action) string { 20 | return map[Action]string{ 21 | Add: "/system/scheduler/add", 22 | Find: "/system/scheduler/print", 23 | Update: "/system/scheduler/set", 24 | Delete: "/system/scheduler/remove", 25 | }[a] 26 | } 27 | 28 | func (b *Scheduler) IDField() string { 29 | return ".id" 30 | } 31 | 32 | func (b *Scheduler) ID() string { 33 | return b.Id 34 | } 35 | 36 | func (b *Scheduler) SetID(id string) { 37 | b.Id = id 38 | } 39 | 40 | func (b *Scheduler) AfterAddHook(r *routeros.Reply) { 41 | b.Id = r.Done.Map["ret"] 42 | } 43 | 44 | func (b *Scheduler) FindField() string { 45 | return "name" 46 | } 47 | 48 | func (b *Scheduler) FindFieldValue() string { 49 | return b.Name 50 | } 51 | 52 | func (b *Scheduler) DeleteField() string { 53 | return "numbers" 54 | } 55 | 56 | func (b *Scheduler) DeleteFieldValue() string { 57 | return b.Id 58 | } 59 | 60 | // typed wrappers 61 | func (client Mikrotik) AddScheduler(s *Scheduler) (*Scheduler, error) { 62 | return client.CreateScheduler(s) 63 | } 64 | 65 | func (client Mikrotik) CreateScheduler(s *Scheduler) (*Scheduler, error) { 66 | r, err := client.Add(s) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | return r.(*Scheduler), nil 72 | } 73 | 74 | func (client Mikrotik) UpdateScheduler(s *Scheduler) (*Scheduler, error) { 75 | r, err := client.Update(s) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | return r.(*Scheduler), nil 81 | } 82 | 83 | func (client Mikrotik) FindScheduler(name string) (*Scheduler, error) { 84 | r, err := client.Find(&Scheduler{Name: name}) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | return r.(*Scheduler), nil 90 | } 91 | 92 | func (client Mikrotik) DeleteScheduler(name string) error { 93 | return client.Delete(&Scheduler{Name: name}) 94 | } 95 | -------------------------------------------------------------------------------- /client/scheduler_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ddelnano/terraform-provider-mikrotik/client/types" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestCreateUpdateDeleteAndFindScheduler(t *testing.T) { 11 | c := NewClient(GetConfigFromEnv()) 12 | 13 | schedulerName := "scheduler_" + RandomString() 14 | onEvent := "onevent" 15 | interval := 0 16 | expectedScheduler := &Scheduler{ 17 | Name: schedulerName, 18 | OnEvent: onEvent, 19 | Interval: types.MikrotikDuration(interval), 20 | } 21 | scheduler, err := c.AddScheduler(expectedScheduler) 22 | require.NoError(t, err) 23 | require.NotNil(t, scheduler) 24 | 25 | expectedScheduler.Id = scheduler.Id 26 | expectedScheduler.StartDate = scheduler.StartDate 27 | expectedScheduler.StartTime = scheduler.StartTime 28 | 29 | require.Equal(t, expectedScheduler, scheduler) 30 | 31 | // update and reassert 32 | expectedScheduler.OnEvent = "test" 33 | scheduler, err = c.UpdateScheduler(expectedScheduler) 34 | require.Equal(t, expectedScheduler, scheduler) 35 | 36 | err = c.DeleteScheduler(schedulerName) 37 | require.NoError(t, err) 38 | } 39 | 40 | func TestFindScheduler_onNonExistantScript(t *testing.T) { 41 | c := NewClient(GetConfigFromEnv()) 42 | 43 | name := "scheduler does not exist" 44 | _, err := c.FindScheduler(name) 45 | 46 | require.Truef(t, IsNotFoundError(err), 47 | "Expecting to receive NotFound error for scheduler %q.", name) 48 | } 49 | -------------------------------------------------------------------------------- /client/script.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/ddelnano/terraform-provider-mikrotik/client/types" 5 | "github.com/go-routeros/routeros" 6 | ) 7 | 8 | type Script struct { 9 | Id string `mikrotik:".id" codegen:"id,deleteID"` 10 | Name string `mikrotik:"name" codegen:"name,required,mikrotikID"` 11 | Owner string `mikrotik:"owner,readonly" codegen:"owner,computed"` 12 | Policy types.MikrotikList `mikrotik:"policy" codegen:"policy,required"` 13 | DontRequirePermissions bool `mikrotik:"dont-require-permissions" codegen:"dont_require_permissions"` 14 | Source string `mikrotik:"source" codegen:"source,required"` 15 | } 16 | 17 | var _ Resource = (*Script)(nil) 18 | 19 | func (b *Script) ActionToCommand(a Action) string { 20 | return map[Action]string{ 21 | Add: "/system/script/add", 22 | Find: "/system/script/print", 23 | Update: "/system/script/set", 24 | Delete: "/system/script/remove", 25 | }[a] 26 | } 27 | 28 | func (b *Script) IDField() string { 29 | return ".id" 30 | } 31 | 32 | func (b *Script) ID() string { 33 | return b.Id 34 | } 35 | 36 | func (b *Script) SetID(id string) { 37 | b.Id = id 38 | } 39 | 40 | func (b *Script) AfterAddHook(r *routeros.Reply) { 41 | b.Id = r.Done.Map["ret"] 42 | } 43 | 44 | func (b *Script) FindField() string { 45 | return "name" 46 | } 47 | 48 | func (b *Script) FindFieldValue() string { 49 | return b.Name 50 | } 51 | 52 | func (b *Script) DeleteField() string { 53 | return "numbers" 54 | } 55 | 56 | func (b *Script) DeleteFieldValue() string { 57 | return b.Id 58 | } 59 | 60 | // Typed wrappers 61 | func (c Mikrotik) AddScript(r *Script) (*Script, error) { 62 | res, err := c.Add(r) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | return res.(*Script), nil 68 | } 69 | 70 | func (c Mikrotik) UpdateScript(r *Script) (*Script, error) { 71 | res, err := c.Update(r) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | return res.(*Script), nil 77 | } 78 | 79 | func (c Mikrotik) FindScript(name string) (*Script, error) { 80 | res, err := c.Find(&Script{Name: name}) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | return res.(*Script), nil 86 | } 87 | 88 | func (c Mikrotik) DeleteScript(id string) error { 89 | return c.Delete(&Script{Id: id}) 90 | } 91 | -------------------------------------------------------------------------------- /client/script_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | var scriptSource string = ":put testing" 11 | var scriptName string = "testing" 12 | var scriptPolicies []string = []string{ 13 | "ftp", 14 | "reboot", 15 | "read", 16 | "write", 17 | "policy", 18 | "test", 19 | "password", 20 | "sniff", 21 | "sensitive", 22 | "romon", 23 | } 24 | var scriptDontReqPerms = true 25 | 26 | func TestCreateScriptAndDeleteScript(t *testing.T) { 27 | c := NewClient(GetConfigFromEnv()) 28 | _, owner, _, _, _, _ := GetConfigFromEnv() 29 | 30 | expectedScript := &Script{ 31 | Name: scriptName, 32 | Owner: owner, 33 | Source: scriptSource, 34 | Policy: scriptPolicies, 35 | DontRequirePermissions: scriptDontReqPerms, 36 | } 37 | script, err := NewClient(GetConfigFromEnv()). 38 | AddScript(&Script{ 39 | Name: scriptName, 40 | Source: scriptSource, 41 | Policy: scriptPolicies, 42 | DontRequirePermissions: scriptDontReqPerms, 43 | }, 44 | ) 45 | require.NoError(t, err) 46 | 47 | expectedScript.Id = script.Id 48 | 49 | defer func() { 50 | if err := c.DeleteScript(scriptName); err != nil { 51 | assert.True(t, IsNotFoundError(err), "the only acceptable error is NotFound") 52 | } 53 | }() 54 | 55 | require.Equal(t, expectedScript, script) 56 | 57 | err = c.DeleteScript(scriptName) 58 | require.NoError(t, err) 59 | } 60 | 61 | func TestFindScript_onNonExistantScript(t *testing.T) { 62 | c := NewClient(GetConfigFromEnv()) 63 | 64 | name := "script-not-found" 65 | _, err := c.FindScript(name) 66 | 67 | if !IsNotFoundError(err) { 68 | t.Errorf("client should have received error indicating the following script `%s` was not found. Instead error was %v", name, err) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /client/setup.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func SetupAndTestMainExec(m *testing.M, sysResources *SystemResources) { 10 | c := NewClient(GetConfigFromEnv()) 11 | s, err := c.GetSystemResources() 12 | 13 | if err != nil { 14 | fmt.Printf("Unable to perform test setup, failed with error: %v\n", err) 15 | os.Exit(1) 16 | } 17 | 18 | *sysResources = *s 19 | 20 | os.Exit(m.Run()) 21 | } 22 | -------------------------------------------------------------------------------- /client/setup_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var sysResources SystemResources 8 | 9 | func TestMain(m *testing.M) { 10 | SetupAndTestMainExec(m, &sysResources) 11 | } 12 | -------------------------------------------------------------------------------- /client/system_resources.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/ddelnano/terraform-provider-mikrotik/client/types" 7 | ) 8 | 9 | type SystemResources struct { 10 | Uptime types.MikrotikDuration `mikrotik:"uptime,readonly"` 11 | Version string `mikrotik:"version,readonly"` 12 | } 13 | 14 | func (d *SystemResources) ActionToCommand(action Action) string { 15 | return map[Action]string{ 16 | Find: "/system/resource/print", 17 | }[action] 18 | } 19 | 20 | func (client Mikrotik) GetSystemResources() (*SystemResources, error) { 21 | c, err := client.getMikrotikClient() 22 | if err != nil { 23 | return nil, err 24 | } 25 | sysResources := &SystemResources{} 26 | cmd := Marshal(sysResources.ActionToCommand(Find), sysResources) 27 | 28 | log.Printf("[INFO] Running the mikrotik command: `%s`", cmd) 29 | r, err := c.RunArgs(cmd) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | err = Unmarshal(*r, sysResources) 35 | return sysResources, err 36 | } 37 | -------------------------------------------------------------------------------- /client/system_resources_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestGetSystemResources(t *testing.T) { 9 | c := NewClient(GetConfigFromEnv()) 10 | sysResources, err := c.GetSystemResources() 11 | 12 | if err != nil { 13 | t.Fatalf("failed to get system resources with error: %v", err) 14 | } 15 | 16 | if sysResources.Uptime <= 0 { 17 | t.Fatalf("expected uptime > 0, instead received '%d'", sysResources.Uptime) 18 | } 19 | 20 | version := sysResources.Version 21 | if strings.Index(version, "6") != 0 && strings.Index(version, "7") != 0 { 22 | t.Errorf("expected RouterOS version to start with a '7' or '6' major release, instead received '%s'", version) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /client/types/duration.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // MikrotikDuration type represents a RouterOS durations [w,d] in seconds 12 | type MikrotikDuration int 13 | 14 | func (m MikrotikDuration) MarshalMikrotik() string { 15 | return strconv.Itoa(int(m)) 16 | } 17 | 18 | func (m *MikrotikDuration) UnmarshalMikrotik(value string) error { 19 | value = strings.TrimSpace(value) 20 | if len(value) == 0 { 21 | return errors.New("cannot unmarshal empty value") 22 | } 23 | d, err := parseDuration(value) 24 | if err != nil { 25 | return err 26 | } 27 | *m = MikrotikDuration(d.Seconds()) 28 | 29 | return nil 30 | } 31 | 32 | func parseDuration(s string) (time.Duration, error) { 33 | var digitsStartIndex, unitStartIndex int 34 | var nanoseconds int64 35 | 36 | parsePart := func(s string, unitStart int) (int64, error) { 37 | var ret int64 38 | digits, err := strconv.Atoi(s[:unitStart]) 39 | if err != nil { 40 | return 0, err 41 | } 42 | 43 | unit := s[unitStart:] 44 | switch unit { 45 | case "ns": 46 | ret = int64(digits) 47 | case "us": 48 | ret = int64(digits) * time.Microsecond.Nanoseconds() 49 | case "ms": 50 | ret = int64(digits) * time.Millisecond.Nanoseconds() 51 | case "s": 52 | ret = int64(digits) * time.Second.Nanoseconds() 53 | case "m": 54 | ret = int64(digits) * time.Minute.Nanoseconds() 55 | case "h": 56 | ret = int64(digits) * time.Hour.Nanoseconds() 57 | case "d": 58 | ret = int64(digits) * time.Hour.Nanoseconds() * 24 59 | case "w": 60 | ret = int64(digits) * time.Hour.Nanoseconds() * 24 * 7 61 | default: 62 | return 0, fmt.Errorf("unknown unit: %q", unit) 63 | } 64 | return ret, nil 65 | } 66 | 67 | for i := 0; i < len(s); i++ { 68 | char := string(s[i]) 69 | if char >= "0" && char <= "9" { 70 | if unitStartIndex > digitsStartIndex { 71 | parsed, err := parsePart(s[digitsStartIndex:i], unitStartIndex-digitsStartIndex) 72 | if err != nil { 73 | return 0, err 74 | } 75 | nanoseconds += parsed 76 | digitsStartIndex = i 77 | unitStartIndex = i 78 | } 79 | continue 80 | } 81 | if digitsStartIndex == unitStartIndex { 82 | unitStartIndex = i 83 | } 84 | continue 85 | } 86 | if digitsStartIndex == unitStartIndex { 87 | return 0, errors.New("duration without unit is not supported") 88 | } 89 | parsed, err := parsePart(s[digitsStartIndex:], unitStartIndex-digitsStartIndex) 90 | if err != nil { 91 | return 0, err 92 | } 93 | nanoseconds += parsed 94 | 95 | return time.Duration(nanoseconds), nil 96 | } 97 | -------------------------------------------------------------------------------- /client/types/duration_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestDurationUnmarshal(t *testing.T) { 12 | 13 | testCases := []struct { 14 | name string 15 | in string 16 | expected MikrotikDuration 17 | expectError bool 18 | }{ 19 | { 20 | name: "single unit", 21 | in: "23s", 22 | expected: MikrotikDuration(23), 23 | }, 24 | { 25 | name: "units below second are zeroed", 26 | in: "20ms", 27 | expected: MikrotikDuration(0), 28 | }, 29 | { 30 | name: "parse week", 31 | in: "2w", 32 | expected: MikrotikDuration(time.Hour.Seconds() * 24 * 7 * 2), 33 | }, 34 | { 35 | name: "multiple units", 36 | in: "2h17m01s", 37 | expected: MikrotikDuration(time.Hour.Seconds()*2 + time.Minute.Seconds()*17 + 1), 38 | }, 39 | { 40 | name: "no-unit produces error", 41 | in: "17", 42 | expectError: true, 43 | }, 44 | { 45 | name: "unit and no-unit produces error", 46 | in: "2h17", 47 | expectError: true, 48 | }, 49 | } 50 | for _, tc := range testCases { 51 | t.Run(tc.name, func(t *testing.T) { 52 | m := MikrotikDuration(0) 53 | err := (&m).UnmarshalMikrotik(tc.in) 54 | if tc.expectError { 55 | require.Error(t, err) 56 | return 57 | } 58 | require.NoError(t, err) 59 | assert.Equal(t, tc.expected, m) 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /client/types/list.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | // MikrotikList type translates slice of strings to comma separated list and back 9 | // 10 | // It is useful to seamless serialize/deserialize data during communication with RouterOS 11 | type MikrotikList []string 12 | 13 | func (m MikrotikList) MarshalMikrotik() string { 14 | return strings.Join(m, ",") 15 | } 16 | 17 | func (m *MikrotikList) UnmarshalMikrotik(value string) error { 18 | if len(value) == 0 { 19 | *m = []string{} 20 | return nil 21 | } 22 | *m = strings.Split(value, ",") 23 | 24 | return nil 25 | } 26 | 27 | // MikrotikIntList type translates slice of ints to comma separated list and back 28 | type MikrotikIntList []int 29 | 30 | func (m MikrotikIntList) MarshalMikrotik() string { 31 | if len(m) == 0 { 32 | return "" 33 | } 34 | if len(m) == 1 { 35 | return strconv.Itoa(m[0]) 36 | } 37 | 38 | buf := strings.Builder{} 39 | buf.WriteString(strconv.Itoa(m[0])) 40 | for i := range m[1:] { 41 | buf.WriteRune(',') 42 | buf.WriteString(strconv.Itoa(m[i+1])) 43 | } 44 | 45 | return buf.String() 46 | } 47 | 48 | func (m *MikrotikIntList) UnmarshalMikrotik(value string) error { 49 | if len(value) == 0 { 50 | *m = []int{} 51 | return nil 52 | } 53 | stringSlice := strings.Split(value, ",") 54 | res := []int{} 55 | for _, s := range stringSlice { 56 | elem, err := strconv.Atoi(s) 57 | if err != nil { 58 | return err 59 | } 60 | res = append(res, elem) 61 | } 62 | *m = res 63 | 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /client/vlan_interface.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/go-routeros/routeros" 5 | ) 6 | 7 | // VlanInterface represents vlan interface resource 8 | type VlanInterface struct { 9 | Id string `mikrotik:".id" codegen:"id,mikrotikID"` 10 | Interface string `mikrotik:"interface" codegen:"interface"` 11 | Mtu int `mikrotik:"mtu" codegen:"mtu"` 12 | Name string `mikrotik:"name" codegen:"name,required,terraformID"` 13 | Disabled bool `mikrotik:"disabled" codegen:"disabled"` 14 | UseServiceTag bool `mikrotik:"use-service-tag" codegen:"use_service_tag"` 15 | VlanId int `mikrotik:"vlan-id" codegen:"vlan_id"` 16 | } 17 | 18 | var _ Resource = (*VlanInterface)(nil) 19 | 20 | func (b *VlanInterface) ActionToCommand(a Action) string { 21 | return map[Action]string{ 22 | Add: "/interface/vlan/add", 23 | Find: "/interface/vlan/print", 24 | Update: "/interface/vlan/set", 25 | Delete: "/interface/vlan/remove", 26 | }[a] 27 | } 28 | 29 | func (b *VlanInterface) IDField() string { 30 | return ".id" 31 | } 32 | 33 | func (b *VlanInterface) ID() string { 34 | return b.Id 35 | } 36 | 37 | func (b *VlanInterface) SetID(id string) { 38 | b.Id = id 39 | } 40 | 41 | func (b *VlanInterface) AfterAddHook(r *routeros.Reply) { 42 | b.Id = r.Done.Map["ret"] 43 | } 44 | 45 | func (b *VlanInterface) FindField() string { 46 | return "name" 47 | } 48 | 49 | func (b *VlanInterface) FindFieldValue() string { 50 | return b.Name 51 | } 52 | 53 | func (b *VlanInterface) DeleteField() string { 54 | return "numbers" 55 | } 56 | 57 | func (b *VlanInterface) DeleteFieldValue() string { 58 | return b.Name 59 | } 60 | 61 | // Typed wrappers 62 | func (c Mikrotik) AddVlanInterface(r *VlanInterface) (*VlanInterface, error) { 63 | res, err := c.Add(r) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | return res.(*VlanInterface), nil 69 | } 70 | 71 | func (c Mikrotik) UpdateVlanInterface(r *VlanInterface) (*VlanInterface, error) { 72 | res, err := c.Update(r) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | return res.(*VlanInterface), nil 78 | } 79 | 80 | func (c Mikrotik) FindVlanInterface(name string) (*VlanInterface, error) { 81 | res, err := c.Find(&VlanInterface{Name: name}) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | return res.(*VlanInterface), nil 87 | } 88 | 89 | func (c Mikrotik) ListVlanInterface() ([]VlanInterface, error) { 90 | res, err := c.List(&VlanInterface{}) 91 | if err != nil { 92 | return nil, err 93 | } 94 | returnSlice := make([]VlanInterface, len(res)) 95 | for i, v := range res { 96 | returnSlice[i] = *(v.(*VlanInterface)) 97 | } 98 | 99 | return returnSlice, nil 100 | } 101 | 102 | func (c Mikrotik) DeleteVlanInterface(name string) error { 103 | return c.Delete(&VlanInterface{Name: name}) 104 | } 105 | -------------------------------------------------------------------------------- /client/vlan_interface_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestAddVlanInterfaceUpdateAndDelete(t *testing.T) { 11 | c := NewClient(GetConfigFromEnv()) 12 | 13 | expectedIface := &VlanInterface{ 14 | Name: "vlan-20", 15 | VlanId: 20, 16 | Mtu: 1000, 17 | Interface: "*0", 18 | Disabled: false, 19 | } 20 | 21 | iface, err := c.AddVlanInterface(&VlanInterface{ 22 | Name: expectedIface.Name, 23 | Disabled: expectedIface.Disabled, 24 | Interface: expectedIface.Interface, 25 | VlanId: expectedIface.VlanId, 26 | Mtu: expectedIface.Mtu, 27 | }) 28 | require.NoError(t, err) 29 | 30 | expectedIface.Id = iface.Id 31 | 32 | foundInterface, err := c.FindVlanInterface(expectedIface.Name) 33 | require.NoError(t, err) 34 | assert.Equal(t, expectedIface, foundInterface) 35 | 36 | expectedIface.Name = expectedIface.Name + "updated" 37 | expectedIface.Mtu = expectedIface.Mtu - 100 38 | updatedIface, err := c.UpdateVlanInterface(expectedIface) 39 | require.NoError(t, err) 40 | assert.Equal(t, expectedIface, updatedIface) 41 | // cleanup 42 | err = c.DeleteVlanInterface(iface.Name) 43 | assert.NoError(t, err) 44 | 45 | _, err = c.FindVlanInterface(expectedIface.Name) 46 | assert.Error(t, err) 47 | } 48 | -------------------------------------------------------------------------------- /client/wireless_interface.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "github.com/go-routeros/routeros" 4 | 5 | const ( 6 | WirelessInterfaceModeStation = "station" 7 | WirelessInterfaceModeStationWDS = "station-wds" 8 | WirelessInterfaceModeAPBridge = "ap-bridge" 9 | WirelessInterfaceModeBridge = "bridge" 10 | WirelessInterfaceModeAlignmentOnly = "alignment-only" 11 | WirelessInterfaceModeNstremeDualSlave = "nstreme-dual-slave" 12 | WirelessInterfaceModeWDSSlave = "wds-slave" 13 | WirelessInterfaceModeStationPseudobridge = "station-pseudobridge" 14 | WirelessInterfaceModeStationsPseudobridgeClone = "station-pseudobridge-clone" 15 | WirelessInterfaceModeStationBridge = "station-bridge" 16 | ) 17 | 18 | // WirelessInterface defines resource 19 | type WirelessInterface struct { 20 | Id string `mikrotik:".id" codegen:"id,mikrotikID"` 21 | Name string `mikrotik:"name" codegen:"name,required"` 22 | MasterInterface string `mikrotik:"master-interface" codegen:"master_interface"` 23 | Mode string `mikrotik:"mode" codegen:"mode"` 24 | Disabled bool `mikrotik:"disabled" codegen:"disabled"` 25 | SecurityProfile string `mikrotik:"security-profile" codegen:"security_profile"` 26 | SSID string `mikrotik:"ssid" codegen:"ssid"` 27 | HideSSID bool `mikrotik:"hide-ssid" codegen:"hide_ssid"` 28 | VlanID int `mikrotik:"vlan-id" codegen:"vlan_id"` 29 | VlanMode string `mikrotik:"vlan-mode" codegen:"vlan_mode"` 30 | } 31 | 32 | var _ Resource = (*WirelessInterface)(nil) 33 | 34 | func (b *WirelessInterface) ActionToCommand(a Action) string { 35 | return map[Action]string{ 36 | Add: "/interface/wireless/add", 37 | Find: "/interface/wireless/print", 38 | Update: "/interface/wireless/set", 39 | Delete: "/interface/wireless/remove", 40 | }[a] 41 | } 42 | 43 | func (b *WirelessInterface) IDField() string { 44 | return ".id" 45 | } 46 | 47 | func (b *WirelessInterface) ID() string { 48 | return b.Id 49 | } 50 | 51 | func (b *WirelessInterface) SetID(id string) { 52 | b.Id = id 53 | } 54 | 55 | func (b *WirelessInterface) AfterAddHook(r *routeros.Reply) { 56 | b.Id = r.Done.Map["ret"] 57 | } 58 | 59 | // Typed wrappers 60 | func (c Mikrotik) AddWirelessInterface(r *WirelessInterface) (*WirelessInterface, error) { 61 | res, err := c.Add(r) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | return res.(*WirelessInterface), nil 67 | } 68 | 69 | func (c Mikrotik) UpdateWirelessInterface(r *WirelessInterface) (*WirelessInterface, error) { 70 | res, err := c.Update(r) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | return res.(*WirelessInterface), nil 76 | } 77 | 78 | func (c Mikrotik) FindWirelessInterface(id string) (*WirelessInterface, error) { 79 | res, err := c.Find(&WirelessInterface{Id: id}) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | return res.(*WirelessInterface), nil 85 | } 86 | 87 | func (c Mikrotik) ListWirelessInterface() ([]WirelessInterface, error) { 88 | res, err := c.List(&WirelessInterface{}) 89 | if err != nil { 90 | return nil, err 91 | } 92 | returnSlice := make([]WirelessInterface, len(res)) 93 | for i, v := range res { 94 | returnSlice[i] = *(v.(*WirelessInterface)) 95 | } 96 | 97 | return returnSlice, nil 98 | } 99 | 100 | func (c Mikrotik) DeleteWirelessInterface(id string) error { 101 | return c.Delete(&WirelessInterface{Id: id}) 102 | } 103 | -------------------------------------------------------------------------------- /client/wireless_interface_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestWirelessInterface_basic(t *testing.T) { 11 | // This test is skipped, until we find a way to include required packages. 12 | // 13 | // Since RouterOS 7.13, 'wireless' package is separate from the main system package 14 | // and there is no easy way to install it in Docker during tests. 15 | // see https://help.mikrotik.com/docs/spaces/ROS/pages/40992872/Packages#Packages-RouterOSpackages 16 | SkipIfRouterOSV7OrLater(t, sysResources) 17 | 18 | randSuffix := RandomString() 19 | c := NewClient(GetConfigFromEnv()) 20 | expected := &WirelessInterface{ 21 | Name: "wireless-" + randSuffix, 22 | SSID: "ssid-" + randSuffix, 23 | MasterInterface: "*0", 24 | } 25 | created, err := c.AddWirelessInterface(expected) 26 | require.NoError(t, err) 27 | defer c.DeleteWirelessInterface(created.Id) 28 | 29 | assert.Equal(t, expected.Name, created.Name) 30 | assert.Equal(t, expected.SSID, created.SSID) 31 | assert.Equal(t, false, created.Disabled) 32 | 33 | created.Disabled = true 34 | created.Name = "wireless-updated-" + randSuffix 35 | updated, err := c.UpdateWirelessInterface(created) 36 | require.NoError(t, err) 37 | assert.Equal(t, created, updated) 38 | 39 | found, err := c.FindWirelessInterface(updated.Id) 40 | require.NoError(t, err) 41 | assert.Equal(t, updated, found) 42 | } 43 | -------------------------------------------------------------------------------- /client/wireless_security_profile.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/ddelnano/terraform-provider-mikrotik/client/types" 5 | "github.com/go-routeros/routeros" 6 | ) 7 | 8 | const ( 9 | WirelessAuthenticationTypeWpaPsk = "wpa-psk" 10 | WirelessAuthenticationTypeWpa2Psk = "wpa2-psk" 11 | WirelessAuthenticationTypeWpaEap = "wpa-eap" 12 | WirelessAuthenticationTypeWpa2Eap = "wpa2-eap" 13 | 14 | WirelessModeNone = "none" 15 | WirelessModeStaticKeysOptional = "static-keys-optional" 16 | WirelessModeStaticKeysRequired = "static-keys-required" 17 | WirelessModeDynamicKeys = "dynamic-keys" 18 | ) 19 | 20 | // WirelessSecurityProfile defines resource 21 | type WirelessSecurityProfile struct { 22 | Id string `mikrotik:".id" codegen:"id,mikrotikID"` 23 | Name string `mikrotik:"name" codegen:"name,required"` 24 | Mode string `mikrotik:"mode" codegen:"mode,optional"` 25 | AuthenticationTypes types.MikrotikList `mikrotik:"authentication-types" codegen:"authentication_types,optional"` 26 | WPA2PreSharedKey string `mikrotik:"wpa2-pre-shared-key" codegen:"wpa2_pre_shared_key"` 27 | } 28 | 29 | var _ Resource = (*WirelessSecurityProfile)(nil) 30 | 31 | func (b *WirelessSecurityProfile) ActionToCommand(a Action) string { 32 | return map[Action]string{ 33 | Add: "/interface/wireless/security-profiles/add", 34 | Find: "/interface/wireless/security-profiles/print", 35 | Update: "/interface/wireless/security-profiles/set", 36 | Delete: "/interface/wireless/security-profiles/remove", 37 | }[a] 38 | } 39 | 40 | func (b *WirelessSecurityProfile) IDField() string { 41 | return ".id" 42 | } 43 | 44 | func (b *WirelessSecurityProfile) ID() string { 45 | return b.Id 46 | } 47 | 48 | func (b *WirelessSecurityProfile) SetID(id string) { 49 | b.Id = id 50 | } 51 | 52 | func (b *WirelessSecurityProfile) AfterAddHook(r *routeros.Reply) { 53 | b.Id = r.Done.Map["ret"] 54 | } 55 | 56 | // Typed wrappers 57 | func (c Mikrotik) AddWirelessSecurityProfile(r *WirelessSecurityProfile) (*WirelessSecurityProfile, error) { 58 | res, err := c.Add(r) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | return res.(*WirelessSecurityProfile), nil 64 | } 65 | 66 | func (c Mikrotik) UpdateWirelessSecurityProfile(r *WirelessSecurityProfile) (*WirelessSecurityProfile, error) { 67 | res, err := c.Update(r) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | return res.(*WirelessSecurityProfile), nil 73 | } 74 | 75 | func (c Mikrotik) FindWirelessSecurityProfile(id string) (*WirelessSecurityProfile, error) { 76 | res, err := c.Find(&WirelessSecurityProfile{Id: id}) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | return res.(*WirelessSecurityProfile), nil 82 | } 83 | 84 | func (c Mikrotik) ListWirelessSecurityProfile() ([]WirelessSecurityProfile, error) { 85 | res, err := c.List(&WirelessSecurityProfile{}) 86 | if err != nil { 87 | return nil, err 88 | } 89 | returnSlice := make([]WirelessSecurityProfile, len(res)) 90 | for i, v := range res { 91 | returnSlice[i] = *(v.(*WirelessSecurityProfile)) 92 | } 93 | 94 | return returnSlice, nil 95 | } 96 | 97 | func (c Mikrotik) DeleteWirelessSecurityProfile(id string) error { 98 | return c.Delete(&WirelessSecurityProfile{Id: id}) 99 | } 100 | -------------------------------------------------------------------------------- /client/wireless_security_profile_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestWirelessSecurityProfile_basic(t *testing.T) { 11 | // This test is skipped, until we find a way to include required packages. 12 | // 13 | // Since RouterOS 7.13, 'wireless' package is separate from the main system package 14 | // and there is no easy way to install it in Docker during tests. 15 | // see https://help.mikrotik.com/docs/spaces/ROS/pages/40992872/Packages#Packages-RouterOSpackages 16 | SkipIfRouterOSV7OrLater(t, sysResources) 17 | 18 | c := NewClient(GetConfigFromEnv()) 19 | 20 | randSuffix := RandomString() 21 | expected := &WirelessSecurityProfile{ 22 | Name: "test-profile-" + randSuffix, 23 | Mode: WirelessModeNone, 24 | AuthenticationTypes: []string{}, 25 | } 26 | 27 | created, err := c.AddWirelessSecurityProfile(expected) 28 | require.NoError(t, err) 29 | defer c.DeleteWirelessSecurityProfile(created.Id) 30 | 31 | expected.Id = created.Id 32 | assert.Equal(t, expected, created) 33 | 34 | updated := &WirelessSecurityProfile{} 35 | *updated = *created 36 | updated.Name += "-updated" 37 | updated.Mode = WirelessModeDynamicKeys 38 | updated.AuthenticationTypes = []string{WirelessAuthenticationTypeWpa2Psk} 39 | updated.WPA2PreSharedKey = "1234567890" 40 | _, err = c.UpdateWirelessSecurityProfile(updated) 41 | require.NoError(t, err) 42 | 43 | found, err := c.FindWirelessSecurityProfile(updated.Id) 44 | require.NoError(t, err) 45 | 46 | assert.Equal(t, updated, found) 47 | 48 | } 49 | -------------------------------------------------------------------------------- /cmd/mikrotik-codegen/internal/codegen/formatter.go: -------------------------------------------------------------------------------- 1 | package codegen 2 | 3 | import ( 4 | "go/format" 5 | ) 6 | 7 | // SourceFormatHook formats code using Go's formatter 8 | func SourceFormatHook(p []byte) ([]byte, error) { 9 | return format.Source(p) 10 | } 11 | -------------------------------------------------------------------------------- /cmd/mikrotik-codegen/internal/codegen/generator_mikrotik.go: -------------------------------------------------------------------------------- 1 | package codegen 2 | 3 | import ( 4 | "io" 5 | "text/template" 6 | consoleinspected "github.com/ddelnano/terraform-provider-mikrotik/client/console-inspected" 7 | "github.com/ddelnano/terraform-provider-mikrotik/cmd/mikrotik-codegen/internal/utils" 8 | ) 9 | 10 | func GenerateMikrotikResource(resourceName, commandBasePath string, 11 | consoleCommandDefinition consoleinspected.ConsoleItem, 12 | w io.Writer) error { 13 | if err := writeWrapper(w, []byte(generatedNotice)); err != nil { 14 | return err 15 | } 16 | t := template.New("resource") 17 | t.Funcs(template.FuncMap{ 18 | "pascalCase": utils.PascalCase, 19 | }) 20 | if _, err := t.Parse(mikrotikResourceDefinitionTemplate); err != nil { 21 | return err 22 | } 23 | 24 | fieldNames := make([]string, 0, len(consoleCommandDefinition.Arguments)) 25 | for i := range consoleCommandDefinition.Arguments { 26 | fieldNames = append(fieldNames, consoleCommandDefinition.Arguments[i].Name) 27 | } 28 | 29 | data := struct { 30 | CommandBasePath string 31 | ResourceName string 32 | FieldNames []string 33 | }{ 34 | CommandBasePath: commandBasePath, 35 | ResourceName: resourceName, 36 | FieldNames: fieldNames, 37 | } 38 | return generateCode( 39 | w, 40 | "resource", 41 | mikrotikResourceDefinitionTemplate, 42 | data, 43 | ) 44 | } 45 | 46 | func GenerateMikrotikResourceTest(resourceName string, s *Struct, w io.Writer) error { 47 | data, err := generateTemplateData(*s) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | return generateCode( 53 | w, 54 | "resource-test", 55 | mikrotikResourceTestDefinitionTemplate, 56 | data, 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /cmd/mikrotik-codegen/internal/codegen/types.go: -------------------------------------------------------------------------------- 1 | package codegen 2 | 3 | const ( 4 | typeString = "String" 5 | typeInt64 = "Int64" 6 | typeList = "List" 7 | typeSet = "Set" 8 | typeBool = "Bool" 9 | typeStringSlice = "StringSlice" 10 | typeIntSlice = "IntSlice" 11 | typeUnknown = "unknown" 12 | ) 13 | 14 | type ( 15 | basetype struct { 16 | typeName string 17 | } 18 | 19 | // Type represents Terraform field type to use for particular MikroTik field. 20 | Type interface { 21 | // Type returns a type name as string. 22 | // It must be stable for the same type. 23 | Name() string 24 | 25 | // Is checks whether two types are the same. 26 | Is(Type) bool 27 | } 28 | ) 29 | 30 | var ( 31 | StringType Type = basetype{typeName: typeString} 32 | Int64Type Type = basetype{typeName: typeInt64} 33 | ListType Type = basetype{typeName: typeList} 34 | SetType Type = basetype{typeName: typeSet} 35 | BoolType Type = basetype{typeName: typeBool} 36 | StringSliceType Type = basetype{typeName: typeStringSlice} 37 | IntSliceType Type = basetype{typeName: typeIntSlice} 38 | UnknownType Type = basetype{typeName: typeUnknown} 39 | ) 40 | 41 | func (b basetype) Name() string { 42 | return b.typeName 43 | } 44 | 45 | func (b basetype) Is(t Type) bool { 46 | return b.typeName == t.Name() 47 | } 48 | -------------------------------------------------------------------------------- /cmd/mikrotik-codegen/internal/codegen/types_test.go: -------------------------------------------------------------------------------- 1 | package codegen 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestTypeIs(t *testing.T) { 10 | testCases := []struct { 11 | name string 12 | type1 Type 13 | type2 Type 14 | expected bool 15 | }{ 16 | { 17 | name: "int==int", 18 | type1: Int64Type, 19 | type2: Int64Type, 20 | expected: true, 21 | }, 22 | { 23 | name: "list==list", 24 | type1: ListType, 25 | type2: ListType, 26 | expected: true, 27 | }, 28 | { 29 | name: "int==string", 30 | type1: Int64Type, 31 | type2: StringType, 32 | }, 33 | { 34 | name: "list==string", 35 | type1: ListType, 36 | type2: StringType, 37 | }, 38 | { 39 | name: "list==set", 40 | type1: ListType, 41 | type2: SetType, 42 | }, 43 | { 44 | name: "bool==unknown", 45 | type1: BoolType, 46 | type2: UnknownType, 47 | }, 48 | } 49 | for _, tc := range testCases { 50 | t.Run(tc.name, func(t *testing.T) { 51 | actual := tc.type1.Is(tc.type2) 52 | assert.Equal(t, tc.expected, actual) 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /cmd/mikrotik-codegen/internal/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | // ToSnakeCase converts in string to snake_case 9 | func ToSnakeCase(in string) string { 10 | var isPrevLower bool 11 | var buf strings.Builder 12 | 13 | for _, r := range in { 14 | if 'A' <= r && r <= 'Z' && isPrevLower { 15 | buf.WriteByte('_') 16 | buf.WriteString(strings.ToLower(string(r))) 17 | isPrevLower = false 18 | continue 19 | } 20 | 21 | isPrevLower = 'a' <= r && r <= 'z' 22 | buf.WriteString(strings.ToLower(string(r))) 23 | } 24 | 25 | return buf.String() 26 | } 27 | 28 | // FirstLower makes first symbol lowercase in the string 29 | func FirstLower(s string) string { 30 | if len(s) < 1 { 31 | return s 32 | } 33 | if len(s) == 1 { 34 | return strings.ToLower(s) 35 | } 36 | 37 | return strings.ToLower(s[:1]) + s[1:] 38 | } 39 | 40 | // PascalCase makes every word in input string upper case and removes all not alpha-numeric symbols. 41 | func PascalCase(s string) string { 42 | r := regexp.MustCompile(`[^0-9a-zA-Z-]+`) 43 | rClean := regexp.MustCompile(`[^0-9a-zA-Z]+`) 44 | s = string(r.ReplaceAll([]byte(s), []byte("-"))) 45 | s = strings.Title(s) 46 | 47 | return string(rClean.ReplaceAll([]byte(s), []byte(""))) 48 | } 49 | -------------------------------------------------------------------------------- /cmd/mikrotik-codegen/internal/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestToSnakeCase(t *testing.T) { 8 | testCases := []struct { 9 | name string 10 | input string 11 | expected string 12 | }{ 13 | { 14 | name: "title case", 15 | input: "ClientResourceName", 16 | expected: "client_resource_name", 17 | }, 18 | { 19 | name: "kebab case", 20 | input: "clientResourceName", 21 | expected: "client_resource_name", 22 | }, 23 | { 24 | name: "all lowercase", 25 | input: "clientresourcename", 26 | expected: "clientresourcename", 27 | }, 28 | { 29 | name: "several uppercase at beginning", 30 | input: "IPAddress", 31 | expected: "ipaddress", 32 | }, 33 | { 34 | name: "several uppercase inside", 35 | input: "DefaultHTTPConfig", 36 | expected: "default_httpconfig", 37 | }, 38 | } 39 | for _, tc := range testCases { 40 | t.Run(tc.name, func(t *testing.T) { 41 | result := ToSnakeCase(tc.input) 42 | if result != tc.expected { 43 | t.Errorf(` 44 | expected %s, 45 | got %s`, tc.expected, result) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | func TestFirstLower(t *testing.T) { 52 | testCases := []struct { 53 | name string 54 | input string 55 | expected string 56 | }{ 57 | { 58 | name: "title case", 59 | input: "ClientResourceName", 60 | expected: "clientResourceName", 61 | }, 62 | { 63 | name: "kebab case", 64 | input: "clientResourceName", 65 | expected: "clientResourceName", 66 | }, 67 | } 68 | for _, tc := range testCases { 69 | t.Run(tc.name, func(t *testing.T) { 70 | result := FirstLower(tc.input) 71 | if result != tc.expected { 72 | t.Errorf(` 73 | expected %s, 74 | got %s`, tc.expected, result) 75 | } 76 | }) 77 | } 78 | } 79 | func TestPascalCase(t *testing.T) { 80 | testCases := []struct { 81 | name string 82 | input string 83 | expected string 84 | }{ 85 | { 86 | name: "already PascalCase", 87 | input: "FieldNameInProperCase", 88 | expected: "FieldNameInProperCase", 89 | }, 90 | { 91 | name: "dashes", 92 | input: "field-name-with-dashes", 93 | expected: "FieldNameWithDashes", 94 | }, 95 | { 96 | name: "dashes, underscores", 97 | input: "field-name_with_dashes-and___underscores", 98 | expected: "FieldNameWithDashesAndUnderscores", 99 | }, 100 | { 101 | name: "other symbols", 102 | input: "field/name with+++++different||||symbols", 103 | expected: "FieldNameWithDifferentSymbols", 104 | }, 105 | { 106 | name: "consecutive upper-cased if one-letter word", 107 | input: "field/name with-a/b-testing", 108 | expected: "FieldNameWithABTesting", 109 | }, 110 | } 111 | for _, tc := range testCases { 112 | t.Run(tc.name, func(t *testing.T) { 113 | result := PascalCase(tc.input) 114 | if result != tc.expected { 115 | t.Errorf(` 116 | expected %s, 117 | got %s`, tc.expected, result) 118 | } 119 | }) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | services: 4 | routeros: 5 | image: mnazarenko/docker-routeros:${ROUTEROS_VERSION:-latest} 6 | environment: 7 | DEBUG: "N" 8 | DISPLAY: "web" 9 | ports: 10 | - 127.0.0.1:8728:8728 11 | - 127.0.0.1:2222:22 12 | - 127.0.0.1:8006:8006 13 | - 127.0.0.1:5900:5900 14 | volumes: 15 | - /dev/net/tun:/dev/net/tun 16 | cap_add: 17 | - "NET_ADMIN" 18 | stop_grace_period: 20s 19 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: "Provider: Mikrotik" 3 | description: |- 4 | The mikrotik provider is used to interact with the resources supported by RouterOS. 5 | --- 6 | 7 | # MIKROTIK Provider 8 | 9 | The mikrotik provider is used to interact with the resources supported by RouterOS. 10 | The provider needs to be configured with the proper credentials before it can be used. 11 | 12 | ## Requirements 13 | 14 | * RouterOS v6.45.2+ (It may work with other versions but it is untested against other versions!) 15 | 16 | 17 | ## Example Usage 18 | ```terraform 19 | # Configure the mikrotik Provider 20 | provider "mikrotik" { 21 | host = "hostname-of-server:8728" # Or set MIKROTIK_HOST environment variable 22 | username = "" # Or set MIKROTIK_USER environment variable 23 | password = "" # Or set MIKROTIK_PASSWORD environment variable 24 | tls = true # Or set MIKROTIK_TLS environment variable 25 | ca_certificate = "/path/to/ca/certificate.pem" # Or set MIKROTIK_CA_CERTIFICATE environment variable 26 | insecure = true # Or set MIKROTIK_INSECURE environment variable 27 | } 28 | ``` 29 | 30 | 31 | ## Schema 32 | 33 | ### Optional 34 | 35 | - `ca_certificate` (String) Path to MikroTik's certificate authority 36 | - `host` (String) Hostname of the MikroTik router 37 | - `insecure` (Boolean) Insecure connection does not verify MikroTik's TLS certificate 38 | - `password` (String, Sensitive) Password for MikroTik api 39 | - `tls` (Boolean) Whether to use TLS when connecting to MikroTik or not 40 | - `username` (String) User account for MikroTik api 41 | -------------------------------------------------------------------------------- /docs/resources/bgp_instance.md: -------------------------------------------------------------------------------- 1 | # mikrotik_bgp_instance (Resource) 2 | Creates a Mikrotik BGP Instance. 3 | 4 | !> This resource will not be supported in RouterOS v7+. 5 | Mikrotik has deprecated the underlying commands so future BGP support will need new resources created 6 | (See [this issue](https://github.com/ddelnano/terraform-provider-mikrotik/issues/52) for status of this work). 7 | 8 | ## Example Usage 9 | ```terraform 10 | resource "mikrotik_bgp_instance" "instance" { 11 | name = "bgp-instance-name" 12 | as = 65533 13 | router_id = "172.21.16.20" 14 | comment = "test comment" 15 | } 16 | ``` 17 | 18 | 19 | ## Schema 20 | 21 | ### Required 22 | 23 | - `as` (Number) The 32-bit BGP autonomous system number. Must be a value within 0 to 4294967295. 24 | - `name` (String) The name of the BGP instance. 25 | - `router_id` (String) BGP Router ID (for this instance). If set to 0.0.0.0, BGP will use one of router's IP addresses. 26 | 27 | ### Optional 28 | 29 | - `client_to_client_reflection` (Boolean) In case this instance is a route reflector: whether to redistribute routes learned from one routing reflection client to other clients. Default: `true`. 30 | - `cluster_id` (String) In case this instance is a route reflector: cluster ID of the router reflector cluster this instance belongs to. Default: `""`. 31 | - `comment` (String) The comment of the BGP instance to be created. Default: `""`. 32 | - `confederation` (Number) In case of BGP confederations: autonomous system number that identifies the [local] confederation as a whole. Default: `0`. 33 | - `confederation_peers` (String) List of AS numbers internal to the [local] confederation. For example: `10,20,30-50`. Default: `""`. 34 | - `disabled` (Boolean) Whether instance is disabled. Default: `false`. 35 | - `ignore_as_path_len` (Boolean) Whether to ignore AS_PATH attribute in BGP route selection algorithm. Default: `false`. 36 | - `out_filter` (String) Output routing filter chain used by all BGP peers belonging to this instance. Default: `""`. 37 | - `redistribute_connected` (Boolean) If enabled, this BGP instance will redistribute the information about connected routes. Default: `false`. 38 | - `redistribute_ospf` (Boolean) If enabled, this BGP instance will redistribute the information about routes learned by OSPF. Default: `false`. 39 | - `redistribute_other_bgp` (Boolean) If enabled, this BGP instance will redistribute the information about routes learned by other BGP instances. Default: `false`. 40 | - `redistribute_rip` (Boolean) If enabled, this BGP instance will redistribute the information about routes learned by RIP. Default: `false`. 41 | - `redistribute_static` (Boolean) If enabled, the router will redistribute the information about static routes added to its routing database. Default: `false`. 42 | - `routing_table` (String) Name of routing table this BGP instance operates on. Default: `""`. 43 | 44 | ### Read-Only 45 | 46 | - `id` (String) ID of this resource. 47 | 48 | ## Import 49 | Import is supported using the following syntax: 50 | ```shell 51 | # import with name of bgp instance 52 | terraform import mikrotik_bgp_instance.instance bgp-instance-name 53 | ``` 54 | -------------------------------------------------------------------------------- /docs/resources/bridge.md: -------------------------------------------------------------------------------- 1 | # mikrotik_bridge (Resource) 2 | Manages a bridge resource on remote MikroTik device. 3 | 4 | ## Example Usage 5 | ```terraform 6 | resource "mikrotik_bridge" "bridge" { 7 | name = "default_bridge" 8 | fast_forward = true 9 | vlan_filtering = false 10 | comment = "Default bridge" 11 | } 12 | ``` 13 | 14 | 15 | ## Schema 16 | 17 | ### Required 18 | 19 | - `name` (String) Name of the bridge interface 20 | 21 | ### Optional 22 | 23 | - `comment` (String) Short description of the interface. 24 | - `fast_forward` (Boolean) Special and faster case of FastPath which works only on bridges with 2 interfaces (enabled by default only for new bridges). Default: `true`. 25 | - `vlan_filtering` (Boolean) Globally enables or disables VLAN functionality for bridge. 26 | 27 | ### Read-Only 28 | 29 | - `id` (String) Unique ID for the instance. 30 | 31 | ## Import 32 | Import is supported using the following syntax: 33 | ```shell 34 | # import with name of bridge 35 | terraform import mikrotik_bridge.bridge 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/resources/bridge_port.md: -------------------------------------------------------------------------------- 1 | # mikrotik_bridge_port (Resource) 2 | Manages ports in bridge associations. 3 | 4 | ## Example Usage 5 | ```terraform 6 | resource "mikrotik_bridge" "bridge" { 7 | name = "default_bridge" 8 | fast_forward = true 9 | vlan_filtering = false 10 | comment = "Default bridge" 11 | } 12 | 13 | resource mikrotik_bridge_port "eth2port" { 14 | bridge = mikrotik_bridge.bridge.name 15 | interface = "ether2" 16 | pvid = 10 17 | comment = "bridge port" 18 | } 19 | ``` 20 | 21 | 22 | ## Schema 23 | 24 | ### Optional 25 | 26 | - `bridge` (String) The bridge interface the respective interface is grouped in. 27 | - `comment` (String) Short description for this association. 28 | - `interface` (String) Name of the interface. 29 | - `pvid` (Number) Port VLAN ID (pvid) specifies which VLAN the untagged ingress traffic is assigned to. This property only has effect when vlan-filtering is set to yes. 30 | 31 | ### Read-Only 32 | 33 | - `id` (String) Unique ID for the instance. 34 | 35 | ## Import 36 | Import is supported using the following syntax: 37 | ```shell 38 | # The ID argument (*19) is a MikroTik's internal id. 39 | terraform import mikrotik_bridge_port.port1 "*19" 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/resources/bridge_vlan.md: -------------------------------------------------------------------------------- 1 | # mikrotik_bridge_vlan (Resource) 2 | Creates a MikroTik BridgeVlan. 3 | 4 | ## Example Usage 5 | ```terraform 6 | resource "mikrotik_bridge" "default" { 7 | name = "main" 8 | } 9 | 10 | resource "mikrotik_bridge_vlan" "testacc" { 11 | bridge = mikrotik_bridge.default.name 12 | tagged = ["ether2", "vlan30"] 13 | untagged = ["ether3"] 14 | vlan_ids = [10, 30] 15 | } 16 | ``` 17 | 18 | 19 | ## Schema 20 | 21 | ### Required 22 | 23 | - `bridge` (String) The bridge interface which the respective VLAN entry is intended for. 24 | 25 | ### Optional 26 | 27 | - `tagged` (Set of String) Interface list with a VLAN tag adding action in egress. 28 | - `untagged` (Set of String) Interface list with a VLAN tag removing action in egress. 29 | - `vlan_ids` (Set of Number) The list of VLAN IDs for certain port configuration. Ranges are not supported yet. 30 | 31 | ### Read-Only 32 | 33 | - `id` (String) A unique ID for this resource. 34 | 35 | ## Import 36 | Import is supported using the following syntax: 37 | ```shell 38 | # import with id of bridge vlan 39 | terraform import mikrotik_bridge_vlan.default "*2" 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/resources/dhcp_lease.md: -------------------------------------------------------------------------------- 1 | # mikrotik_dhcp_lease (Resource) 2 | Creates a DHCP lease on the MikroTik device. 3 | 4 | ## Example Usage 5 | ```terraform 6 | resource "mikrotik_dhcp_lease" "file_server" { 7 | address = "192.168.88.1" 8 | macaddress = "11:22:33:44:55:66" 9 | comment = "file server" 10 | blocked = "false" 11 | } 12 | ``` 13 | 14 | 15 | ## Schema 16 | 17 | ### Required 18 | 19 | - `address` (String) The IP address of the DHCP lease to be created. 20 | - `macaddress` (String) The MAC addreess of the DHCP lease to be created. 21 | 22 | ### Optional 23 | 24 | - `blocked` (Boolean) Whether to block access for this DHCP client (true|false). Default: `false`. 25 | - `comment` (String) The comment of the DHCP lease to be created. 26 | 27 | ### Read-Only 28 | 29 | - `dynamic` (Boolean) Whether the dhcp lease is static or dynamic. Dynamic leases are not guaranteed to continue to be assigned to that specific device. Defaults to false. 30 | - `hostname` (String) The hostname of the device 31 | - `id` (String) Unique resource identifier. 32 | 33 | ## Import 34 | Import is supported using the following syntax: 35 | ```shell 36 | # The resource ID (*19) is a MikroTik's internal id. 37 | # It can be obtained via CLI: 38 | # [admin@MikroTik] /ip dhcp-server lease> :put [find where address=10.0.1.254] 39 | # *19 40 | terraform import mikrotik_dhcp_lease.file_server '*19' 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/resources/dhcp_server.md: -------------------------------------------------------------------------------- 1 | # mikrotik_dhcp_server (Resource) 2 | Manages a DHCP server resource within MikroTik device. 3 | 4 | ## Example Usage 5 | ```terraform 6 | resource "mikrotik_pool" "bar" { 7 | name = "dhcp-pool" 8 | ranges = "10.10.10.100-10.10.10.200" 9 | comment = "Home devices" 10 | } 11 | 12 | resource "mikrotik_dhcp_server" "default" { 13 | address_pool = mikrotik_pool.bar.name 14 | authoritative = "yes" 15 | disabled = false 16 | interface = "ether2" 17 | name = "main-dhcp-server" 18 | } 19 | ``` 20 | 21 | 22 | ## Schema 23 | 24 | ### Required 25 | 26 | - `name` (String) Reference name. 27 | 28 | ### Optional 29 | 30 | - `add_arp` (Boolean) Whether to add dynamic ARP entry. If set to no either ARP mode should be enabled on that interface or static ARP entries should be administratively defined. 31 | - `address_pool` (String) IP pool, from which to take IP addresses for the clients. If set to static-only, then only the clients that have a static lease (added in lease submenu) will be allowed. Default: `static-only`. 32 | - `authoritative` (String) Option changes the way how server responds to DHCP requests. Default: `yes`. 33 | - `disabled` (Boolean) Disable this DHCP server instance. Default: `true`. 34 | - `interface` (String) Interface on which server will be running. Default: `*0`. 35 | - `lease_script` (String) Script that will be executed after lease is assigned or de-assigned. Internal "global" variables that can be used in the script. 36 | 37 | ### Read-Only 38 | 39 | - `id` (String) Unique ID of this resource. 40 | 41 | ## Import 42 | Import is supported using the following syntax: 43 | ```shell 44 | terraform import mikrotik_dhcp_server.default 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/resources/dhcp_server_network.md: -------------------------------------------------------------------------------- 1 | # mikrotik_dhcp_server_network (Resource) 2 | Manages a DHCP network resource within Mikrotik device. 3 | 4 | ## Example Usage 5 | ```terraform 6 | resource "mikrotik_dhcp_server_network" "default" { 7 | address = "192.168.100.0/24" 8 | netmask = "0" # use mask from address 9 | gateway = "192.168.100.1" 10 | dns_server = "192.168.100.2" 11 | comment = "Default DHCP server network" 12 | } 13 | ``` 14 | 15 | 16 | ## Schema 17 | 18 | ### Optional 19 | 20 | - `address` (String) The network DHCP server(s) will lease addresses from. 21 | - `comment` (String) 22 | - `dns_server` (String) The DHCP client will use these as the default DNS servers. 23 | - `gateway` (String) The default gateway to be used by DHCP Client. Default: `0.0.0.0`. 24 | - `netmask` (String) The actual network mask to be used by DHCP client. If set to '0' - netmask from network address will be used. 25 | 26 | ### Read-Only 27 | 28 | - `id` (String) Unique ID of this resource. 29 | 30 | ## Import 31 | Import is supported using the following syntax: 32 | ```shell 33 | terraform import mikrotik_dhcp_server_network.default 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/resources/dns_record.md: -------------------------------------------------------------------------------- 1 | # mikrotik_dns_record (Resource) 2 | Creates a DNS record on the MikroTik device. 3 | 4 | ## Example Usage 5 | ```terraform 6 | resource "mikrotik_dns_record" "record" { 7 | name = "example.domain.com" 8 | address = "192.168.88.1" 9 | ttl = 300 10 | } 11 | 12 | resource "mikrotik_dns_record" "record_regexp" { 13 | regexp = ".+\\.example\\.domain\\.com" 14 | address = "192.168.88.1" 15 | ttl = 300 16 | } 17 | ``` 18 | 19 | 20 | ## Schema 21 | 22 | ### Required 23 | 24 | - `address` (String) The A record to be returend from the DNS hostname. 25 | 26 | ### Optional 27 | 28 | - `comment` (String) The comment text associated with the DNS record. 29 | - `name` (String) The name of the DNS hostname to be created. 30 | - `regexp` (String) Regular expression against which domain names should be verified. 31 | - `ttl` (Number) The ttl of the DNS record. 32 | 33 | ### Read-Only 34 | 35 | - `id` (String) Unique ID of this resource. 36 | 37 | ## Import 38 | Import is supported using the following syntax: 39 | ```shell 40 | # The ID argument (*2) is a MikroTik's internal id. 41 | # It can be obtained via CLI: 42 | # 43 | # [admin@MikroTik] /ip dns static> :put [find where address="192.168.88.1/24"] 44 | # *2 45 | terraform import mikrotik_dns_record.record "*2" 46 | ``` 47 | -------------------------------------------------------------------------------- /docs/resources/firewall_filter_rule.md: -------------------------------------------------------------------------------- 1 | # mikrotik_firewall_filter_rule (Resource) 2 | Creates a MikroTik FirewallFilterRule. 3 | 4 | ## Example Usage 5 | ```terraform 6 | resource "mikrotik_firewall_filter_rule" "https" { 7 | action = "accept" 8 | chain = "forward" 9 | comment = "Web access to local HTTP server" 10 | connection_state = ["new"] 11 | dst_port = "443" 12 | in_interface = "ether1" 13 | in_interface_list = "local_lan" 14 | out_interface_list = "ether3" 15 | protocol = "tcp" 16 | } 17 | ``` 18 | 19 | 20 | ## Schema 21 | 22 | ### Required 23 | 24 | - `chain` (String) Specifies to which chain rule will be added. If the input does not match the name of an already defined chain, a new chain will be created. 25 | 26 | ### Optional 27 | 28 | - `action` (String) Action to take if packet is matched by the rule. Default: `accept`. 29 | - `comment` (String) Comment to the rule. 30 | - `connection_state` (Set of String) Interprets the connection tracking analysis data for a particular packet. 31 | - `dst_port` (String) List of destination port numbers or port number ranges. 32 | - `in_interface` (String) Interface the packet has entered the router. 33 | - `in_interface_list` (String) Set of interfaces defined in interface list. Works the same as in-interface. 34 | - `out_interface_list` (String) Set of interfaces defined in interface list. Works the same as out-interface. 35 | - `protocol` (String) Matches particular IP protocol specified by protocol name or number. Default: `tcp`. 36 | 37 | ### Read-Only 38 | 39 | - `id` (String) Unique ID of this resource. 40 | 41 | ## Import 42 | Import is supported using the following syntax: 43 | ```shell 44 | terraform import mikrotik_firewall_filter_rule.forward[0] '*19' 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/resources/interface_list.md: -------------------------------------------------------------------------------- 1 | # mikrotik_interface_list (Resource) 2 | Allows to define set of interfaces for easier interface management. 3 | 4 | ## Example Usage 5 | ```terraform 6 | resource "mikrotik_interface_list" "default" { 7 | name = "ethernet_interfaces" 8 | comment = "All ethernet interfaces" 9 | } 10 | ``` 11 | 12 | 13 | ## Schema 14 | 15 | ### Required 16 | 17 | - `name` (String) Name of the interface list. 18 | 19 | ### Optional 20 | 21 | - `comment` (String) Comment to this list. 22 | 23 | ### Read-Only 24 | 25 | - `id` (String) Unique ID of this resource. 26 | 27 | ## Import 28 | Import is supported using the following syntax: 29 | ```shell 30 | terraform import mikrotik_interface_list.default 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/resources/interface_list_member.md: -------------------------------------------------------------------------------- 1 | # mikrotik_interface_list_member (Resource) 2 | Allows to define set of interfaces for easier interface management. 3 | 4 | ## Example Usage 5 | ```terraform 6 | resource "mikrotik_interface_list" "lan" { 7 | name = "lan" 8 | } 9 | 10 | resource "mikrotik_interface_list_member" "lan" { 11 | interface = "ether2" 12 | list = mikrotik_interface_list.lan.name 13 | } 14 | ``` 15 | 16 | 17 | ## Schema 18 | 19 | ### Required 20 | 21 | - `interface` (String) Name of the interface. 22 | - `list` (String) Name of the interface list 23 | 24 | ### Read-Only 25 | 26 | - `id` (String) Unique ID of this resource. 27 | 28 | ## Import 29 | Import is supported using the following syntax: 30 | ```shell 31 | terraform import mikrotik_interface_list_member.default 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/resources/interface_wireguard.md: -------------------------------------------------------------------------------- 1 | # mikrotik_interface_wireguard (Resource) 2 | Creates a Mikrotik interface wireguard only supported by RouterOS v7+. 3 | 4 | ## Example Usage 5 | ```terraform 6 | resource "mikrotik_interface_wireguard" "default" { 7 | name = "wireguard-interface" 8 | comment = "new interface" 9 | } 10 | ``` 11 | 12 | 13 | ## Schema 14 | 15 | ### Required 16 | 17 | - `name` (String) Name of the interface wireguard. 18 | 19 | ### Optional 20 | 21 | - `comment` (String) Comment associated with interface wireguard. Default: `""`. 22 | - `disabled` (Boolean) Boolean for whether or not the interface wireguard is disabled. Default: `false`. 23 | - `listen_port` (Number) Port for WireGuard service to listen on for incoming sessions. Default: `13231`. 24 | - `mtu` (Number) Layer3 Maximum transmission unit. Default: `1420`. 25 | - `private_key` (String, Sensitive) A base64 private key. If not specified, it will be automatically generated upon interface creation. 26 | 27 | ### Read-Only 28 | 29 | - `id` (String) Identifier of this resource assigned by RouterOS 30 | - `public_key` (String) A base64 public key is calculated from the private key. 31 | - `running` (Boolean) Whether the interface is running. 32 | 33 | ## Import 34 | Import is supported using the following syntax: 35 | ```shell 36 | terraform import mikrotik_interface_wireguard.default 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/resources/interface_wireguard_peer.md: -------------------------------------------------------------------------------- 1 | # mikrotik_interface_wireguard_peer (Resource) 2 | Creates a Mikrotik Interface Wireguard Peer only supported by RouterOS v7+. 3 | 4 | ## Example Usage 5 | ```terraform 6 | resource "mikrotik_interface_wireguard" "default" { 7 | name = "wireguard-interface" 8 | comment = "new interface" 9 | } 10 | 11 | resource "mikrotik_interface_wireguard_peer" "default" { 12 | interface = mikrotik_interface_wireguard.default.name 13 | public_key = "v/oIzPyFm1FPHrqhytZgsKjU7mUToQHLrW+Tb5e601M=" 14 | comment = "peer-1" 15 | allowed_address = "0.0.0.0/0" 16 | } 17 | ``` 18 | 19 | 20 | ## Schema 21 | 22 | ### Required 23 | 24 | - `interface` (String) Name of the WireGuard interface the peer belongs to. 25 | 26 | ### Optional 27 | 28 | - `allowed_address` (String) List of IP (v4 or v6) addresses with CIDR masks from which incoming traffic for this peer is allowed and to which outgoing traffic for this peer is directed. The catch-all 0.0.0.0/0 may be specified for matching all IPv4 addresses, and ::/0 may be specified for matching all IPv6 addresses. Default: `""`. 29 | - `comment` (String) Short description of the peer. Default: `""`. 30 | - `disabled` (Boolean) Boolean for whether or not the interface peer is disabled. Default: `false`. 31 | - `endpoint_address` (String) An endpoint IP or hostname can be left blank to allow remote connection from any address. Default: `""`. 32 | - `endpoint_port` (Number) An endpoint port can be left blank to allow remote connection from any port. Default: `0`. 33 | - `persistent_keepalive` (Number) A seconds interval, between 1 and 65535 inclusive, of how often to send an authenticated empty packet to the peer for the purpose of keeping a stateful firewall or NAT mapping valid persistently. For example, if the interface very rarely sends traffic, but it might at anytime receive traffic from a peer, and it is behind NAT, the interface might benefit from having a persistent keepalive interval of 25 seconds. Default: `0`. 34 | - `preshared_key` (String) A base64 preshared key. Optional, and may be omitted. This option adds an additional layer of symmetric-key cryptography to be mixed into the already existing public-key cryptography, for post-quantum resistance. Default: `""`. 35 | - `public_key` (String) The remote peer's calculated public key. 36 | 37 | ### Read-Only 38 | 39 | - `id` (String) Identifier of this resource assigned by RouterOS 40 | 41 | ## Import 42 | Import is supported using the following syntax: 43 | ```shell 44 | terraform import mikrotik_interface_wireguard_peer.default 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/resources/ip_address.md: -------------------------------------------------------------------------------- 1 | # mikrotik_ip_address (Resource) 2 | Assigns an IP address to an interface. 3 | 4 | ## Example Usage 5 | ```terraform 6 | resource "mikrotik_ip_address" "lan" { 7 | address = "192.168.88.1/24" 8 | comment = "LAN Network" 9 | interface = "ether1" 10 | } 11 | ``` 12 | 13 | 14 | ## Schema 15 | 16 | ### Required 17 | 18 | - `address` (String) The IP address and netmask of the interface using slash notation. 19 | - `interface` (String) The interface on which the IP address is assigned. 20 | 21 | ### Optional 22 | 23 | - `comment` (String) The comment for the IP address assignment. 24 | - `disabled` (Boolean) Whether to disable IP address. 25 | 26 | ### Read-Only 27 | 28 | - `id` (String) Unique ID of this resource. 29 | - `network` (String) IP address for the network. 30 | 31 | ## Import 32 | Import is supported using the following syntax: 33 | ```shell 34 | # The ID argument (*19) is a MikroTik's internal id. 35 | # It can be obtained via CLI: 36 | # 37 | # [admin@MikroTik] /ip address> :put [find where address="192.168.88.1/24"] 38 | # *19 39 | terraform import mikrotik_ip_address.lan '*19' 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/resources/ipv6_address.md: -------------------------------------------------------------------------------- 1 | # mikrotik_ipv6_address (Resource) 2 | Creates a MikroTik Ipv6Address. 3 | 4 | ## Example Usage 5 | ```terraform 6 | resource "mikrotik_ipv6_address" "lan" { 7 | address = "2001::1/64" 8 | comment = "LAN Network" 9 | interface = "ether1" 10 | } 11 | ``` 12 | 13 | 14 | ## Schema 15 | 16 | ### Required 17 | 18 | - `address` (String) The IPv6 address and prefix length of the interface using slash notation. 19 | - `interface` (String) The interface on which the IPv6 address is assigned. 20 | 21 | ### Optional 22 | 23 | - `advertise` (Boolean) Whether to enable stateless address configuration. The prefix of that address is automatically advertised to hosts using ICMPv6 protocol. The option is set by default for addresses with prefix length 64. Default: `false`. 24 | - `comment` (String) The comment for the IPv6 address assignment. 25 | - `disabled` (Boolean) Whether to disable IPv6 address. Default: `false`. 26 | - `eui_64` (Boolean) Whether to calculate EUI-64 address and use it as last 64 bits of the IPv6 address. Default: `false`. 27 | - `from_pool` (String) Name of the pool from which prefix will be taken to construct IPv6 address taking last part of the address from address property. Default: `""`. 28 | - `no_dad` (Boolean) If set indicates that address is anycast address and Duplicate Address Detection should not be performed. Default: `false`. 29 | 30 | ### Read-Only 31 | 32 | - `id` (String) Unique identifier for this resource. 33 | 34 | ## Import 35 | Import is supported using the following syntax: 36 | ```shell 37 | # The ID argument (*19) is a MikroTik's internal id. 38 | # It can be obtained via CLI: 39 | # 40 | # [admin@MikroTik] /ipv6 address> :put [find where address="192.168.88.1/24"] 41 | # *19 42 | terraform import mikrotik_ipv6_address.lan *19 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/resources/pool.md: -------------------------------------------------------------------------------- 1 | # mikrotik_pool (Resource) 2 | Creates a Mikrotik IP Pool. 3 | 4 | ## Example Usage 5 | ```terraform 6 | resource "mikrotik_pool" "pool" { 7 | name = "pool-name" 8 | ranges = "172.16.0.6-172.16.0.12" 9 | comment = "ip pool with range specified" 10 | } 11 | ``` 12 | 13 | 14 | ## Schema 15 | 16 | ### Required 17 | 18 | - `name` (String) The name of IP pool. 19 | - `ranges` (String) The IP range(s) of the pool. Multiple ranges can be specified, separated by commas: `172.16.0.6-172.16.0.12,172.16.0.50-172.16.0.60`. 20 | 21 | ### Optional 22 | 23 | - `comment` (String) The comment of the IP Pool to be created. 24 | - `next_pool` (String) The IP pool to pick next address from if current is exhausted. 25 | 26 | ### Read-Only 27 | 28 | - `id` (String) ID of this resource. 29 | 30 | ## Import 31 | Import is supported using the following syntax: 32 | ```shell 33 | # The ID argument (*17) is a MikroTik's internal id. 34 | # It can be obtained via CLI: 35 | # 36 | # [admin@MikroTik] /ip pool> :put [ find where name=pool-name] 37 | # *17 38 | terraform import mikrotik_pool.pool '*17' 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/resources/scheduler.md: -------------------------------------------------------------------------------- 1 | # mikrotik_scheduler (Resource) 2 | Creates a Mikrotik scheduler. 3 | 4 | ## Example Usage 5 | ```terraform 6 | resource "mikrotik_scheduler" "scheduler" { 7 | name = "scheduler-name" 8 | on_event = "scheduler-to-execute" 9 | # Run every 5 mins 10 | interval = 300 11 | } 12 | ``` 13 | 14 | 15 | ## Schema 16 | 17 | ### Required 18 | 19 | - `name` (String) Name of the task. 20 | - `on_event` (String) Name of the script to execute. It must exist `/system script`. 21 | 22 | ### Optional 23 | 24 | - `interval` (Number) Interval between two script executions, if time interval is set to zero, the script is only executed at its start time, otherwise it is executed repeatedly at the time interval is specified. 25 | - `start_date` (String) Date of the first script execution. 26 | - `start_time` (String) Time of the first script execution. 27 | 28 | ### Read-Only 29 | 30 | - `id` (String) Identifier of this resource assigned by RouterOS 31 | 32 | ## Import 33 | Import is supported using the following syntax: 34 | ```shell 35 | terraform import mikrotik_scheduler.scheduler scheduler-name 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/resources/script.md: -------------------------------------------------------------------------------- 1 | # mikrotik_script (Resource) 2 | Creates a MikroTik Script. 3 | 4 | ## Example Usage 5 | ```terraform 6 | resource "mikrotik_script" "script" { 7 | name = "script-name" 8 | owner = "admin" 9 | policy = [ 10 | "ftp", 11 | "reboot", 12 | ] 13 | source = < 20 | ## Schema 21 | 22 | ### Required 23 | 24 | - `name` (String) The name of script. 25 | - `policy` (List of String) What permissions the script has. This must be one of the following: ftp, reboot, read, write, policy, test, password, sniff, sensitive, romon. 26 | - `source` (String) The source code of the script. See the [MikroTik docs](https://wiki.mikrotik.com/wiki/Manual:Scripting) for the scripting language. 27 | 28 | ### Optional 29 | 30 | - `dont_require_permissions` (Boolean) If the script requires permissions or not. Default: `false`. 31 | 32 | ### Read-Only 33 | 34 | - `id` (String) ID of this resource. 35 | - `owner` (String) The owner of the script. 36 | 37 | ## Import 38 | Import is supported using the following syntax: 39 | ```shell 40 | terraform import mikrotik_script.script script-name 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/resources/vlan_interface.md: -------------------------------------------------------------------------------- 1 | # mikrotik_vlan_interface (Resource) 2 | Manages Virtual Local Area Network (VLAN) interfaces. 3 | 4 | ## Example Usage 5 | ```terraform 6 | resource "mikrotik_vlan_interface" "default" { 7 | interface = "ether2" 8 | mtu = 1500 9 | name = "vlan-20" 10 | vlan_id = 20 11 | } 12 | ``` 13 | 14 | 15 | ## Schema 16 | 17 | ### Required 18 | 19 | - `name` (String) Interface name. 20 | 21 | ### Optional 22 | 23 | - `disabled` (Boolean) Whether to create the interface in disabled state. Default: `false`. 24 | - `interface` (String) Name of physical interface on top of which VLAN will work. Default: `*0`. 25 | - `mtu` (Number) Layer3 Maximum transmission unit. Default: `1500`. 26 | - `use_service_tag` (Boolean) 802.1ad compatible Service Tag. Default: `false`. 27 | - `vlan_id` (Number) Virtual LAN identifier or tag that is used to distinguish VLANs. Must be equal for all computers that belong to the same VLAN. Default: `1`. 28 | 29 | ### Read-Only 30 | 31 | - `id` (String) ID of the resource. 32 | 33 | ## Import 34 | Import is supported using the following syntax: 35 | ```shell 36 | terraform import mikrotik_vlan_interface.default 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/resources/wireless_interface.md: -------------------------------------------------------------------------------- 1 | # mikrotik_wireless_interface (Resource) 2 | Creates a MikroTik WirelessInterface. 3 | 4 | 5 | 6 | 7 | ## Schema 8 | 9 | ### Required 10 | 11 | - `name` (String) Name of the interface. 12 | 13 | ### Optional 14 | 15 | - `disabled` (Boolean) Whether interface is disabled. Default: `true`. 16 | - `hide_ssid` (Boolean) This property has an effect only in AP mode. Default: `false`. 17 | - `master_interface` (String) Name of wireless interface that has virtual-ap capability. Virtual AP interface will only work if master interface is in ap-bridge, bridge, station or wds-slave mode. This property is only for virtual AP interfaces. Default: `""`. 18 | - `mode` (String) Selection between different station and access point (AP) modes. Default: `station`. 19 | - `security_profile` (String) Name of profile from security-profiles. Default: `default`. 20 | - `ssid` (String) SSID (service set identifier) is a name that identifies wireless network. 21 | - `vlan_id` (Number) VLAN identification number. Default: `1`. 22 | - `vlan_mode` (String) Three VLAN modes are available: no-tag|use-service-tag|use-tag. Default: `no-tag`. 23 | 24 | ### Read-Only 25 | 26 | - `id` (String) Unique identifier for this resource. 27 | 28 | 29 | -------------------------------------------------------------------------------- /examples/provider/provider.tf: -------------------------------------------------------------------------------- 1 | # Configure the mikrotik Provider 2 | provider "mikrotik" { 3 | host = "hostname-of-server:8728" # Or set MIKROTIK_HOST environment variable 4 | username = "" # Or set MIKROTIK_USER environment variable 5 | password = "" # Or set MIKROTIK_PASSWORD environment variable 6 | tls = true # Or set MIKROTIK_TLS environment variable 7 | ca_certificate = "/path/to/ca/certificate.pem" # Or set MIKROTIK_CA_CERTIFICATE environment variable 8 | insecure = true # Or set MIKROTIK_INSECURE environment variable 9 | } 10 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_bgp_instance/import.sh: -------------------------------------------------------------------------------- 1 | # import with name of bgp instance 2 | terraform import mikrotik_bgp_instance.instance bgp-instance-name 3 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_bgp_instance/resource.tf: -------------------------------------------------------------------------------- 1 | resource "mikrotik_bgp_instance" "instance" { 2 | name = "bgp-instance-name" 3 | as = 65533 4 | router_id = "172.21.16.20" 5 | comment = "test comment" 6 | } 7 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_bgp_peer/import.sh: -------------------------------------------------------------------------------- 1 | # import with name of bgp peer 2 | terraform import mikrotik_bgp_peer.peer bgp-peer-name 3 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_bgp_peer/resource.tf: -------------------------------------------------------------------------------- 1 | resource "mikrotik_bpg_instance" "instance" { 2 | name = "bgp-instance-name" 3 | as = 65533 4 | router_id = "172.21.16.20" 5 | comment = "test comment" 6 | } 7 | 8 | resource "mikrotik_bgp_peer" "peer" { 9 | name = "bgp-peer-name" 10 | remote_as = 65533 11 | remote_address = "172.21.16.20" 12 | instance = mikrotik_bgp_instance.instance.name 13 | } 14 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_bridge/import.sh: -------------------------------------------------------------------------------- 1 | # import with name of bridge 2 | terraform import mikrotik_bridge.bridge 3 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_bridge/resource.tf: -------------------------------------------------------------------------------- 1 | resource "mikrotik_bridge" "bridge" { 2 | name = "default_bridge" 3 | fast_forward = true 4 | vlan_filtering = false 5 | comment = "Default bridge" 6 | } 7 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_bridge_port/import.sh: -------------------------------------------------------------------------------- 1 | # The ID argument (*19) is a MikroTik's internal id. 2 | terraform import mikrotik_bridge_port.port1 "*19" 3 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_bridge_port/resource.tf: -------------------------------------------------------------------------------- 1 | resource "mikrotik_bridge" "bridge" { 2 | name = "default_bridge" 3 | fast_forward = true 4 | vlan_filtering = false 5 | comment = "Default bridge" 6 | } 7 | 8 | resource mikrotik_bridge_port "eth2port" { 9 | bridge = mikrotik_bridge.bridge.name 10 | interface = "ether2" 11 | pvid = 10 12 | comment = "bridge port" 13 | } 14 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_bridge_vlan/import.sh: -------------------------------------------------------------------------------- 1 | # import with id of bridge vlan 2 | terraform import mikrotik_bridge_vlan.default "*2" 3 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_bridge_vlan/resource.tf: -------------------------------------------------------------------------------- 1 | resource "mikrotik_bridge" "default" { 2 | name = "main" 3 | } 4 | 5 | resource "mikrotik_bridge_vlan" "testacc" { 6 | bridge = mikrotik_bridge.default.name 7 | tagged = ["ether2", "vlan30"] 8 | untagged = ["ether3"] 9 | vlan_ids = [10, 30] 10 | } 11 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_dhcp_lease/import.sh: -------------------------------------------------------------------------------- 1 | # The resource ID (*19) is a MikroTik's internal id. 2 | # It can be obtained via CLI: 3 | # [admin@MikroTik] /ip dhcp-server lease> :put [find where address=10.0.1.254] 4 | # *19 5 | terraform import mikrotik_dhcp_lease.file_server '*19' 6 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_dhcp_lease/resource.tf: -------------------------------------------------------------------------------- 1 | resource "mikrotik_dhcp_lease" "file_server" { 2 | address = "192.168.88.1" 3 | macaddress = "11:22:33:44:55:66" 4 | comment = "file server" 5 | blocked = "false" 6 | } 7 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_dhcp_server/import.sh: -------------------------------------------------------------------------------- 1 | terraform import mikrotik_dhcp_server.default 2 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_dhcp_server/resource.tf: -------------------------------------------------------------------------------- 1 | resource "mikrotik_pool" "bar" { 2 | name = "dhcp-pool" 3 | ranges = "10.10.10.100-10.10.10.200" 4 | comment = "Home devices" 5 | } 6 | 7 | resource "mikrotik_dhcp_server" "default" { 8 | address_pool = mikrotik_pool.bar.name 9 | authoritative = "yes" 10 | disabled = false 11 | interface = "ether2" 12 | name = "main-dhcp-server" 13 | } 14 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_dhcp_server_network/import.sh: -------------------------------------------------------------------------------- 1 | terraform import mikrotik_dhcp_server_network.default 2 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_dhcp_server_network/resource.tf: -------------------------------------------------------------------------------- 1 | resource "mikrotik_dhcp_server_network" "default" { 2 | address = "192.168.100.0/24" 3 | netmask = "0" # use mask from address 4 | gateway = "192.168.100.1" 5 | dns_server = "192.168.100.2" 6 | comment = "Default DHCP server network" 7 | } 8 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_dns_record/import.sh: -------------------------------------------------------------------------------- 1 | # The ID argument (*2) is a MikroTik's internal id. 2 | # It can be obtained via CLI: 3 | # 4 | # [admin@MikroTik] /ip dns static> :put [find where address="192.168.88.1/24"] 5 | # *2 6 | terraform import mikrotik_dns_record.record "*2" 7 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_dns_record/resource.tf: -------------------------------------------------------------------------------- 1 | resource "mikrotik_dns_record" "record" { 2 | name = "example.domain.com" 3 | address = "192.168.88.1" 4 | ttl = 300 5 | } 6 | 7 | resource "mikrotik_dns_record" "record_regexp" { 8 | regexp = ".+\\.example\\.domain\\.com" 9 | address = "192.168.88.1" 10 | ttl = 300 11 | } 12 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_firewall_filter_rule/import.sh: -------------------------------------------------------------------------------- 1 | terraform import mikrotik_firewall_filter_rule.forward[0] '*19' 2 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_firewall_filter_rule/resource.tf: -------------------------------------------------------------------------------- 1 | resource "mikrotik_firewall_filter_rule" "https" { 2 | action = "accept" 3 | chain = "forward" 4 | comment = "Web access to local HTTP server" 5 | connection_state = ["new"] 6 | dst_port = "443" 7 | in_interface = "ether1" 8 | in_interface_list = "local_lan" 9 | out_interface_list = "ether3" 10 | protocol = "tcp" 11 | } 12 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_interface_list/import.sh: -------------------------------------------------------------------------------- 1 | terraform import mikrotik_interface_list.default 2 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_interface_list/resource.tf: -------------------------------------------------------------------------------- 1 | resource "mikrotik_interface_list" "default" { 2 | name = "ethernet_interfaces" 3 | comment = "All ethernet interfaces" 4 | } 5 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_interface_list_member/import.sh: -------------------------------------------------------------------------------- 1 | terraform import mikrotik_interface_list_member.default 2 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_interface_list_member/resource.tf: -------------------------------------------------------------------------------- 1 | resource "mikrotik_interface_list" "lan" { 2 | name = "lan" 3 | } 4 | 5 | resource "mikrotik_interface_list_member" "lan" { 6 | interface = "ether2" 7 | list = mikrotik_interface_list.lan.name 8 | } 9 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_interface_wireguard/import.sh: -------------------------------------------------------------------------------- 1 | terraform import mikrotik_interface_wireguard.default 2 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_interface_wireguard/resource.tf: -------------------------------------------------------------------------------- 1 | resource "mikrotik_interface_wireguard" "default" { 2 | name = "wireguard-interface" 3 | comment = "new interface" 4 | } 5 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_interface_wireguard_peer/import.sh: -------------------------------------------------------------------------------- 1 | terraform import mikrotik_interface_wireguard_peer.default 2 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_interface_wireguard_peer/resource.tf: -------------------------------------------------------------------------------- 1 | resource "mikrotik_interface_wireguard" "default" { 2 | name = "wireguard-interface" 3 | comment = "new interface" 4 | } 5 | 6 | resource "mikrotik_interface_wireguard_peer" "default" { 7 | interface = mikrotik_interface_wireguard.default.name 8 | public_key = "v/oIzPyFm1FPHrqhytZgsKjU7mUToQHLrW+Tb5e601M=" 9 | comment = "peer-1" 10 | allowed_address = "0.0.0.0/0" 11 | } 12 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_ip_address/import.sh: -------------------------------------------------------------------------------- 1 | # The ID argument (*19) is a MikroTik's internal id. 2 | # It can be obtained via CLI: 3 | # 4 | # [admin@MikroTik] /ip address> :put [find where address="192.168.88.1/24"] 5 | # *19 6 | terraform import mikrotik_ip_address.lan '*19' 7 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_ip_address/resource.tf: -------------------------------------------------------------------------------- 1 | resource "mikrotik_ip_address" "lan" { 2 | address = "192.168.88.1/24" 3 | comment = "LAN Network" 4 | interface = "ether1" 5 | } 6 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_ipv6_address/import.sh: -------------------------------------------------------------------------------- 1 | # The ID argument (*19) is a MikroTik's internal id. 2 | # It can be obtained via CLI: 3 | # 4 | # [admin@MikroTik] /ipv6 address> :put [find where address="192.168.88.1/24"] 5 | # *19 6 | terraform import mikrotik_ipv6_address.lan *19 7 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_ipv6_address/resource.tf: -------------------------------------------------------------------------------- 1 | resource "mikrotik_ipv6_address" "lan" { 2 | address = "2001::1/64" 3 | comment = "LAN Network" 4 | interface = "ether1" 5 | } 6 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_pool/import.sh: -------------------------------------------------------------------------------- 1 | # The ID argument (*17) is a MikroTik's internal id. 2 | # It can be obtained via CLI: 3 | # 4 | # [admin@MikroTik] /ip pool> :put [ find where name=pool-name] 5 | # *17 6 | terraform import mikrotik_pool.pool '*17' 7 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_pool/resource.tf: -------------------------------------------------------------------------------- 1 | resource "mikrotik_pool" "pool" { 2 | name = "pool-name" 3 | ranges = "172.16.0.6-172.16.0.12" 4 | comment = "ip pool with range specified" 5 | } 6 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_scheduler/import.sh: -------------------------------------------------------------------------------- 1 | terraform import mikrotik_scheduler.scheduler scheduler-name 2 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_scheduler/resource.tf: -------------------------------------------------------------------------------- 1 | resource "mikrotik_scheduler" "scheduler" { 2 | name = "scheduler-name" 3 | on_event = "scheduler-to-execute" 4 | # Run every 5 mins 5 | interval = 300 6 | } 7 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_script/import.sh: -------------------------------------------------------------------------------- 1 | terraform import mikrotik_script.script script-name 2 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_script/resource.tf: -------------------------------------------------------------------------------- 1 | resource "mikrotik_script" "script" { 2 | name = "script-name" 3 | owner = "admin" 4 | policy = [ 5 | "ftp", 6 | "reboot", 7 | ] 8 | source = < 2 | -------------------------------------------------------------------------------- /examples/resources/mikrotik_vlan_interface/resource.tf: -------------------------------------------------------------------------------- 1 | resource "mikrotik_vlan_interface" "default" { 2 | interface = "ether2" 3 | mtu = 1500 4 | name = "vlan-20" 5 | vlan_id = 20 6 | } 7 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.18 2 | 3 | use ( 4 | . 5 | ./client 6 | ) 7 | -------------------------------------------------------------------------------- /go.work.sum: -------------------------------------------------------------------------------- 1 | github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0= 2 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= 3 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "log" 7 | 8 | "github.com/ddelnano/terraform-provider-mikrotik/mikrotik" 9 | "github.com/hashicorp/terraform-plugin-framework/providerserver" 10 | "github.com/hashicorp/terraform-plugin-go/tfprotov5" 11 | "github.com/hashicorp/terraform-plugin-go/tfprotov5/tf5server" 12 | "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" 13 | "github.com/hashicorp/terraform-plugin-mux/tf6to5server" 14 | ) 15 | 16 | // Generate the Terraform provider documentation using `tfplugindocs`: 17 | // 18 | //go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs 19 | func main() { 20 | var debugMode bool 21 | 22 | flag.BoolVar(&debugMode, "debuggable", false, "set to true to run the provider with support for debuggers like delve") 23 | flag.Parse() 24 | 25 | ctx := context.Background() 26 | 27 | downgradedProviderFramework, err := tf6to5server.DowngradeServer( 28 | ctx, 29 | providerserver.NewProtocol6(mikrotik.NewProviderFramework(nil)), 30 | ) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | 35 | providers := []func() tfprotov5.ProviderServer{ 36 | mikrotik.NewProvider().GRPCProvider, 37 | func() tfprotov5.ProviderServer { return downgradedProviderFramework }, 38 | } 39 | 40 | muxServer, err := tf5muxserver.NewMuxServer(ctx, providers...) 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | 45 | serverOpts := []tf5server.ServeOpt{} 46 | if debugMode { 47 | serverOpts = append(serverOpts, tf5server.WithManagedDebug()) 48 | } 49 | 50 | err = tf5server.Serve("registry.terraform.io/ddelnano/mikrotik", muxServer.ProviderServer, serverOpts...) 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /mikrotik/acc_setup_test.go: -------------------------------------------------------------------------------- 1 | package mikrotik 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ddelnano/terraform-provider-mikrotik/client" 7 | ) 8 | 9 | var sysResources client.SystemResources 10 | 11 | func TestMain(m *testing.M) { 12 | client.SetupAndTestMainExec(m, &sysResources) 13 | } 14 | -------------------------------------------------------------------------------- /mikrotik/internal/test_helpers.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | const ipv6U uint64 = 0x2001000000000000 // upper half of ipv6 address 11 | 12 | var ipCounter uint = 0xC0A80001 // 192 = C0, 168 = A8, 0 = 00, 1 = 01 13 | var ipRangeCounter uint = 0xAC100001 // 172 = AC, 16 = 10, 0 = 00, 1 = 01 14 | var ipv6LCounter uint64 = 0x0000000000000000 // lower half of ipv6 address 15 | var macCounter = 0 16 | var dnsCounter = 0 17 | 18 | func GetNewIpAddr() string { 19 | ipCounter++ 20 | return formatIPv4(ipCounter) 21 | } 22 | 23 | func GetNewIpv6Addr() string { 24 | ipv6LCounter++ 25 | return formatIPv6(ipv6LCounter) 26 | } 27 | 28 | func GetNewIpAddrRange(count uint) string { 29 | var ipRangeStart = ipRangeCounter + 1 30 | ipRangeCounter = ipRangeCounter + count 31 | return fmt.Sprintf("%s-%s", formatIPv4(ipRangeStart), formatIPv4(ipRangeCounter)) 32 | } 33 | 34 | func GetNewMacAddr() string { 35 | macCounter++ 36 | 37 | if macCounter > 255 { 38 | macCounter = 1 39 | } 40 | 41 | return fmt.Sprintf("01:23:45:67:89:%02x", macCounter) 42 | } 43 | 44 | func GetNewDnsName() string { 45 | dnsCounter++ 46 | return fmt.Sprintf("dns-%02d.terraform", dnsCounter) 47 | } 48 | 49 | // JoinIntsToString builds textualrepresentation of a list of integers 50 | func JoinIntsToString(ints []int, sep string) string { 51 | if len(ints) < 1 { 52 | return "" 53 | } 54 | 55 | if len(ints) == 1 { 56 | return strconv.Itoa(ints[0]) 57 | } 58 | 59 | s := strings.Builder{} 60 | s.WriteString(strconv.Itoa(ints[0])) 61 | ints = ints[1:] 62 | for _, v := range ints { 63 | s.WriteString(sep) 64 | s.WriteString(strconv.Itoa(v)) 65 | } 66 | 67 | return s.String() 68 | } 69 | 70 | // JoinStringsToString builds textual representation of a list of strings 71 | func JoinStringsToString(items []string, sep string) string { 72 | if len(items) < 1 { 73 | return "" 74 | } 75 | 76 | if len(items) == 1 { 77 | return "\"" + items[0] + "\"" 78 | } 79 | 80 | return "\"" + strings.Join(items, "\",\"") + "\"" 81 | } 82 | 83 | func formatIPv4(ipAddr uint) string { 84 | return fmt.Sprintf("%d.%d.%d.%d", (ipAddr>>24)&0xFF, (ipAddr>>16)&0xFF, (ipAddr>>8)&0xFF, ipAddr&0xFF) 85 | } 86 | 87 | func formatIPv6(ipv6Addr uint64) string { 88 | return net.ParseIP(fmt.Sprintf( 89 | "%x:%x:%x:%x:%x:%x:%x:%x", 90 | (ipv6U>>48)&0xFFFF, 91 | (ipv6U>>32)&0xFFFF, 92 | (ipv6U>>16)&0xFFFF, 93 | ipv6U&0xFFFF, 94 | (ipv6Addr>>48)&0xFFFF, 95 | (ipv6Addr>>32)&0xFFFF, 96 | (ipv6Addr>>16)&0xFFFF, 97 | ipv6Addr&0xFFFF, 98 | )).String() 99 | } 100 | -------------------------------------------------------------------------------- /mikrotik/internal/types/defaultaware/bool.go: -------------------------------------------------------------------------------- 1 | package defaultaware 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 8 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" 9 | ) 10 | 11 | // BoolAttribute creates a wrapper for schema.BoolAttribute object and generates documentation with default value. 12 | func BoolAttribute(wrapped schema.BoolAttribute) schema.Attribute { 13 | return boolWrapper{wrapped} 14 | } 15 | 16 | func (w boolWrapper) GetDescription() string { 17 | desc := w.BoolAttribute.GetDescription() 18 | if w.Default == nil { 19 | return desc 20 | } 21 | 22 | resp := defaults.BoolResponse{} 23 | w.Default.DefaultBool(context.TODO(), defaults.BoolRequest{}, &resp) 24 | defaultValue := resp.PlanValue.ValueBool() 25 | desc = fmt.Sprintf("%s Default: `%t`.", desc, defaultValue) 26 | 27 | return desc 28 | } 29 | 30 | type boolWrapper struct { 31 | schema.BoolAttribute 32 | } 33 | 34 | var _ schema.Attribute = &boolWrapper{} 35 | -------------------------------------------------------------------------------- /mikrotik/internal/types/defaultaware/bool_test.go: -------------------------------------------------------------------------------- 1 | package defaultaware 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 7 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" 8 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestBoolWrapper(t *testing.T) { 13 | testCases := []struct { 14 | name string 15 | description string 16 | defaultValue defaults.Bool 17 | expectedResult string 18 | }{ 19 | { 20 | name: "no default value", 21 | description: "Attribute description.", 22 | expectedResult: "Attribute description.", 23 | }, 24 | { 25 | name: "true default value", 26 | description: "Attribute description.", 27 | defaultValue: booldefault.StaticBool(true), 28 | expectedResult: "Attribute description. Default: `true`.", 29 | }, { 30 | name: "false default value", 31 | description: "Attribute description.", 32 | defaultValue: booldefault.StaticBool(false), 33 | expectedResult: "Attribute description. Default: `false`.", 34 | }, 35 | } 36 | for _, tc := range testCases { 37 | t.Run(tc.name, func(t *testing.T) { 38 | attr := BoolAttribute( 39 | schema.BoolAttribute{ 40 | Description: tc.description, 41 | Default: tc.defaultValue, 42 | }, 43 | ) 44 | require.Equal(t, tc.expectedResult, attr.GetDescription()) 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /mikrotik/internal/types/defaultaware/int64.go: -------------------------------------------------------------------------------- 1 | package defaultaware 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 8 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" 9 | ) 10 | 11 | // Int64Attribute creates a wrapper for schema.Int64Attribute object and generates documentation with default value. 12 | func Int64Attribute(wrapped schema.Int64Attribute) schema.Attribute { 13 | return int64Wrapper{wrapped} 14 | } 15 | 16 | func (w int64Wrapper) GetDescription() string { 17 | desc := w.Int64Attribute.GetDescription() 18 | if w.Default == nil { 19 | return desc 20 | } 21 | 22 | resp := defaults.Int64Response{} 23 | w.Default.DefaultInt64(context.TODO(), defaults.Int64Request{}, &resp) 24 | defaultValue := resp.PlanValue.ValueInt64() 25 | desc = fmt.Sprintf("%s Default: `%d`.", desc, defaultValue) 26 | 27 | return desc 28 | } 29 | 30 | type int64Wrapper struct { 31 | schema.Int64Attribute 32 | } 33 | 34 | var _ schema.Attribute = &int64Wrapper{} 35 | -------------------------------------------------------------------------------- /mikrotik/internal/types/defaultaware/int64_test.go: -------------------------------------------------------------------------------- 1 | package defaultaware 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 7 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" 8 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestInt64Wrapper(t *testing.T) { 13 | testCases := []struct { 14 | name string 15 | description string 16 | defaultValue defaults.Int64 17 | expectedResult string 18 | }{ 19 | { 20 | name: "no default value", 21 | description: "Attribute description.", 22 | expectedResult: "Attribute description.", 23 | }, 24 | { 25 | name: "with default value", 26 | description: "Attribute description.", 27 | defaultValue: int64default.StaticInt64(2), 28 | expectedResult: "Attribute description. Default: `2`.", 29 | }, { 30 | name: "with zero default value", 31 | description: "Attribute description.", 32 | defaultValue: int64default.StaticInt64(0), 33 | expectedResult: "Attribute description. Default: `0`.", 34 | }, 35 | } 36 | for _, tc := range testCases { 37 | t.Run(tc.name, func(t *testing.T) { 38 | attr := Int64Attribute( 39 | schema.Int64Attribute{ 40 | Description: tc.description, 41 | Default: tc.defaultValue, 42 | }, 43 | ) 44 | require.Equal(t, tc.expectedResult, attr.GetDescription()) 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /mikrotik/internal/types/defaultaware/resource_wrapper.go: -------------------------------------------------------------------------------- 1 | package defaultaware 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/terraform-plugin-framework/resource" 7 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 8 | ) 9 | 10 | // WrapResources wraps the list of provider's resource contructors. 11 | // 12 | // Later, during actual call, the resource instance is wrapped in special proxy to replace every attribute in the schema 13 | // with proper wrapper from "defaultsaware" package. 14 | func WrapResources(funcs []func() resource.Resource) []func() resource.Resource { 15 | for i, f := range funcs { 16 | f := f 17 | funcs[i] = func() resource.Resource { 18 | r := resourceWrapper{f()} 19 | return &r 20 | } 21 | } 22 | 23 | return funcs 24 | } 25 | 26 | // Schema overrides Schema functions from the wrapped resource and makes attributes default-aware. 27 | // 28 | // Default-aware wrappers allows generating documentation with default values, if any. 29 | func (r resourceWrapper) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { 30 | r.Resource.Schema(ctx, req, resp) 31 | 32 | for name, attr := range resp.Schema.Attributes { 33 | switch schemaAttr := attr.(type) { 34 | case schema.StringAttribute: 35 | if schemaAttr.Default != nil { 36 | resp.Schema.Attributes[name] = StringAttribute(schemaAttr) 37 | } 38 | case schema.BoolAttribute: 39 | if schemaAttr.Default != nil { 40 | resp.Schema.Attributes[name] = BoolAttribute(schemaAttr) 41 | } 42 | case schema.Int64Attribute: 43 | if schemaAttr.Default != nil { 44 | resp.Schema.Attributes[name] = Int64Attribute(schemaAttr) 45 | } 46 | } 47 | } 48 | } 49 | 50 | func (r resourceWrapper) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { 51 | rwc := r.Resource.(resource.ResourceWithConfigure) 52 | rwc.Configure(ctx, req, resp) 53 | } 54 | 55 | func (r resourceWrapper) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { 56 | rwi := r.Resource.(resource.ResourceWithImportState) 57 | rwi.ImportState(ctx, req, resp) 58 | } 59 | 60 | type resourceWrapper struct { 61 | resource.Resource 62 | } 63 | 64 | var ( 65 | _ resource.Resource = &resourceWrapper{} 66 | _ resource.ResourceWithConfigure = &resourceWrapper{} 67 | _ resource.ResourceWithImportState = &resourceWrapper{} 68 | ) 69 | -------------------------------------------------------------------------------- /mikrotik/internal/types/defaultaware/string.go: -------------------------------------------------------------------------------- 1 | package defaultaware 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 8 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" 9 | ) 10 | 11 | // StringAttribute creates a wrapper for schema.StringAttribute object and generates documentation with default value. 12 | func StringAttribute(wrapped schema.StringAttribute) schema.Attribute { 13 | return stringWrapper{wrapped} 14 | } 15 | 16 | func (w stringWrapper) GetDescription() string { 17 | desc := w.StringAttribute.GetDescription() 18 | if w.Default == nil { 19 | return desc 20 | } 21 | 22 | resp := defaults.StringResponse{} 23 | w.Default.DefaultString(context.TODO(), defaults.StringRequest{}, &resp) 24 | defaultValue := resp.PlanValue.ValueString() 25 | if defaultValue == "" { 26 | defaultValue = `""` 27 | } 28 | desc = fmt.Sprintf("%s Default: `%s`.", desc, defaultValue) 29 | 30 | return desc 31 | } 32 | 33 | type stringWrapper struct { 34 | schema.StringAttribute 35 | } 36 | 37 | var _ schema.Attribute = &stringWrapper{} 38 | -------------------------------------------------------------------------------- /mikrotik/internal/types/defaultaware/string_test.go: -------------------------------------------------------------------------------- 1 | package defaultaware 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 7 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" 8 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestStringWrapper(t *testing.T) { 13 | testCases := []struct { 14 | name string 15 | description string 16 | defaultValue defaults.String 17 | expectedResult string 18 | }{ 19 | { 20 | name: "no default value", 21 | description: "Attribute description.", 22 | expectedResult: "Attribute description.", 23 | }, 24 | { 25 | name: "with default value", 26 | description: "Attribute description.", 27 | defaultValue: stringdefault.StaticString("some value"), 28 | expectedResult: "Attribute description. Default: `some value`.", 29 | }, { 30 | name: "with empty string default value", 31 | description: "Attribute description.", 32 | defaultValue: stringdefault.StaticString(""), 33 | expectedResult: "Attribute description. Default: `\"\"`.", 34 | }, 35 | } 36 | for _, tc := range testCases { 37 | t.Run(tc.name, func(t *testing.T) { 38 | attr := StringAttribute( 39 | schema.StringAttribute{ 40 | Description: tc.description, 41 | Default: tc.defaultValue, 42 | }, 43 | ) 44 | require.Equal(t, tc.expectedResult, attr.GetDescription()) 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /mikrotik/internal/utils/provider.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/path" 8 | "github.com/hashicorp/terraform-plugin-framework/resource" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | ) 11 | 12 | // ImportStateContextUppercaseWrapper changes the ID of the resource to upper case before passing it to wrappedFunction 13 | // 14 | // This wrapper is useful when resource ID is MikroTik's .id. 15 | // Due to wierd behavior, listing via MikroTik's CLI reports lowercase .id, but find request with this id via API fails 16 | // as it expects upper case string. 17 | // 18 | // Usage in resource definition. 19 | // 20 | // SDKv2 21 | // 22 | // schema.Resource{ 23 | // Importer: &schema.ResourceImporter{ 24 | // StateContext: utils.ImportStateContextUppercaseWrapper(schema.ImportStatePassthroughContext), 25 | // } 26 | // } 27 | func ImportStateContextUppercaseWrapper(wrappedFunc schema.StateContextFunc) schema.StateContextFunc { 28 | return func(ctx context.Context, rd *schema.ResourceData, i interface{}) ([]*schema.ResourceData, error) { 29 | rd.SetId(strings.ToUpper(rd.Id())) 30 | return wrappedFunc(ctx, rd, i) 31 | } 32 | } 33 | 34 | // ImportUppercaseWrapper is ImportStateContextUppercaseWrapper equivalent for PluginFramework. 35 | func ImportUppercaseWrapper(wrappedFunc importStateFunc) importStateFunc { 36 | return func(ctx context.Context, p path.Path, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { 37 | wrappedFunc(ctx, p, resource.ImportStateRequest{ID: strings.ToUpper(req.ID)}, resp) 38 | } 39 | } 40 | 41 | type importStateFunc = func(context.Context, path.Path, resource.ImportStateRequest, *resource.ImportStateResponse) 42 | -------------------------------------------------------------------------------- /mikrotik/internal/utils/provider_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestImportStateContextUppercaseWrapper(t *testing.T) { 12 | testCases := []struct { 13 | name string 14 | in string 15 | expected string 16 | }{ 17 | { 18 | name: "input contains no letter, should be unchanged", 19 | in: "*123", 20 | expected: "*123", 21 | }, 22 | { 23 | name: "input contains digits and only upper case letters, should be unchanged", 24 | in: "*2E", 25 | expected: "*2E", 26 | }, 27 | { 28 | name: "input contains lower case letters, should be mapped to upper case", 29 | in: "*f2", 30 | expected: "*F2", 31 | }, 32 | } 33 | for _, tc := range testCases { 34 | t.Run(tc.name, func(t *testing.T) { 35 | var actual string 36 | f := ImportStateContextUppercaseWrapper( 37 | func(ctx context.Context, rd *schema.ResourceData, i interface{}) ([]*schema.ResourceData, error) { 38 | actual = rd.Id() 39 | return nil, nil 40 | }, 41 | ) 42 | rd := schema.ResourceData{} 43 | rd.SetId(tc.in) 44 | _, _ = f(context.TODO(), &rd, nil) 45 | assert.Equal(t, tc.expected, actual) 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /mikrotik/internal/utils/string.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | // ParseBool is wrapper around strconv.ParseBool to save few lines of code 9 | func ParseBool(v string) (bool, error) { 10 | res, err := strconv.ParseBool(v) 11 | if err != nil { 12 | return res, fmt.Errorf("could not parse %q as bool: %w", v, err) 13 | } 14 | 15 | return res, nil 16 | } 17 | -------------------------------------------------------------------------------- /mikrotik/provider_test.go: -------------------------------------------------------------------------------- 1 | package mikrotik 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | 8 | "github.com/ddelnano/terraform-provider-mikrotik/client" 9 | "github.com/hashicorp/terraform-plugin-framework/providerserver" 10 | "github.com/hashicorp/terraform-plugin-go/tfprotov5" 11 | "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" 12 | "github.com/hashicorp/terraform-plugin-mux/tf6to5server" 13 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 14 | ) 15 | 16 | const ( 17 | // Provider name for single configuration testing 18 | ProviderNameMikrotik = "mikrotik" 19 | ) 20 | 21 | var testAccProtoV5ProviderFactories map[string]func() (tfprotov5.ProviderServer, error) 22 | var testAccProvider *schema.Provider 23 | 24 | var apiClient *client.Mikrotik 25 | 26 | func init() { 27 | apiClient = client.NewClient(os.Getenv("MIKROTIK_HOST"), os.Getenv("MIKROTIK_USER"), os.Getenv("MIKROTIK_PASSWORD"), false, "", true) 28 | 29 | testAccProvider = Provider(apiClient) 30 | downgradedProviderFramework, _ := tf6to5server.DowngradeServer( 31 | context.Background(), 32 | providerserver.NewProtocol6(NewProviderFramework(apiClient)), 33 | ) 34 | servers := []func() tfprotov5.ProviderServer{ 35 | testAccProvider.GRPCProvider, 36 | func() tfprotov5.ProviderServer { 37 | return downgradedProviderFramework 38 | }, 39 | } 40 | muxServer, _ := tf5muxserver.NewMuxServer(context.Background(), servers...) 41 | 42 | testAccProtoV5ProviderFactories = map[string]func() (tfprotov5.ProviderServer, error){ 43 | ProviderNameMikrotik: func() (tfprotov5.ProviderServer, error) { 44 | return muxServer, nil 45 | }, 46 | } 47 | } 48 | 49 | func testAccPreCheck(t *testing.T) { 50 | if v := os.Getenv("MIKROTIK_HOST"); v == "" { 51 | t.Fatal("The MIKROTIK_HOST environment variable must be set") 52 | } 53 | if v := os.Getenv("MIKROTIK_USER"); v == "" { 54 | t.Fatal("The MIKROTIK_USER environment variable must be set") 55 | } 56 | if _, exists := os.LookupEnv("MIKROTIK_PASSWORD"); !exists { 57 | t.Fatal("The MIKROTIK_PASSWORD environment variable must be set") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /mikrotik/resource_bridge_vlan_test.go: -------------------------------------------------------------------------------- 1 | package mikrotik 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/ddelnano/terraform-provider-mikrotik/client" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 10 | ) 11 | 12 | func TestBridgeVlan_basic(t *testing.T) { 13 | 14 | resourceName := "mikrotik_bridge_vlan.testacc" 15 | 16 | createdBridgeVlan := client.BridgeVlan{} 17 | resource.Test(t, resource.TestCase{ 18 | PreCheck: func() { testAccPreCheck(t) }, 19 | ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, 20 | CheckDestroy: testAccCheckBridgeVlanDestroy, 21 | Steps: []resource.TestStep{ 22 | { 23 | Config: ` 24 | resource "mikrotik_bridge" "default" { 25 | name = "test_bridge" 26 | } 27 | 28 | resource "mikrotik_bridge_vlan" "testacc" { 29 | bridge = mikrotik_bridge.default.name 30 | vlan_ids = [10, 15, 18] 31 | untagged = ["*0"] 32 | } 33 | `, 34 | Check: resource.ComposeAggregateTestCheckFunc( 35 | testAccBridgeVlanExists(resourceName, &createdBridgeVlan), 36 | resource.TestCheckResourceAttrSet(resourceName, "id"), 37 | resource.TestCheckResourceAttr(resourceName, "bridge", "test_bridge"), 38 | resource.TestCheckResourceAttr(resourceName, "untagged.#", "1"), 39 | ), 40 | }, 41 | { 42 | Config: ` 43 | resource "mikrotik_bridge" "default" { 44 | name = "test_bridge" 45 | } 46 | 47 | resource "mikrotik_bridge_vlan" "testacc" { 48 | bridge = mikrotik_bridge.default.name 49 | vlan_ids = [10, 15, 18] 50 | untagged = [] 51 | } 52 | `, 53 | Check: resource.ComposeAggregateTestCheckFunc( 54 | testAccBridgeVlanExists(resourceName, &createdBridgeVlan), 55 | resource.TestCheckResourceAttrSet(resourceName, "id"), 56 | resource.TestCheckResourceAttr(resourceName, "bridge", "test_bridge"), 57 | resource.TestCheckResourceAttr(resourceName, "untagged.#", "0"), 58 | ), 59 | }, 60 | }, 61 | }) 62 | } 63 | 64 | func testAccBridgeVlanExists(resourceID string, record *client.BridgeVlan) resource.TestCheckFunc { 65 | return func(s *terraform.State) error { 66 | r, ok := s.RootModule().Resources[resourceID] 67 | if !ok { 68 | return fmt.Errorf("resource %q not found in state", resourceID) 69 | } 70 | if r.Primary.ID == "" { 71 | return fmt.Errorf("resource %q has empty primary ID in state", resourceID) 72 | } 73 | c := client.NewClient(client.GetConfigFromEnv()) 74 | remoteRecord, err := c.FindBridgeVlan(r.Primary.ID) 75 | if err != nil { 76 | return err 77 | } 78 | *record = *remoteRecord 79 | 80 | return nil 81 | } 82 | } 83 | 84 | func testAccCheckBridgeVlanDestroy(s *terraform.State) error { 85 | c := client.NewClient(client.GetConfigFromEnv()) 86 | for _, rs := range s.RootModule().Resources { 87 | if rs.Type != "mikrotik_bridge_vlan" { 88 | continue 89 | } 90 | 91 | remoteRecord, err := c.FindBridgeVlan(rs.Primary.ID) 92 | if err != nil && !client.IsNotFoundError(err) { 93 | return fmt.Errorf("expected not found error, got %+#v", err) 94 | } 95 | 96 | if remoteRecord != nil { 97 | return fmt.Errorf("bridge vlan %q still exists in remote system", remoteRecord.Id) 98 | } 99 | } 100 | 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /mikrotik/resource_dhcp_server_test.go: -------------------------------------------------------------------------------- 1 | package mikrotik 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/ddelnano/terraform-provider-mikrotik/client" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 10 | ) 11 | 12 | func TestAccDhcpServer_basic(t *testing.T) { 13 | rName := "dhcp-server" 14 | rLeaseScript := ":put 123" 15 | dhcpServer := client.DhcpServer{} 16 | resource.Test(t, resource.TestCase{ 17 | PreCheck: func() { testAccPreCheck(t) }, 18 | ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, 19 | CheckDestroy: testAccCheckDhcpServerDestroy, 20 | Steps: []resource.TestStep{ 21 | { 22 | Config: testAccDhcpServerConfig(rName, true, rLeaseScript), 23 | Check: resource.ComposeAggregateTestCheckFunc( 24 | testAccDhcpServerResourceExists("mikrotik_dhcp_server.testacc", &dhcpServer), 25 | resource.TestCheckResourceAttr("mikrotik_dhcp_server.testacc", "name", rName), 26 | resource.TestCheckResourceAttr("mikrotik_dhcp_server.testacc", "disabled", "true"), 27 | resource.TestCheckResourceAttr("mikrotik_dhcp_server.testacc", "lease_script", rLeaseScript), 28 | ), 29 | }, 30 | { 31 | Config: testAccDhcpServerConfig(rName, false, ":put updated"), 32 | Check: resource.ComposeAggregateTestCheckFunc( 33 | testAccDhcpServerResourceExists("mikrotik_dhcp_server.testacc", &dhcpServer), 34 | resource.TestCheckResourceAttr("mikrotik_dhcp_server.testacc", "name", rName), 35 | resource.TestCheckResourceAttr("mikrotik_dhcp_server.testacc", "disabled", "false"), 36 | resource.TestCheckResourceAttr("mikrotik_dhcp_server.testacc", "lease_script", ":put updated"), 37 | ), 38 | }, 39 | }, 40 | }) 41 | } 42 | 43 | func testAccDhcpServerResourceExists(resource string, record *client.DhcpServer) resource.TestCheckFunc { 44 | return func(s *terraform.State) error { 45 | 46 | r, ok := s.RootModule().Resources[resource] 47 | if !ok { 48 | return fmt.Errorf("resource %q not found in state", resource) 49 | } 50 | if r.Primary.ID == "" { 51 | return fmt.Errorf("resource %q has empty primary ID in state", resource) 52 | } 53 | c := client.NewClient(client.GetConfigFromEnv()) 54 | dhcpServer, err := c.FindDhcpServer(r.Primary.Attributes["name"]) 55 | if err != nil { 56 | return err 57 | } 58 | *record = *dhcpServer 59 | 60 | return nil 61 | } 62 | } 63 | 64 | func testAccCheckDhcpServerDestroy(s *terraform.State) error { 65 | c := client.NewClient(client.GetConfigFromEnv()) 66 | for _, rs := range s.RootModule().Resources { 67 | if rs.Type != "mikrotik_dhcp_server" { 68 | continue 69 | } 70 | 71 | dhcpServer, err := c.FindDhcpServer(rs.Primary.Attributes["name"]) 72 | if err != nil && !client.IsNotFoundError(err) { 73 | return fmt.Errorf("expected not found error, got %+#v", err) 74 | } 75 | 76 | if dhcpServer != nil { 77 | return fmt.Errorf("dhcp-server %q (%s) still exists in remote system", dhcpServer.Name, dhcpServer.Name) 78 | } 79 | } 80 | 81 | return nil 82 | } 83 | 84 | func testAccDhcpServerConfig(name string, disabled bool, leaseScript string) string { 85 | return fmt.Sprintf(` 86 | resource "mikrotik_dhcp_server" "testacc" { 87 | name = %q 88 | disabled = %t 89 | lease_script = %q 90 | } 91 | `, name, disabled, leaseScript) 92 | } 93 | -------------------------------------------------------------------------------- /mikrotik/resource_firewall_filter_rule_test.go: -------------------------------------------------------------------------------- 1 | package mikrotik 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/ddelnano/terraform-provider-mikrotik/client" 8 | "github.com/ddelnano/terraform-provider-mikrotik/mikrotik/internal" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 11 | ) 12 | 13 | var terraformResourceTypeFirewallFilterRule string = "mikrotik_firewall_filter_rule" 14 | 15 | func TestFirewallFilterRule_basic(t *testing.T) { 16 | 17 | resourceName := terraformResourceTypeFirewallFilterRule + ".testacc" 18 | 19 | action := "accept" 20 | chain := "testChain" 21 | connectionState := []string{"new"} 22 | resource.Test(t, resource.TestCase{ 23 | PreCheck: func() { testAccPreCheck(t) }, 24 | ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, 25 | CheckDestroy: testAccCheckFirewallFilterRuleDestroy, 26 | Steps: []resource.TestStep{ 27 | { 28 | Config: testAccFirewallFilterRuleConfigBasic(action, chain, connectionState, "80", "tcp"), 29 | Check: resource.ComposeAggregateTestCheckFunc( 30 | resource.TestCheckResourceAttrSet(resourceName, "id"), 31 | resource.TestCheckResourceAttr(resourceName, "action", action), 32 | resource.TestCheckResourceAttr(resourceName, "chain", chain), 33 | resource.TestCheckResourceAttr(resourceName, "dst_port", "80"), 34 | resource.TestCheckResourceAttr(resourceName, "protocol", "tcp"), 35 | ), 36 | }, 37 | { 38 | Config: testAccFirewallFilterRuleConfigBasic(action, chain, connectionState, "68", "udp"), 39 | Check: resource.ComposeAggregateTestCheckFunc( 40 | resource.TestCheckResourceAttrSet(resourceName, "id"), 41 | resource.TestCheckResourceAttr(resourceName, "action", action), 42 | resource.TestCheckResourceAttr(resourceName, "chain", chain), 43 | resource.TestCheckResourceAttr(resourceName, "dst_port", "68"), 44 | resource.TestCheckResourceAttr(resourceName, "protocol", "udp"), 45 | ), 46 | }, 47 | }, 48 | }) 49 | } 50 | 51 | func testAccCheckFirewallFilterRuleDestroy(s *terraform.State) error { 52 | c := client.NewClient(client.GetConfigFromEnv()) 53 | for _, rs := range s.RootModule().Resources { 54 | if rs.Type != terraformResourceTypeFirewallFilterRule { 55 | continue 56 | } 57 | 58 | remoteRecord, err := c.FindFirewallFilterRule(rs.Primary.ID) 59 | if err != nil && !client.IsNotFoundError(err) { 60 | return fmt.Errorf("expected not found error, got %+#v", err) 61 | } 62 | 63 | if remoteRecord != nil { 64 | return fmt.Errorf("resource %T with id %q still exists in remote system", remoteRecord, remoteRecord.Id) 65 | } 66 | } 67 | return nil 68 | } 69 | 70 | func testAccFirewallFilterRuleConfigBasic(action, chain string, connectionState []string, destPort, protocol string) string { 71 | return fmt.Sprintf(` 72 | resource "mikrotik_firewall_filter_rule" "testacc" { 73 | action = %q 74 | chain = %q 75 | connection_state = [%s] 76 | dst_port = %q 77 | protocol = %q 78 | } 79 | `, action, chain, internal.JoinStringsToString(connectionState, ","), destPort, protocol) 80 | } 81 | -------------------------------------------------------------------------------- /mikrotik/resource_interface_list_member_test.go: -------------------------------------------------------------------------------- 1 | package mikrotik 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/ddelnano/terraform-provider-mikrotik/client" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 10 | ) 11 | 12 | func TestInterfaceListMember_basic(t *testing.T) { 13 | resourceName := "mikrotik_interface_list_member.list_member" 14 | 15 | listName1 := "interface_list1" 16 | listName2 := "interface_list2" 17 | resource.Test(t, resource.TestCase{ 18 | PreCheck: func() { testAccPreCheck(t) }, 19 | ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, 20 | CheckDestroy: testAccCheckInterfaceListMemberDestroy, 21 | Steps: []resource.TestStep{ 22 | { 23 | Config: testAccInterfaceListMember(listName1, listName2, "list1", "*0"), 24 | Check: resource.ComposeAggregateTestCheckFunc( 25 | testAccInterfaceListMemberExists(resourceName), 26 | resource.TestCheckResourceAttr(resourceName, "list", listName1), 27 | resource.TestCheckResourceAttr(resourceName, "interface", "*0"), 28 | ), 29 | }, 30 | { 31 | Config: testAccInterfaceListMember(listName1, listName2, "list2", "*0"), 32 | Check: resource.ComposeAggregateTestCheckFunc( 33 | testAccInterfaceListMemberExists(resourceName), 34 | resource.TestCheckResourceAttr(resourceName, "list", listName2), 35 | resource.TestCheckResourceAttr(resourceName, "interface", "*0"), 36 | ), 37 | }, 38 | }, 39 | }) 40 | } 41 | 42 | func testAccInterfaceListMemberExists(resourceName string) resource.TestCheckFunc { 43 | return func(s *terraform.State) error { 44 | rs, ok := s.RootModule().Resources[resourceName] 45 | if !ok { 46 | return fmt.Errorf("Not found: %s", resourceName) 47 | } 48 | 49 | if rs.Primary.ID == "" { 50 | return fmt.Errorf("%s does not exist in the statefile", resourceName) 51 | } 52 | 53 | c := client.NewClient(client.GetConfigFromEnv()) 54 | record, err := c.FindInterfaceListMember(rs.Primary.ID) 55 | if err != nil { 56 | return fmt.Errorf("Unable to get remote record for %s: %v", resourceName, err) 57 | } 58 | 59 | if record == nil { 60 | return fmt.Errorf("Unable to get the remote record %s", resourceName) 61 | } 62 | 63 | return nil 64 | } 65 | } 66 | 67 | func testAccCheckInterfaceListMemberDestroy(s *terraform.State) error { 68 | c := client.NewClient(client.GetConfigFromEnv()) 69 | for _, rs := range s.RootModule().Resources { 70 | if rs.Type != "mikrotik_interface_list_member" { 71 | continue 72 | } 73 | 74 | remoteRecord, err := c.FindInterfaceListMember(rs.Primary.ID) 75 | if !client.IsNotFoundError(err) && err != nil { 76 | return err 77 | } 78 | 79 | if remoteRecord != nil { 80 | return fmt.Errorf("remote record (%s) still exists", remoteRecord.Id) 81 | } 82 | } 83 | 84 | return nil 85 | } 86 | 87 | func testAccInterfaceListMember(listName1, listName2, listToUse string, iface string) string { 88 | return fmt.Sprintf(` 89 | resource mikrotik_interface_list "list1" { 90 | name = %q 91 | } 92 | 93 | resource mikrotik_interface_list "list2" { 94 | name = %q 95 | } 96 | 97 | resource mikrotik_interface_list_member "list_member" { 98 | interface = %q 99 | list = mikrotik_interface_list.%s.name 100 | } 101 | `, listName1, listName2, iface, listToUse) 102 | } 103 | -------------------------------------------------------------------------------- /mikrotik/resource_interface_list_test.go: -------------------------------------------------------------------------------- 1 | package mikrotik 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/ddelnano/terraform-provider-mikrotik/client" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 10 | ) 11 | 12 | func TestInterfaceList_basic(t *testing.T) { 13 | resourceName := "mikrotik_interface_list.testacc" 14 | listName := "custom_list" 15 | resource.Test(t, resource.TestCase{ 16 | PreCheck: func() { testAccPreCheck(t) }, 17 | ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, 18 | CheckDestroy: testAccCheckInterfaceListDestroy, 19 | Steps: []resource.TestStep{ 20 | { 21 | Config: testAccInterfaceList(listName, "Initial record"), 22 | Check: resource.ComposeAggregateTestCheckFunc( 23 | testAccInterfaceListExists(resourceName), 24 | resource.TestCheckResourceAttr(resourceName, "name", listName), 25 | ), 26 | }, 27 | { 28 | Config: testAccInterfaceList(listName+"_updated", "updated record"), 29 | Check: resource.ComposeAggregateTestCheckFunc( 30 | testAccInterfaceListExists(resourceName), 31 | resource.TestCheckResourceAttr(resourceName, "name", listName+"_updated"), 32 | resource.TestCheckResourceAttr(resourceName, "comment", "updated record"), 33 | ), 34 | }, 35 | }, 36 | }) 37 | } 38 | 39 | func testAccCheckInterfaceListDestroy(s *terraform.State) error { 40 | c := client.NewClient(client.GetConfigFromEnv()) 41 | for _, rs := range s.RootModule().Resources { 42 | if rs.Type != "mikrotik_interface_list" { 43 | continue 44 | } 45 | 46 | remoteRecord, err := c.FindInterfaceList(rs.Primary.Attributes["name"]) 47 | if !client.IsNotFoundError(err) && err != nil { 48 | return err 49 | } 50 | 51 | if remoteRecord != nil { 52 | return fmt.Errorf("remote record (%s) still exists", remoteRecord.Name) 53 | } 54 | } 55 | 56 | return nil 57 | } 58 | 59 | func testAccInterfaceListExists(resourceName string) resource.TestCheckFunc { 60 | return func(s *terraform.State) error { 61 | rs, ok := s.RootModule().Resources[resourceName] 62 | if !ok { 63 | return fmt.Errorf("Not found: %s", resourceName) 64 | } 65 | 66 | if rs.Primary.ID == "" { 67 | return fmt.Errorf("%s does not exist in the statefile", resourceName) 68 | } 69 | 70 | c := client.NewClient(client.GetConfigFromEnv()) 71 | record, err := c.FindInterfaceList(rs.Primary.Attributes["name"]) 72 | if err != nil { 73 | return fmt.Errorf("Unable to get remote record for %s: %v", resourceName, err) 74 | } 75 | 76 | if record == nil { 77 | return fmt.Errorf("Unable to get the remote record %s", resourceName) 78 | } 79 | 80 | return nil 81 | } 82 | } 83 | 84 | func testAccInterfaceList(name, comment string) string { 85 | return fmt.Sprintf(` 86 | resource "mikrotik_interface_list" "testacc" { 87 | name = %q 88 | comment = %q 89 | } 90 | `, name, comment) 91 | } 92 | -------------------------------------------------------------------------------- /mikrotik/resource_wireless_interface_test.go: -------------------------------------------------------------------------------- 1 | package mikrotik 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/ddelnano/terraform-provider-mikrotik/client" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 10 | ) 11 | 12 | func TestWirelessInterface_basic(t *testing.T) { 13 | // This test is skipped, until we find a way to include required packages. 14 | // 15 | // Since RouterOS 7.13, 'wireless' package is separate from the main system package 16 | // and there is no easy way to install it in Docker during tests. 17 | // see https://help.mikrotik.com/docs/spaces/ROS/pages/40992872/Packages#Packages-RouterOSpackages 18 | client.SkipIfRouterOSV7OrLater(t, sysResources) 19 | 20 | resourceName := "mikrotik_wireless_interface.testacc" 21 | name := acctest.RandomWithPrefix("ssid") 22 | resource.Test(t, 23 | resource.TestCase{ 24 | ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, 25 | Steps: []resource.TestStep{ 26 | { 27 | Config: fmt.Sprintf(` 28 | resource "mikrotik_wireless_interface" "testacc" { 29 | name = %q 30 | mode = %q 31 | ssid = %q 32 | vlan_id = 2 33 | hide_ssid = false 34 | master_interface = "*0" 35 | }`, name, client.WirelessInterfaceModeAPBridge, name+"-ssid"), 36 | 37 | Check: resource.ComposeAggregateTestCheckFunc( 38 | resource.TestCheckResourceAttrSet(resourceName, "id"), 39 | resource.TestCheckResourceAttr(resourceName, "name", name), 40 | resource.TestCheckResourceAttr(resourceName, "disabled", "true"), 41 | resource.TestCheckResourceAttr(resourceName, "mode", client.WirelessInterfaceModeAPBridge), 42 | resource.TestCheckResourceAttr(resourceName, "ssid", name+"-ssid"), 43 | resource.TestCheckResourceAttr(resourceName, "hide_ssid", "false"), 44 | resource.TestCheckResourceAttr(resourceName, "vlan_id", "2"), 45 | ), 46 | }, 47 | { 48 | Config: fmt.Sprintf(` 49 | resource mikrotik_wireless_interface testacc { 50 | name = %q 51 | mode = %q 52 | disabled = false 53 | ssid = %q 54 | hide_ssid = true 55 | master_interface = "*0" 56 | }`, name, client.WirelessInterfaceModeAPBridge, name+"-ssid"), 57 | 58 | Check: resource.ComposeAggregateTestCheckFunc( 59 | resource.TestCheckResourceAttrSet(resourceName, "id"), 60 | resource.TestCheckResourceAttr(resourceName, "name", name), 61 | resource.TestCheckResourceAttr(resourceName, "disabled", "false"), 62 | resource.TestCheckResourceAttr(resourceName, "mode", client.WirelessInterfaceModeAPBridge), 63 | resource.TestCheckResourceAttr(resourceName, "ssid", name+"-ssid"), 64 | resource.TestCheckResourceAttr(resourceName, "hide_ssid", "true"), 65 | ), 66 | }, 67 | { 68 | ImportState: true, 69 | ImportStateVerify: true, 70 | ResourceName: resourceName, 71 | }, 72 | }, 73 | }, 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /templates/index.md.tmpl: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: "Provider: Mikrotik" 3 | description: |- 4 | The mikrotik provider is used to interact with the resources supported by RouterOS. 5 | --- 6 | 7 | # {{ .ProviderShortName | upper }} Provider 8 | 9 | The mikrotik provider is used to interact with the resources supported by RouterOS. 10 | The provider needs to be configured with the proper credentials before it can be used. 11 | 12 | ## Requirements 13 | 14 | * RouterOS v6.45.2+ (It may work with other versions but it is untested against other versions!) 15 | 16 | 17 | {{ if .HasExample -}} 18 | ## Example Usage 19 | {{ tffile .ExampleFile }} 20 | {{- end }} 21 | 22 | {{ .SchemaMarkdown | trimspace }} 23 | -------------------------------------------------------------------------------- /templates/resources.md.tmpl: -------------------------------------------------------------------------------- 1 | # {{.Name}} ({{.Type}}) 2 | {{ .Description | trimspace }} 3 | 4 | {{ if .HasExample -}} 5 | ## Example Usage 6 | {{ tffile .ExampleFile }} 7 | {{- end }} 8 | 9 | {{ .SchemaMarkdown | trimspace }} 10 | 11 | {{ if .HasImport -}} 12 | ## Import 13 | Import is supported using the following syntax: 14 | {{ codefile "shell" .ImportFile }} 15 | {{- end }} 16 | -------------------------------------------------------------------------------- /templates/resources/bgp_instance.md.tmpl: -------------------------------------------------------------------------------- 1 | # {{.Name}} ({{.Type}}) 2 | {{ .Description | trimspace }} 3 | 4 | !> This resource will not be supported in RouterOS v7+. 5 | Mikrotik has deprecated the underlying commands so future BGP support will need new resources created 6 | (See [this issue](https://github.com/ddelnano/terraform-provider-mikrotik/issues/52) for status of this work). 7 | 8 | {{ if .HasExample -}} 9 | ## Example Usage 10 | {{ tffile .ExampleFile }} 11 | {{- end }} 12 | 13 | {{ .SchemaMarkdown | trimspace }} 14 | 15 | {{ if .HasImport -}} 16 | ## Import 17 | Import is supported using the following syntax: 18 | {{ codefile "shell" .ImportFile }} 19 | {{- end }} 20 | -------------------------------------------------------------------------------- /templates/resources/bgp_peer.md.tmpl: -------------------------------------------------------------------------------- 1 | # {{.Name}} ({{.Type}}) 2 | {{ .Description | trimspace }} 3 | 4 | !> This resource will not be supported in RouterOS v7+. 5 | Mikrotik has deprecated the underlying commands so future BGP support will need new resources created 6 | (See [this issue](https://github.com/ddelnano/terraform-provider-mikrotik/issues/52) for status of this work). 7 | 8 | {{ if .HasExample -}} 9 | ## Example Usage 10 | {{ tffile .ExampleFile }} 11 | {{- end }} 12 | 13 | {{ .SchemaMarkdown | trimspace }} 14 | 15 | {{ if .HasImport -}} 16 | ## Import 17 | Import is supported using the following syntax: 18 | {{ codefile "shell" .ImportFile }} 19 | {{- end }} 20 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package tools 5 | 6 | import ( 7 | _ "github.com/ddelnano/terraform-provider-mikrotik/cmd/mikrotik-codegen/internal/codegen" 8 | _ "github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs" 9 | ) 10 | --------------------------------------------------------------------------------