Skip to content

Commit 7014a25

Browse files
authored
Merge pull request #1256 from rackerlabs/global-secrets
feat: Automate Nautobot service account provisioning
2 parents 4509f2d + a59a975 commit 7014a25

File tree

17 files changed

+379
-4
lines changed

17 files changed

+379
-4
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
- name: Nautobot service accounts for Undercloud services
3+
connection: local
4+
hosts: nautobot
5+
gather_facts: false
6+
7+
pre_tasks:
8+
- name: Load Nautobot credentials from environment
9+
ansible.builtin.set_fact:
10+
nautobot_data: "{{ lookup('env', 'EXTRA_VARS') | from_json }}"
11+
12+
- name: Decode Nautobot credentials
13+
ansible.builtin.set_fact:
14+
nautobot_hostname: "{{ nautobot_data.hostname | b64decode }}"
15+
nautobot_username: "{{ nautobot_data.username | b64decode }}"
16+
nautobot_password: "{{ nautobot_data.password | b64decode }}"
17+
nautobot_user_token: "{{ nautobot_data.token | b64decode }}"
18+
19+
- name: Ensure nautobot is up and responding
20+
ansible.builtin.uri:
21+
url: "https://{{ nautobot_hostname }}/health/"
22+
method: GET
23+
validate_certs: false
24+
register: nautobot_up_check
25+
until: nautobot_up_check.status == 200
26+
retries: 24 # Retries for 24 * 5 seconds = 120 seconds = 2 minutes
27+
delay: 5 # Every 5 seconds
28+
check_mode: false
29+
30+
roles:
31+
- role: users
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
---
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from ansible.module_utils.basic import AnsibleModule
2+
import requests
3+
4+
5+
def get_existing_token(base_url, username, password, user_token, module):
6+
headers = {"Accept": "application/json"}
7+
tokens_url = f"{base_url}/api/users/tokens/"
8+
9+
try:
10+
response = requests.get(tokens_url, headers=headers, auth=(username, password))
11+
response.raise_for_status()
12+
except requests.exceptions.RequestException as e:
13+
module.fail_json(
14+
msg=f"Failed to fetch existing tokens for user {username}: {str(e)}"
15+
)
16+
17+
tokens = response.json().get("results", [])
18+
return next((t for t in tokens if t.get("key") == user_token), None)
19+
20+
21+
def create_new_token(base_url, username, password, user_token, description, module):
22+
"""Create a new Nautobot token using Basic Auth."""
23+
tokens_url = f"{base_url}/api/users/tokens/"
24+
headers = {"Content-Type": "application/json", "Accept": "application/json"}
25+
payload = {"key": user_token, "description": description, "write_enabled": True}
26+
27+
try:
28+
response = requests.post(
29+
tokens_url, headers=headers, json=payload, auth=(username, password)
30+
)
31+
response.raise_for_status()
32+
except requests.exceptions.RequestException as e:
33+
module.fail_json(
34+
msg=f"Failed to create new token for user {username}: {str(e)}"
35+
)
36+
37+
return response.json()
38+
39+
40+
def run_module():
41+
module_args = dict(
42+
base_url=dict(type="str", required=True),
43+
username=dict(type="str", required=True),
44+
password=dict(type="str", required=True, no_log=True),
45+
user_token=dict(type="str", required=True, no_log=True),
46+
token_description=dict(type="str", default="ansible-created-token"),
47+
)
48+
49+
module = AnsibleModule(argument_spec=module_args, supports_check_mode=True)
50+
51+
base_url = module.params["base_url"].rstrip("/")
52+
username = module.params["username"]
53+
password = module.params["password"]
54+
user_token = module.params["user_token"]
55+
token_description = module.params["token_description"]
56+
57+
# fetch existing token
58+
token = get_existing_token(base_url, username, password, user_token, module)
59+
if token:
60+
module.exit_json(
61+
changed=False,
62+
username=username,
63+
message=f"Found existing Nautobot token for user {username}",
64+
)
65+
66+
# No token found → try creating new
67+
new_token = create_new_token(
68+
base_url, username, password, user_token, token_description, module
69+
)
70+
if not new_token:
71+
module.fail_json(msg=f"Failed to create new token for user {username}")
72+
73+
module.exit_json(
74+
changed=True,
75+
username=username,
76+
message=f"No token found, created new Nautobot token for user {username}",
77+
)
78+
79+
80+
def main():
81+
run_module()
82+
83+
84+
if __name__ == "__main__":
85+
main()
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
- name: Query user in Nautobot
3+
ansible.builtin.uri:
4+
url: "https://{{ nautobot_hostname }}/api/users/users/{{ nautobot_username }}"
5+
method: GET
6+
headers:
7+
Authorization: "Token {{ nautobot_token }}"
8+
return_content: true
9+
status_code: [200, 404]
10+
register: user_query_result
11+
12+
- name: Create user in Nautobot if missing
13+
ansible.builtin.uri:
14+
url: "https://{{ nautobot_hostname }}/api/users/users/"
15+
method: POST
16+
headers:
17+
Authorization: "Token {{ nautobot_token }}"
18+
Content-Type: "application/json"
19+
body_format: json
20+
body:
21+
username: "{{ nautobot_username }}"
22+
password: "{{ nautobot_password }}"
23+
is_staff: true
24+
is_superuser: true
25+
status_code: [201]
26+
when: user_query_result.status == 404
27+
28+
- name: Ensure Nautobot token exists for user
29+
nautobot_token:
30+
base_url: "https://{{ nautobot_hostname }}"
31+
username: "{{ nautobot_username }}"
32+
password: "{{ nautobot_password }}"
33+
user_token: "{{ nautobot_user_token }}"
34+
register: nautobot_token_result

apps/site/nautobot-site.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
component: nautobot
3+
sources:
4+
- ref: deploy
5+
path: '{{.name}}/manifests/nautobot'

components/nautobot/secretstore-nautobot.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ rules:
2626
- watch
2727
resourceNames:
2828
- nautobot-superuser
29+
- ansible-token
30+
- openstack-token
31+
- workflow-token
32+
- undersync-token
2933
- apiGroups:
3034
- authorization.k8s.io
3135
resources:
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
apiVersion: external-secrets.io/v1
3+
kind: ExternalSecret
4+
metadata:
5+
name: nautobot-token
6+
spec:
7+
refreshInterval: 1h
8+
secretStoreRef:
9+
kind: ClusterSecretStore
10+
name: nautobot
11+
target:
12+
name: nautobot-token
13+
creationPolicy: Owner
14+
deletionPolicy: Delete
15+
template:
16+
engineVersion: v2
17+
data:
18+
token: "{{ .token }}"
19+
bearer_token: "Token {{ .token }}"
20+
data:
21+
- secretKey: token
22+
remoteRef:
23+
key: undersync-token
24+
property: token
25+
# necessary to avoid argoproj/argo-cd#13004
26+
conversionStrategy: Default
27+
decodingStrategy: None
28+
metadataPolicy: None

workflows/argo-events/secrets/nautobot-token.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ spec:
2121
data:
2222
- secretKey: token
2323
remoteRef:
24-
key: nautobot-superuser
25-
property: apitoken
24+
key: workflow-token
25+
property: token
2626
# necessary to avoid argoproj/argo-cd#13004
2727
conversionStrategy: Default
2828
decodingStrategy: None

workflows/kustomization.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ kind: Kustomization
44
resources:
55
- openstack
66
- argo-events
7+
- nautobot
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# default NATS EventBus sourced from:
2+
# https://raw.githubusercontent.com/argoproj/argo-events/stable/examples/eventbus/native.yaml
3+
4+
apiVersion: argoproj.io/v1alpha1
5+
kind: EventBus
6+
metadata:
7+
name: default
8+
spec:
9+
nats:
10+
native:
11+
# Optional, defaults to 3. If it is < 3, set it to 3, that is the minimal requirement.
12+
replicas: 3
13+
# Optional, authen strategy, "none" or "token", defaults to "none"
14+
auth: token

0 commit comments

Comments
 (0)