Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions ansible/playbooks/nautobot-user-token.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
- name: Nautobot service accounts for Undercloud services
connection: local
hosts: nautobot
gather_facts: false

pre_tasks:
- name: Load Nautobot credentials from environment
ansible.builtin.set_fact:
nautobot_data: "{{ lookup('env', 'EXTRA_VARS') | from_json }}"

- name: Decode Nautobot credentials
ansible.builtin.set_fact:
nautobot_hostname: "{{ nautobot_data.hostname | b64decode }}"
nautobot_username: "{{ nautobot_data.username | b64decode }}"
nautobot_password: "{{ nautobot_data.password | b64decode }}"
nautobot_user_token: "{{ nautobot_data.token | b64decode }}"

- name: Ensure nautobot is up and responding
ansible.builtin.uri:
url: "https://{{ nautobot_hostname }}/health/"
method: GET
validate_certs: false
register: nautobot_up_check
until: nautobot_up_check.status == 200
retries: 24 # Retries for 24 * 5 seconds = 120 seconds = 2 minutes
delay: 5 # Every 5 seconds
check_mode: false

roles:
- role: users
1 change: 1 addition & 0 deletions ansible/roles/users/defaults/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
---
85 changes: 85 additions & 0 deletions ansible/roles/users/library/nautobot_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from ansible.module_utils.basic import AnsibleModule
import requests


def get_existing_token(base_url, username, password, user_token, module):
headers = {"Accept": "application/json"}
tokens_url = f"{base_url}/api/users/tokens/"

try:
response = requests.get(tokens_url, headers=headers, auth=(username, password))
response.raise_for_status()
except requests.exceptions.RequestException as e:
module.fail_json(
msg=f"Failed to fetch existing tokens for user {username}: {str(e)}"
)

tokens = response.json().get("results", [])
return next((t for t in tokens if t.get("key") == user_token), None)


def create_new_token(base_url, username, password, user_token, description, module):
"""Create a new Nautobot token using Basic Auth."""
tokens_url = f"{base_url}/api/users/tokens/"
headers = {"Content-Type": "application/json", "Accept": "application/json"}
payload = {"key": user_token, "description": description, "write_enabled": True}

try:
response = requests.post(
tokens_url, headers=headers, json=payload, auth=(username, password)
)
response.raise_for_status()
except requests.exceptions.RequestException as e:
module.fail_json(
msg=f"Failed to create new token for user {username}: {str(e)}"
)

return response.json()


def run_module():
module_args = dict(
base_url=dict(type="str", required=True),
username=dict(type="str", required=True),
password=dict(type="str", required=True, no_log=True),
user_token=dict(type="str", required=True, no_log=True),
token_description=dict(type="str", default="ansible-created-token"),
)

module = AnsibleModule(argument_spec=module_args, supports_check_mode=True)

base_url = module.params["base_url"].rstrip("/")
username = module.params["username"]
password = module.params["password"]
user_token = module.params["user_token"]
token_description = module.params["token_description"]

# fetch existing token
token = get_existing_token(base_url, username, password, user_token, module)
if token:
module.exit_json(
changed=False,
username=username,
message=f"Found existing Nautobot token for user {username}",
)

# No token found → try creating new
new_token = create_new_token(
base_url, username, password, user_token, token_description, module
)
if not new_token:
module.fail_json(msg=f"Failed to create new token for user {username}")

module.exit_json(
changed=True,
username=username,
message=f"No token found, created new Nautobot token for user {username}",
)


def main():
run_module()


if __name__ == "__main__":
main()
34 changes: 34 additions & 0 deletions ansible/roles/users/tasks/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
- name: Query user in Nautobot
ansible.builtin.uri:
url: "https://{{ nautobot_hostname }}/api/users/users/{{ nautobot_username }}"
method: GET
headers:
Authorization: "Token {{ nautobot_token }}"
return_content: true
status_code: [200, 404]
register: user_query_result

- name: Create user in Nautobot if missing
ansible.builtin.uri:
url: "https://{{ nautobot_hostname }}/api/users/users/"
method: POST
headers:
Authorization: "Token {{ nautobot_token }}"
Content-Type: "application/json"
body_format: json
body:
username: "{{ nautobot_username }}"
password: "{{ nautobot_password }}"
is_staff: true
is_superuser: true
status_code: [201]
when: user_query_result.status == 404

- name: Ensure Nautobot token exists for user
nautobot_token:
base_url: "https://{{ nautobot_hostname }}"
username: "{{ nautobot_username }}"
password: "{{ nautobot_password }}"
user_token: "{{ nautobot_user_token }}"
register: nautobot_token_result
5 changes: 5 additions & 0 deletions apps/site/nautobot-site.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
component: nautobot
sources:
- ref: deploy
path: '{{.name}}/manifests/nautobot'
4 changes: 4 additions & 0 deletions components/nautobot/secretstore-nautobot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ rules:
- watch
resourceNames:
- nautobot-superuser
- ansible-token
- openstack-token
- workflow-token
- undersync-token
- apiGroups:
- authorization.k8s.io
resources:
Expand Down
28 changes: 28 additions & 0 deletions components/undersync/nautobot-token.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: nautobot-token
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: nautobot
target:
name: nautobot-token
creationPolicy: Owner
deletionPolicy: Delete
template:
engineVersion: v2
data:
token: "{{ .token }}"
bearer_token: "Token {{ .token }}"
data:
- secretKey: token
remoteRef:
key: undersync-token
property: token
# necessary to avoid argoproj/argo-cd#13004
conversionStrategy: Default
decodingStrategy: None
metadataPolicy: None
4 changes: 2 additions & 2 deletions workflows/argo-events/secrets/nautobot-token.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ spec:
data:
- secretKey: token
remoteRef:
key: nautobot-superuser
property: apitoken
key: workflow-token
property: token
# necessary to avoid argoproj/argo-cd#13004
conversionStrategy: Default
decodingStrategy: None
Expand Down
1 change: 1 addition & 0 deletions workflows/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ kind: Kustomization
resources:
- openstack
- argo-events
- nautobot
14 changes: 14 additions & 0 deletions workflows/nautobot/eventbus/eventbus-default.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# default NATS EventBus sourced from:
# https://raw.githubusercontent.com/argoproj/argo-events/stable/examples/eventbus/native.yaml

apiVersion: argoproj.io/v1alpha1
kind: EventBus
metadata:
name: default
spec:
nats:
native:
# Optional, defaults to 3. If it is < 3, set it to 3, that is the minimal requirement.
replicas: 3
# Optional, authen strategy, "none" or "token", defaults to "none"
auth: token
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: eventbus-default-pdb
spec:
maxUnavailable: 1
selector:
matchLabels:
controller: eventbus-controller
eventbus-name: default
19 changes: 19 additions & 0 deletions workflows/nautobot/eventsources/k8s-secret-nautobot-token.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
apiVersion: argoproj.io/v1alpha1
kind: EventSource
metadata:
name: k8s-secret-nautobot-token
spec:
template:
serviceAccountName: k8s-events-secret-nautobot
resource:
nautobot-token-secret:
namespace: nautobot
resource: secrets
version: v1
eventTypes:
- ADD
- UPDATE
filter:
labels:
- key: token/type
value: nautobot
14 changes: 14 additions & 0 deletions workflows/nautobot/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: nautobot

resources:
- eventbus/eventbus-default.yaml
- eventbus/poddisruptionbudget-eventbus-default-pdb.yaml

# nautobot secret
- serviceaccounts/k8s-secret-events-nautobot.yaml
- serviceaccounts/k8s-job-create.yaml
- eventsources/k8s-secret-nautobot-token.yaml
- sensors/k8s-nautobot-secret.yaml
71 changes: 71 additions & 0 deletions workflows/nautobot/sensors/k8s-nautobot-secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
name: secret-nautobot-token
annotations:
workflows.argoproj.io/title: Nautobot Token
workflows.argoproj.io/description: |+
Triggered when the Kubernetes Nautobot token secret is created,
this process ensures the user is created in Nautobot and a corresponding token is provisioned.
spec:
template:
serviceAccountName: k8s-job-create
dependencies:
- name: nautobot-token-secret
eventSourceName: k8s-secret-nautobot-token
eventName: nautobot-token-secret
triggers:
- template:
name: nautobot-users
k8s:
operation: create
parameters:
# Pass the body.data as JSON string into the Job environment
- src:
dependencyName: nautobot-token-secret
dataKey: body.data
transformer:
jqFilter: '@json'
dest: spec.template.spec.containers.0.env.0.value
source:
resource:
apiVersion: batch/v1
kind: Job
metadata:
generateName: nautobot-create-token-
spec:
template:
spec:
containers:
- name: nautobot-create-token
image: ghcr.io/rackerlabs/understack/ansible:latest
imagePullPolicy: Always
command:
- "ansible-runner"
- "run"
- "/runner"
- "--playbook"
- "nautobot-user-token.yaml"
env:
- name: EXTRA_VARS
value: "" # Will be populated by the Sensor mapping
- name: NAUTOBOT_TOKEN
valueFrom:
secretKeyRef:
name: nautobot-superuser
key: apitoken
volumeMounts:
- name: ansible-inventory
mountPath: /runner/inventory/
- name: ansible-group-vars
mountPath: /runner/inventory/group_vars/
volumes:
- name: runner-data
emptyDir: {}
- name: ansible-inventory
configMap:
name: ansible-inventory
- name: ansible-group-vars
configMap:
name: ansible-group-vars
restartPolicy: OnFailure
29 changes: 29 additions & 0 deletions workflows/nautobot/serviceaccounts/k8s-job-create.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: k8s-job-create

---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: k8s-job
rules:
- apiGroups: ["batch"]
resources: ["jobs"]
verbs: ["create"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: k8s-job-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: k8s-job
subjects:
- kind: ServiceAccount
name: k8s-job-create
namespace: nautobot
Loading
Loading