sync with v0.67.0

This commit is contained in:
Tomas Kloda
2026-03-24 16:34:41 +01:00
parent 7a7f458b63
commit 0cb8dffb5a
15 changed files with 1091 additions and 42 deletions
+88 -2
View File
@@ -20,8 +20,10 @@ This collection provides comprehensive management of NetBird resources:
- **Routes** - Manage legacy routes (deprecated, use Networks instead)
- **DNS** - Configure nameserver groups and DNS settings
- **Posture Checks** - Define security compliance requirements
- **Accounts** - Manage account-wide settings
- **Accounts** - Manage account-wide settings (including extra settings, auto-update, peer expose)
- **Tokens** - Create and manage personal access tokens
- **Identity Providers** - Configure identity providers (Google, Okta, Entra, OIDC, etc.)
- **Invites** - Manage user invite links with expiration and regeneration
- **Info** - Gather information about any resource
## Requirements
@@ -189,6 +191,8 @@ Manage NetBird access policies.
name: "developers-to-servers"
description: "Allow developers SSH access to servers"
enabled: true
source_posture_checks:
- "posture-check-id"
rules:
- name: "ssh-access"
sources:
@@ -200,6 +204,16 @@ Manage NetBird access policies.
ports:
- "22"
action: "accept"
- name: "web-access"
sources:
- "developers-group-id"
destinations:
- "servers-group-id"
protocol: "tcp"
port_ranges:
- start: 8000
end: 9000
action: "accept"
state: present
```
@@ -383,6 +397,11 @@ Manage NetBird account settings.
jwt_groups_enabled: true
jwt_groups_claim_name: "groups"
dns_domain: "netbird.example.com"
auto_update_always: true
auto_update_version: "latest"
peer_expose_enabled: true
extra_peer_approval_enabled: false
extra_network_traffic_logs_enabled: true
state: present
```
@@ -407,6 +426,72 @@ Manage NetBird personal access tokens.
when: new_token.changed
```
### netbird_idp
Manage NetBird identity providers.
```yaml
# Create an OIDC identity provider
- name: Configure identity provider
community.ansible_netbird.netbird_idp:
api_url: "{{ netbird_api_url }}"
api_token: "{{ netbird_api_token }}"
name: "corporate-sso"
type: "oidc"
issuer: "https://auth.example.com"
client_id: "your-client-id"
client_secret: "your-client-secret"
state: present
# Create a Google identity provider
- name: Configure Google IDP
community.ansible_netbird.netbird_idp:
api_url: "{{ netbird_api_url }}"
api_token: "{{ netbird_api_token }}"
name: "google-workspace"
type: "google"
issuer: "https://accounts.google.com"
client_id: "your-google-client-id"
client_secret: "your-google-client-secret"
state: present
```
### netbird_invite
Manage NetBird user invites.
```yaml
# Create a user invite
- name: Invite a new user
community.ansible_netbird.netbird_invite:
api_url: "{{ netbird_api_url }}"
api_token: "{{ netbird_api_token }}"
email: "newuser@example.com"
name: "New User"
role: "user"
auto_groups:
- "developers-group-id"
expires_in: 604800 # 7 days
state: present
# Regenerate an expired invite
- name: Regenerate invite link
community.ansible_netbird.netbird_invite:
api_url: "{{ netbird_api_url }}"
api_token: "{{ netbird_api_token }}"
email: "newuser@example.com"
regenerate: true
state: present
# Delete an invite
- name: Remove invite
community.ansible_netbird.netbird_invite:
api_url: "{{ netbird_api_url }}"
api_token: "{{ netbird_api_token }}"
invite_id: "invite-id-123"
state: absent
```
### netbird_info
Gather information about NetBird resources.
@@ -434,7 +519,7 @@ Gather information about NetBird resources.
register: me
```
Available resources: `accounts`, `users`, `peers`, `groups`, `setup_keys`, `policies`, `networks`, `routes`, `dns_nameservers`, `dns_settings`, `posture_checks`, `events`, `countries`, `current_user`
Available resources: `accounts`, `users`, `peers`, `groups`, `setup_keys`, `policies`, `networks`, `routes`, `dns_nameservers`, `dns_settings`, `posture_checks`, `events`, `countries`, `current_user`, `identity_providers`, `invites`
## Role Usage
@@ -516,6 +601,7 @@ This collection implements the [NetBird REST API](https://docs.netbird.io/api).
- [Networks](https://docs.netbird.io/api/resources/networks)
- [DNS](https://docs.netbird.io/api/resources/dns)
- [Posture Checks](https://docs.netbird.io/api/resources/posture-checks)
- [Identity Providers](https://docs.netbird.io/api/resources/identity-providers)
- [Events](https://docs.netbird.io/api/resources/events)
## Contributing
+22
View File
@@ -158,6 +158,28 @@ netbird_posture_checks: []
# min_version: "0.25.0"
# state: present
# Identity provider management
netbird_identity_providers: []
# Example:
# netbird_identity_providers:
# - name: "google-idp"
# type: "google" # entra, google, microsoft, oidc, okta, pocketid, zitadel
# issuer: "https://accounts.google.com"
# client_id: "your-client-id"
# client_secret: "your-client-secret"
# state: present
# User invite management
netbird_invites: []
# Example:
# netbird_invites:
# - email: "newuser@example.com"
# name: "New User"
# role: "user" # admin, user
# auto_groups: []
# expires_in: 604800 # 7 days in seconds
# state: present
# Account settings
netbird_account_settings: {}
# Example:
+99 -12
View File
@@ -244,8 +244,8 @@ class NetBirdAPI:
"""Get a specific peer."""
return self.get(f'/api/peers/{peer_id}')
def update_peer(self, peer_id, name=None, ssh_enabled=None, login_expiration_enabled=None,
inactivity_expiration_enabled=None, approval_required=None):
def update_peer(self, peer_id, name=None, ssh_enabled=None, login_expiration_enabled=None,
inactivity_expiration_enabled=None, approval_required=None, ip=None):
"""Update a peer."""
data = {}
if name is not None:
@@ -258,6 +258,8 @@ class NetBirdAPI:
data['inactivity_expiration_enabled'] = inactivity_expiration_enabled
if approval_required is not None:
data['approval_required'] = approval_required
if ip is not None:
data['ip'] = ip
return self.put(f'/api/peers/{peer_id}', data=data)
def delete_peer(self, peer_id):
@@ -288,11 +290,9 @@ class NetBirdAPI:
}
return self.post('/api/setup-keys', data=data)
def update_setup_key(self, key_id, name=None, revoked=None, auto_groups=None):
"""Update a setup key."""
def update_setup_key(self, key_id, revoked=None, auto_groups=None):
"""Update a setup key. Only revoked and auto_groups can be changed after creation."""
data = {}
if name is not None:
data['name'] = name
if revoked is not None:
data['revoked'] = revoked
if auto_groups is not None:
@@ -346,7 +346,7 @@ class NetBirdAPI:
"""Get a specific policy."""
return self.get(f'/api/policies/{policy_id}')
def create_policy(self, name, enabled=True, description='', rules=None):
def create_policy(self, name, enabled=True, description='', rules=None, source_posture_checks=None):
"""Create a new policy."""
data = {
'name': name,
@@ -354,9 +354,12 @@ class NetBirdAPI:
'description': description,
'rules': rules or []
}
if source_posture_checks is not None:
data['source_posture_checks'] = source_posture_checks
return self.post('/api/policies', data=data)
def update_policy(self, policy_id, name=None, enabled=None, description=None, rules=None):
def update_policy(self, policy_id, name=None, enabled=None, description=None, rules=None,
source_posture_checks=None):
"""Update a policy."""
data = {}
if name is not None:
@@ -367,6 +370,8 @@ class NetBirdAPI:
data['description'] = description
if rules is not None:
data['rules'] = rules
if source_posture_checks is not None:
data['source_posture_checks'] = source_posture_checks
return self.put(f'/api/policies/{policy_id}', data=data)
def delete_policy(self, policy_id):
@@ -412,11 +417,13 @@ class NetBirdAPI:
"""Get a specific network router."""
return self.get(f'/api/networks/{network_id}/routers/{router_id}')
def create_network_router(self, network_id, peer_id=None, peer_groups=None, metric=9999, masquerade=False):
def create_network_router(self, network_id, peer_id=None, peer_groups=None, metric=9999,
masquerade=False, enabled=True):
"""Create a new network router."""
data = {
'metric': metric,
'masquerade': masquerade
'masquerade': masquerade,
'enabled': enabled
}
if peer_id:
data['peer'] = peer_id
@@ -424,8 +431,8 @@ class NetBirdAPI:
data['peer_groups'] = peer_groups
return self.post(f'/api/networks/{network_id}/routers', data=data)
def update_network_router(self, network_id, router_id, peer_id=None, peer_groups=None,
metric=None, masquerade=None):
def update_network_router(self, network_id, router_id, peer_id=None, peer_groups=None,
metric=None, masquerade=None, enabled=None):
"""Update a network router."""
data = {}
if peer_id is not None:
@@ -436,6 +443,8 @@ class NetBirdAPI:
data['metric'] = metric
if masquerade is not None:
data['masquerade'] = masquerade
if enabled is not None:
data['enabled'] = enabled
return self.put(f'/api/networks/{network_id}/routers/{router_id}', data=data)
def delete_network_router(self, network_id, router_id):
@@ -647,6 +656,84 @@ class NetBirdAPI:
"""List all events."""
return self.get('/api/events')
# Identity Provider operations
def list_identity_providers(self):
"""List all identity providers."""
return self.get('/api/identity-providers')
def get_identity_provider(self, idp_id):
"""Get a specific identity provider."""
return self.get(f'/api/identity-providers/{idp_id}')
def create_identity_provider(self, name, idp_type, issuer, client_id, client_secret):
"""Create a new identity provider."""
data = {
'name': name,
'type': idp_type,
'issuer': issuer,
'client_id': client_id,
'client_secret': client_secret
}
return self.post('/api/identity-providers', data=data)
def update_identity_provider(self, idp_id, name=None, idp_type=None, issuer=None,
client_id=None, client_secret=None):
"""Update an identity provider."""
data = {}
if name is not None:
data['name'] = name
if idp_type is not None:
data['type'] = idp_type
if issuer is not None:
data['issuer'] = issuer
if client_id is not None:
data['client_id'] = client_id
if client_secret is not None:
data['client_secret'] = client_secret
return self.put(f'/api/identity-providers/{idp_id}', data=data)
def delete_identity_provider(self, idp_id):
"""Delete an identity provider."""
return self.delete(f'/api/identity-providers/{idp_id}')
# User Invite operations
def list_user_invites(self):
"""List all user invites."""
return self.get('/api/users/invites')
def create_user_invite(self, email, name=None, role=None, auto_groups=None, expires_in=None):
"""Create a new user invite."""
data = {'email': email}
if name is not None:
data['name'] = name
if role is not None:
data['role'] = role
if auto_groups is not None:
data['auto_groups'] = auto_groups
if expires_in is not None:
data['expires_in'] = expires_in
return self.post('/api/users/invites', data=data)
def delete_user_invite(self, invite_id):
"""Delete a user invite."""
return self.delete(f'/api/users/invites/{invite_id}')
def regenerate_user_invite(self, invite_id, expires_in=None):
"""Regenerate a user invite token."""
data = {}
if expires_in is not None:
data['expires_in'] = expires_in
return self.post(f'/api/users/invites/{invite_id}/regenerate', data=data)
# User approval operations
def approve_user(self, user_id):
"""Approve a pending user."""
return self.post(f'/api/users/{user_id}/approve')
def reject_user(self, user_id):
"""Reject a pending user."""
return self.delete(f'/api/users/{user_id}/reject')
# Geo-location operations
def list_countries(self):
"""List all countries."""
+89 -3
View File
@@ -83,6 +83,53 @@ options:
description:
- Enable or disable experimental lazy connection.
type: bool
extra_peer_approval_enabled:
description:
- Enable or disable peer approval globally.
- When enabled, all peers added will be in pending state until approved by an admin.
type: bool
extra_user_approval_required:
description:
- Enable manual approval for new users joining via domain matching.
- When enabled, users are blocked with pending approval status until approved by an admin.
type: bool
extra_network_traffic_logs_enabled:
description:
- Enable or disable network traffic logging.
- When enabled, all network traffic events from peers will be stored.
type: bool
extra_network_traffic_logs_groups:
description:
- Limits traffic logging to these groups.
- If empty, all peers are enabled.
type: list
elements: str
extra_network_traffic_packet_counter_enabled:
description:
- Enable or disable network traffic packet counter.
- When enabled, network packets and their size will be counted and reported.
type: bool
auto_update_always:
description:
- When true, updates are installed automatically in the background.
- When false, updates require user interaction from the UI.
type: bool
auto_update_version:
description:
- Set clients auto-update version.
- Use "latest", "disabled", or a specific version (e.g., "0.50.1").
type: str
peer_expose_enabled:
description:
- Enable or disable peer expose.
- When enabled, peers can expose local services through the reverse proxy using the CLI.
type: bool
peer_expose_groups:
description:
- Limits which peer groups are allowed to expose services.
- If empty, all peers are allowed when peer expose is enabled.
type: list
elements: str
extends_documentation_fragment:
- community.ansible_netbird.netbird
requirements:
@@ -215,14 +262,44 @@ def build_settings_update(module):
value = module.params.get(param)
if value is not None:
settings[api_field] = value
# Build nested extra settings
extra = {}
if module.params.get('extra_peer_approval_enabled') is not None:
extra['peer_approval_enabled'] = module.params['extra_peer_approval_enabled']
if module.params.get('extra_user_approval_required') is not None:
extra['user_approval_required'] = module.params['extra_user_approval_required']
if module.params.get('extra_network_traffic_logs_enabled') is not None:
extra['network_traffic_logs_enabled'] = module.params['extra_network_traffic_logs_enabled']
if module.params.get('extra_network_traffic_logs_groups') is not None:
extra['network_traffic_logs_groups'] = module.params['extra_network_traffic_logs_groups']
if module.params.get('extra_network_traffic_packet_counter_enabled') is not None:
extra['network_traffic_packet_counter_enabled'] = module.params['extra_network_traffic_packet_counter_enabled']
if extra:
settings['extra'] = extra
if module.params.get('auto_update_always') is not None:
settings['auto_update_always'] = module.params['auto_update_always']
if module.params.get('auto_update_version') is not None:
settings['auto_update_version'] = module.params['auto_update_version']
if module.params.get('peer_expose_enabled') is not None:
settings['peer_expose_enabled'] = module.params['peer_expose_enabled']
if module.params.get('peer_expose_groups') is not None:
settings['peer_expose_groups'] = module.params['peer_expose_groups']
return settings
def settings_need_update(current_settings, desired_settings):
"""Check if account settings need to be updated."""
for key, value in desired_settings.items():
if current_settings.get(key) != value:
if key == 'extra':
# Compare nested extra settings
current_extra = current_settings.get('extra', {})
for extra_key, extra_value in value.items():
if current_extra.get(extra_key) != extra_value:
return True
elif current_settings.get(key) != value:
return True
return False
@@ -245,7 +322,16 @@ def run_module():
routing_peer_dns_resolution_enabled=dict(type='bool'),
dns_domain=dict(type='str'),
network_range=dict(type='str'),
lazy_connection_enabled=dict(type='bool')
lazy_connection_enabled=dict(type='bool'),
extra_peer_approval_enabled=dict(type='bool'),
extra_user_approval_required=dict(type='bool'),
extra_network_traffic_logs_enabled=dict(type='bool'),
extra_network_traffic_logs_groups=dict(type='list', elements='str'),
extra_network_traffic_packet_counter_enabled=dict(type='bool'),
auto_update_always=dict(type='bool'),
auto_update_version=dict(type='str'),
peer_expose_enabled=dict(type='bool'),
peer_expose_groups=dict(type='list', elements='str')
)
module = AnsibleModule(
+286
View File
@@ -0,0 +1,286 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2024, Community
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""Ansible module for managing NetBird identity providers."""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: netbird_idp
short_description: Manage NetBird identity providers
description:
- Create, update, and delete identity providers in NetBird.
- Identity providers are used to authenticate users via OIDC-compatible providers.
version_added: "1.0.0"
author:
- Community
options:
state:
description:
- The desired state of the identity provider.
type: str
choices: ['present', 'absent']
default: present
idp_id:
description:
- The unique identifier of the identity provider.
- Used for update or delete by ID.
type: str
name:
description:
- Name of the identity provider.
- Required when state is present.
- Used for lookup and creation.
type: str
type:
description:
- The type of identity provider.
- Required when creating a new identity provider.
type: str
choices: ['entra', 'google', 'microsoft', 'oidc', 'okta', 'pocketid', 'zitadel']
issuer:
description:
- The OIDC issuer URL for the identity provider.
- Required when creating a new identity provider.
type: str
client_id:
description:
- The OIDC client ID for the identity provider.
- Required when creating a new identity provider.
type: str
client_secret:
description:
- The OIDC client secret for the identity provider.
- Required when creating a new identity provider.
- This value is write-only and never returned by the API.
type: str
no_log: true
extends_documentation_fragment:
- community.ansible_netbird.netbird
requirements:
- python >= 3.6
'''
EXAMPLES = r'''
- name: Create an identity provider
community.ansible_netbird.netbird_idp:
api_url: "https://netbird.example.com"
api_token: "{{ netbird_token }}"
name: "corporate-okta"
type: "okta"
issuer: "https://dev-123456.okta.com"
client_id: "0oa1b2c3d4e5f6g7h8i9"
client_secret: "{{ okta_client_secret }}"
state: present
- name: Update an identity provider by name
community.ansible_netbird.netbird_idp:
api_url: "https://netbird.example.com"
api_token: "{{ netbird_token }}"
name: "corporate-okta"
type: "okta"
issuer: "https://dev-789012.okta.com"
client_id: "0oa1b2c3d4e5f6g7h8i9"
client_secret: "{{ okta_client_secret }}"
state: present
- name: Delete an identity provider by ID
community.ansible_netbird.netbird_idp:
api_url: "https://netbird.example.com"
api_token: "{{ netbird_token }}"
idp_id: "idp-id-123"
state: absent
- name: Delete an identity provider by name
community.ansible_netbird.netbird_idp:
api_url: "https://netbird.example.com"
api_token: "{{ netbird_token }}"
name: "corporate-okta"
state: absent
'''
RETURN = r'''
identity_provider:
description: The identity provider object.
returned: success
type: dict
contains:
id:
description: Identity provider ID.
type: str
name:
description: Identity provider name.
type: str
type:
description: Identity provider type.
type: str
issuer:
description: OIDC issuer URL.
type: str
client_id:
description: OIDC client ID.
type: str
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.ansible_netbird.plugins.module_utils.netbird_api import (
NetBirdAPI,
NetBirdAPIError,
netbird_argument_spec
)
def find_idp_by_name(api, name):
"""Find an identity provider by name."""
idps, _ = api.list_identity_providers()
for idp in idps:
if idp.get('name') == name:
return idp
return None
def idp_needs_update(current, desired):
"""Check if identity provider needs to be updated."""
if 'name' in desired and desired['name'] is not None:
if current.get('name') != desired['name']:
return True
if 'type' in desired and desired['type'] is not None:
if current.get('type') != desired['type']:
return True
if 'issuer' in desired and desired['issuer'] is not None:
if current.get('issuer') != desired['issuer']:
return True
if 'client_id' in desired and desired['client_id'] is not None:
if current.get('client_id') != desired['client_id']:
return True
# Skip client_secret comparison since it's write-only/never returned by the API
return False
def run_module():
"""Main module execution."""
argument_spec = netbird_argument_spec()
argument_spec.update(
state=dict(type='str', choices=['present', 'absent'], default='present'),
idp_id=dict(type='str'),
name=dict(type='str'),
type=dict(type='str', choices=['entra', 'google', 'microsoft', 'oidc', 'okta', 'pocketid', 'zitadel']),
issuer=dict(type='str'),
client_id=dict(type='str'),
client_secret=dict(type='str', no_log=True)
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
required_if=[
('state', 'present', ['name'], True),
],
required_one_of=[
('idp_id', 'name'),
]
)
api = NetBirdAPI(
module,
module.params['api_url'],
module.params['api_token'],
module.params['validate_certs']
)
state = module.params['state']
idp_id = module.params['idp_id']
name = module.params['name']
idp_type = module.params['type']
issuer = module.params['issuer']
client_id = module.params['client_id']
client_secret = module.params['client_secret']
result = dict(
changed=False,
identity_provider={}
)
try:
# Find existing identity provider
existing_idp = None
if idp_id:
try:
existing_idp, _ = api.get_identity_provider(idp_id)
except NetBirdAPIError as e:
if e.status_code != 404:
raise
elif name:
existing_idp = find_idp_by_name(api, name)
if state == 'absent':
if existing_idp:
if not module.check_mode:
api.delete_identity_provider(existing_idp['id'])
result['changed'] = True
result['msg'] = 'Identity provider deleted successfully'
module.exit_json(**result)
# state == 'present'
if existing_idp:
# Check if update is needed
desired = {
'name': name,
'type': idp_type,
'issuer': issuer,
'client_id': client_id
}
if idp_needs_update(existing_idp, desired):
if not module.check_mode:
idp, _ = api.update_identity_provider(
existing_idp['id'],
name=name,
idp_type=idp_type,
issuer=issuer,
client_id=client_id,
client_secret=client_secret
)
result['identity_provider'] = idp
else:
result['identity_provider'] = existing_idp
result['changed'] = True
else:
result['identity_provider'] = existing_idp
else:
# Create new identity provider
if not name:
module.fail_json(msg="name is required when creating a new identity provider")
if not module.check_mode:
idp, _ = api.create_identity_provider(
name=name,
idp_type=idp_type,
issuer=issuer,
client_id=client_id,
client_secret=client_secret
)
result['identity_provider'] = idp
result['changed'] = True
module.exit_json(**result)
except NetBirdAPIError as e:
module.fail_json(msg=str(e), status_code=e.status_code, response=e.response)
def main():
run_module()
if __name__ == '__main__':
main()
+11 -6
View File
@@ -23,9 +23,10 @@ options:
description:
- Type of resource to gather information about.
type: str
choices: ['accounts', 'users', 'peers', 'groups', 'setup_keys', 'policies',
'networks', 'routes', 'dns_nameservers', 'dns_settings',
'posture_checks', 'events', 'countries', 'current_user']
choices: ['accounts', 'users', 'peers', 'groups', 'setup_keys', 'policies',
'networks', 'routes', 'dns_nameservers', 'dns_settings',
'posture_checks', 'events', 'countries', 'current_user',
'identity_providers', 'invites']
required: true
service_user:
description:
@@ -133,10 +134,10 @@ def run_module():
resource=dict(
type='str',
required=True,
choices=['accounts', 'users', 'peers', 'groups', 'setup_keys',
'policies', 'networks', 'routes', 'dns_nameservers',
choices=['accounts', 'users', 'peers', 'groups', 'setup_keys',
'policies', 'networks', 'routes', 'dns_nameservers',
'dns_settings', 'posture_checks', 'events', 'countries',
'current_user']
'current_user', 'identity_providers', 'invites']
),
service_user=dict(type='bool'),
country_code=dict(type='str')
@@ -192,6 +193,10 @@ def run_module():
data, _ = api.list_events()
elif resource == 'countries':
data, _ = api.list_countries()
elif resource == 'identity_providers':
data, _ = api.list_identity_providers()
elif resource == 'invites':
data, _ = api.list_user_invites()
else:
module.fail_json(msg=f"Unknown resource type: {resource}")
+266
View File
@@ -0,0 +1,266 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2024, Community
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""Ansible module for managing NetBird user invites."""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: netbird_invite
short_description: Manage NetBird user invites
description:
- Create, delete, and regenerate user invites in NetBird.
- Invites are used to onboard new users to a NetBird account.
- Invites cannot be updated, only created, deleted, or regenerated.
version_added: "1.0.0"
author:
- Community
options:
state:
description:
- The desired state of the user invite.
type: str
choices: ['present', 'absent']
default: present
invite_id:
description:
- The unique identifier of the user invite.
- Can be used to delete a specific invite when state is absent.
type: str
email:
description:
- Email address for the invite.
- Required when state is present.
type: str
name:
description:
- Name of the invited user.
type: str
role:
description:
- Role to assign to the invited user.
type: str
choices: ['admin', 'user']
default: user
auto_groups:
description:
- List of group IDs to auto-assign to the invited user.
type: list
elements: str
default: []
expires_in:
description:
- Expiration time for the invite in seconds.
type: int
regenerate:
description:
- If true and an invite already exists for the email, regenerate the invite token.
type: bool
default: false
extends_documentation_fragment:
- community.ansible_netbird.netbird
requirements:
- python >= 3.6
'''
EXAMPLES = r'''
- name: Create a user invite
community.ansible_netbird.netbird_invite:
api_url: "https://netbird.example.com"
api_token: "{{ netbird_token }}"
email: "newuser@example.com"
name: "New User"
role: "user"
auto_groups:
- "group-id-1"
expires_in: 604800
state: present
- name: Create an admin invite
community.ansible_netbird.netbird_invite:
api_url: "https://netbird.example.com"
api_token: "{{ netbird_token }}"
email: "admin@example.com"
name: "Admin User"
role: "admin"
state: present
- name: Regenerate an existing invite
community.ansible_netbird.netbird_invite:
api_url: "https://netbird.example.com"
api_token: "{{ netbird_token }}"
email: "newuser@example.com"
regenerate: true
state: present
- name: Delete an invite by email
community.ansible_netbird.netbird_invite:
api_url: "https://netbird.example.com"
api_token: "{{ netbird_token }}"
email: "newuser@example.com"
state: absent
- name: Delete an invite by ID
community.ansible_netbird.netbird_invite:
api_url: "https://netbird.example.com"
api_token: "{{ netbird_token }}"
invite_id: "invite-id-123"
state: absent
'''
RETURN = r'''
invite:
description: The user invite object.
returned: success
type: dict
contains:
id:
description: Invite ID.
type: str
email:
description: Invited user email.
type: str
name:
description: Invited user name.
type: str
role:
description: Assigned role.
type: str
auto_groups:
description: Auto-assigned group IDs.
type: list
expires_at:
description: Invite expiration timestamp.
type: str
created_at:
description: Invite creation timestamp.
type: str
expired:
description: Whether the invite has expired.
type: bool
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.ansible_netbird.plugins.module_utils.netbird_api import (
NetBirdAPI,
NetBirdAPIError,
netbird_argument_spec
)
def find_invite_by_email(api, email):
"""Find a user invite by email address."""
invites, _ = api.list_user_invites()
for invite in invites:
if invite.get('email') == email:
return invite
return None
def run_module():
"""Main module execution."""
argument_spec = netbird_argument_spec()
argument_spec.update(
state=dict(type='str', choices=['present', 'absent'], default='present'),
invite_id=dict(type='str'),
email=dict(type='str'),
name=dict(type='str'),
role=dict(type='str', choices=['admin', 'user'], default='user'),
auto_groups=dict(type='list', elements='str', default=[]),
expires_in=dict(type='int'),
regenerate=dict(type='bool', default=False)
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
required_if=[
('state', 'present', ['email']),
]
)
api = NetBirdAPI(
module,
module.params['api_url'],
module.params['api_token'],
module.params['validate_certs']
)
state = module.params['state']
invite_id = module.params['invite_id']
email = module.params['email']
name = module.params['name']
role = module.params['role']
auto_groups = module.params['auto_groups']
expires_in = module.params['expires_in']
regenerate = module.params['regenerate']
result = dict(
changed=False,
invite={}
)
try:
if state == 'absent':
if invite_id:
if not module.check_mode:
api.delete_user_invite(invite_id)
result['changed'] = True
result['msg'] = 'User invite deleted successfully'
elif email:
existing_invite = find_invite_by_email(api, email)
if existing_invite:
if not module.check_mode:
api.delete_user_invite(existing_invite['id'])
result['changed'] = True
result['msg'] = 'User invite deleted successfully'
module.exit_json(**result)
# state == 'present'
existing_invite = find_invite_by_email(api, email)
if existing_invite:
if regenerate:
# Regenerate the invite token
if not module.check_mode:
invite, _ = api.regenerate_user_invite(
existing_invite['id'],
expires_in=expires_in
)
result['invite'] = invite
else:
result['invite'] = existing_invite
result['changed'] = True
else:
# Invite already exists, return it
result['invite'] = existing_invite
result['msg'] = 'User invite already exists'
else:
# Create new invite
if not module.check_mode:
invite, _ = api.create_user_invite(
email=email,
name=name,
role=role,
auto_groups=auto_groups,
expires_in=expires_in
)
result['invite'] = invite
result['changed'] = True
module.exit_json(**result)
except NetBirdAPIError as e:
module.fail_json(msg=str(e), status_code=e.status_code, response=e.response)
def main():
run_module()
if __name__ == '__main__':
main()
+13 -3
View File
@@ -72,6 +72,11 @@ options:
- Whether to masquerade (NAT) traffic through this router.
type: bool
default: false
enabled:
description:
- Whether the router is enabled.
type: bool
default: true
resources:
description:
- List of resources (network addresses, CIDRs, or domains) for this network.
@@ -348,6 +353,8 @@ def router_needs_update(current, desired):
return True
if current.get('masquerade') != desired.get('masquerade', False):
return True
if current.get('enabled') != desired.get('enabled', True):
return True
return False
@@ -399,7 +406,8 @@ def sync_routers(api, module, network_id, desired_routers):
peer_id=peer if peer else None,
peer_groups=peer_groups,
metric=desired.get('metric', 9999),
masquerade=desired.get('masquerade', False)
masquerade=desired.get('masquerade', False),
enabled=desired.get('enabled', True)
)
final_routers.append(updated)
else:
@@ -415,7 +423,8 @@ def sync_routers(api, module, network_id, desired_routers):
peer_id=peer if peer else None,
peer_groups=peer_groups,
metric=desired.get('metric', 9999),
masquerade=desired.get('masquerade', False)
masquerade=desired.get('masquerade', False),
enabled=desired.get('enabled', True)
)
final_routers.append(created)
changed = True
@@ -503,7 +512,8 @@ def run_module():
peer=dict(type='str'),
peer_groups=dict(type='list', elements='str'),
metric=dict(type='int', default=9999),
masquerade=dict(type='bool', default=False)
masquerade=dict(type='bool', default=False),
enabled=dict(type='bool', default=True)
),
required_one_of=[('peer', 'peer_groups')],
mutually_exclusive=[('peer', 'peer_groups')]
+14 -6
View File
@@ -52,6 +52,11 @@ options:
description:
- Whether approval is required for the peer.
type: bool
ip:
description:
- The IP address to assign to the peer within the NetBird network.
- Allows reassigning a peer's IP.
type: str
extends_documentation_fragment:
- community.ansible_netbird.netbird
requirements:
@@ -167,8 +172,8 @@ from ansible_collections.community.ansible_netbird.plugins.module_utils.netbird_
def peer_needs_update(current, params):
"""Check if peer needs to be updated."""
for key in ['name', 'ssh_enabled', 'login_expiration_enabled',
'inactivity_expiration_enabled', 'approval_required']:
for key in ['name', 'ssh_enabled', 'login_expiration_enabled',
'inactivity_expiration_enabled', 'approval_required', 'ip']:
if params.get(key) is not None:
if current.get(key) != params[key]:
return True
@@ -185,7 +190,8 @@ def run_module():
ssh_enabled=dict(type='bool'),
login_expiration_enabled=dict(type='bool'),
inactivity_expiration_enabled=dict(type='bool'),
approval_required=dict(type='bool')
approval_required=dict(type='bool'),
ip=dict(type='str')
)
module = AnsibleModule(
@@ -232,9 +238,10 @@ def run_module():
'ssh_enabled': module.params['ssh_enabled'],
'login_expiration_enabled': module.params['login_expiration_enabled'],
'inactivity_expiration_enabled': module.params['inactivity_expiration_enabled'],
'approval_required': module.params['approval_required']
'approval_required': module.params['approval_required'],
'ip': module.params['ip']
}
if peer_needs_update(existing_peer, update_params):
if not module.check_mode:
peer, _ = api.update_peer(
@@ -243,7 +250,8 @@ def run_module():
ssh_enabled=module.params['ssh_enabled'],
login_expiration_enabled=module.params['login_expiration_enabled'],
inactivity_expiration_enabled=module.params['inactivity_expiration_enabled'],
approval_required=module.params['approval_required']
approval_required=module.params['approval_required'],
ip=module.params['ip']
)
result['peer'] = peer
else:
+123 -5
View File
@@ -45,6 +45,11 @@ options:
- Whether the policy is enabled.
type: bool
default: true
source_posture_checks:
description:
- List of posture check IDs applied to policy source groups.
type: list
elements: str
rules:
description:
- List of policy rules.
@@ -82,7 +87,7 @@ options:
default: true
protocol:
description:
- Network protocol (all, tcp, udp, icmp).
- Network protocol (all, tcp, udp, icmp, netbird-ssh).
type: str
default: all
ports:
@@ -90,6 +95,53 @@ options:
- List of destination ports (e.g., ["80", "443", "8000-9000"]).
type: list
elements: str
port_ranges:
description:
- List of port ranges.
- Each range has start and end port numbers.
type: list
elements: dict
suboptions:
start:
description:
- Start port number.
type: int
required: true
end:
description:
- End port number.
type: int
required: true
destination_resource:
description:
- Destination network resource for the rule.
type: dict
suboptions:
id:
description:
- Resource ID.
type: str
required: true
type:
description:
- Resource type.
type: str
required: true
source_resource:
description:
- Source network resource for the rule.
type: dict
suboptions:
id:
description:
- Resource ID.
type: str
required: true
type:
description:
- Resource type.
type: str
required: true
action:
description:
- Action to take (accept, drop).
@@ -194,6 +246,43 @@ def find_policy_by_name(api, name):
return None
def build_rule_data(rule):
"""Build rule payload for the API from Ansible rule config."""
rule_data = {}
if rule.get('name') is not None:
rule_data['name'] = rule['name']
if rule.get('description') is not None:
rule_data['description'] = rule['description']
if rule.get('enabled') is not None:
rule_data['enabled'] = rule['enabled']
if rule.get('sources') is not None:
rule_data['sources'] = rule['sources']
if rule.get('destinations') is not None:
rule_data['destinations'] = rule['destinations']
if rule.get('bidirectional') is not None:
rule_data['bidirectional'] = rule['bidirectional']
if rule.get('protocol') is not None:
rule_data['protocol'] = rule['protocol']
if rule.get('ports') is not None:
rule_data['ports'] = rule['ports']
if rule.get('port_ranges') is not None:
rule_data['port_ranges'] = rule['port_ranges']
if rule.get('destination_resource') is not None:
rule_data['destinationResource'] = rule['destination_resource']
if rule.get('source_resource') is not None:
rule_data['sourceResource'] = rule['source_resource']
if rule.get('action') is not None:
rule_data['action'] = rule['action']
return rule_data
def build_rules_data(rules):
"""Build list of rule payloads for the API."""
if rules is None:
return None
return [build_rule_data(rule) for rule in rules]
def policy_needs_update(current, params):
"""Check if policy needs to be updated."""
if params.get('name') is not None and current.get('name') != params['name']:
@@ -202,6 +291,8 @@ def policy_needs_update(current, params):
return True
if params.get('enabled') is not None and current.get('enabled') != params['enabled']:
return True
if params.get('source_posture_checks') is not None and current.get('source_posture_checks') != params['source_posture_checks']:
return True
# For rules, always update if provided to ensure they match exactly
if params.get('rules') is not None:
return True
@@ -217,7 +308,30 @@ def run_module():
name=dict(type='str'),
description=dict(type='str', default=''),
enabled=dict(type='bool', default=True),
rules=dict(type='list', elements='dict')
source_posture_checks=dict(type='list', elements='str'),
rules=dict(type='list', elements='dict', options=dict(
name=dict(type='str'),
description=dict(type='str'),
enabled=dict(type='bool', default=True),
sources=dict(type='list', elements='str'),
destinations=dict(type='list', elements='str'),
bidirectional=dict(type='bool', default=True),
protocol=dict(type='str', default='all'),
ports=dict(type='list', elements='str'),
port_ranges=dict(type='list', elements='dict', options=dict(
start=dict(type='int', required=True),
end=dict(type='int', required=True)
)),
destination_resource=dict(type='dict', options=dict(
id=dict(type='str', required=True),
type=dict(type='str', required=True)
)),
source_resource=dict(type='dict', options=dict(
id=dict(type='str', required=True),
type=dict(type='str', required=True)
)),
action=dict(type='str', default='accept')
))
)
module = AnsibleModule(
@@ -240,7 +354,8 @@ def run_module():
name = module.params['name']
description = module.params['description']
enabled = module.params['enabled']
rules = module.params['rules']
source_posture_checks = module.params['source_posture_checks']
rules = build_rules_data(module.params['rules'])
result = dict(
changed=False,
@@ -274,6 +389,7 @@ def run_module():
'name': name,
'description': description,
'enabled': enabled,
'source_posture_checks': source_posture_checks,
'rules': rules
}
@@ -284,7 +400,8 @@ def run_module():
name=name,
enabled=enabled,
description=description,
rules=rules
rules=rules,
source_posture_checks=source_posture_checks
)
result['policy'] = policy
else:
@@ -302,7 +419,8 @@ def run_module():
name=name,
enabled=enabled,
description=description,
rules=rules or []
rules=rules or [],
source_posture_checks=source_posture_checks
)
result['policy'] = policy
result['changed'] = True
-1
View File
@@ -278,7 +278,6 @@ def run_module():
if not module.check_mode:
key, _ = api.update_setup_key(
existing_key['id'],
name=name,
revoked=module.params['revoked'],
auto_groups=module.params['auto_groups']
)
+29 -4
View File
@@ -24,7 +24,6 @@ options:
- The desired state of the user.
type: str
choices: ['present', 'absent']
default: present
user_id:
description:
- The unique identifier of the user.
@@ -60,6 +59,14 @@ options:
description:
- If set to true, the user is blocked and cannot use the system.
type: bool
action:
description:
- Action to perform on the user.
- Use 'approve' to approve a user with pending approval status.
- Use 'reject' to reject a user with pending approval status.
- Mutually exclusive with state.
type: str
choices: ['approve', 'reject']
resend_invitation:
description:
- Resend user invitation email.
@@ -193,7 +200,7 @@ def run_module():
"""Main module execution."""
argument_spec = netbird_argument_spec()
argument_spec.update(
state=dict(type='str', choices=['present', 'absent'], default='present'),
state=dict(type='str', choices=['present', 'absent']),
user_id=dict(type='str'),
email=dict(type='str'),
name=dict(type='str'),
@@ -201,7 +208,8 @@ def run_module():
auto_groups=dict(type='list', elements='str', default=[]),
is_service_user=dict(type='bool', default=False),
is_blocked=dict(type='bool'),
resend_invitation=dict(type='bool', default=False)
resend_invitation=dict(type='bool', default=False),
action=dict(type='str', choices=['approve', 'reject']),
)
module = AnsibleModule(
@@ -209,7 +217,9 @@ def run_module():
supports_check_mode=True,
required_if=[
('state', 'absent', ['user_id']),
]
],
mutually_exclusive=[['state', 'action']],
required_one_of=[['state', 'action']]
)
api = NetBirdAPI(
@@ -235,6 +245,21 @@ def run_module():
)
try:
# Handle action parameter (approve/reject)
action = module.params.get('action')
if action:
user_id = module.params['user_id']
if not user_id:
module.fail_json(msg="user_id is required when using action parameter")
if not module.check_mode:
if action == 'approve':
api.approve_user(user_id)
elif action == 'reject':
api.reject_user(user_id)
result['changed'] = True
result['msg'] = f'User {action}d successfully'
module.exit_json(**result)
# Handle resend invitation
if resend_invitation and user_id:
if not module.check_mode:
+18
View File
@@ -0,0 +1,18 @@
---
# tasks/identity_providers.yml - Identity provider management tasks
- name: Manage NetBird identity providers
community.ansible_netbird.netbird_idp:
api_url: "{{ netbird_api_url }}"
api_token: "{{ netbird_api_token }}"
validate_certs: "{{ netbird_validate_certs }}"
name: "{{ item.name }}"
type: "{{ item.type }}"
issuer: "{{ item.issuer }}"
client_id: "{{ item.client_id }}"
client_secret: "{{ item.client_secret }}"
state: "{{ item.state | default('present') }}"
loop: "{{ netbird_identity_providers }}"
loop_control:
label: "{{ item.name }}"
when: netbird_identity_providers | length > 0
+19
View File
@@ -0,0 +1,19 @@
---
# tasks/invites.yml - User invite management tasks
- name: Manage NetBird user invites
community.ansible_netbird.netbird_invite:
api_url: "{{ netbird_api_url }}"
api_token: "{{ netbird_api_token }}"
validate_certs: "{{ netbird_validate_certs }}"
email: "{{ item.email }}"
name: "{{ item.name | default(omit) }}"
role: "{{ item.role | default('user') }}"
auto_groups: "{{ item.auto_groups | default([]) }}"
expires_in: "{{ item.expires_in | default(omit) }}"
regenerate: "{{ item.regenerate | default(false) }}"
state: "{{ item.state | default('present') }}"
loop: "{{ netbird_invites }}"
loop_control:
label: "{{ item.email }}"
when: netbird_invites | length > 0
+14
View File
@@ -64,3 +64,17 @@
- netbird
- netbird-posture-checks
- name: Include identity provider management tasks
ansible.builtin.include_tasks: identity_providers.yml
when: netbird_identity_providers | length > 0
tags:
- netbird
- netbird-identity-providers
- name: Include user invite management tasks
ansible.builtin.include_tasks: invites.yml
when: netbird_invites | length > 0
tags:
- netbird
- netbird-invites