Skip to content

Commit dcb83db

Browse files
nautobot secrets
1 parent c99cbda commit dcb83db

File tree

14 files changed

+428
-0
lines changed

14 files changed

+428
-0
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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: Ensure nautobot is up and responding
13+
ansible.builtin.uri:
14+
url: "https://{{ nautobot_data.hostname }}/health/"
15+
method: GET
16+
validate_certs: false
17+
register: nautobot_up_check
18+
until: nautobot_up_check.status == 200
19+
retries: 24 # Retries for 24 * 5 seconds = 120 seconds = 2 minutes
20+
delay: 5 # Every 5 seconds
21+
check_mode: false
22+
23+
roles:
24+
- role: users
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
---
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
from datetime import datetime, timedelta
2+
from ansible.module_utils.basic import AnsibleModule
3+
import requests
4+
5+
6+
def check_existing_token(base_url, username, password):
7+
"""Check existing tokens for user and return token + warning if expiring."""
8+
headers = {"Accept": "application/json"}
9+
tokens_url = f"{base_url}/api/users/tokens/"
10+
11+
try:
12+
response = requests.get(tokens_url, headers=headers, auth=(username, password))
13+
response.raise_for_status()
14+
except requests.exceptions.RequestException as e:
15+
return None, f"Failed to fetch tokens: {e}"
16+
17+
data = response.json()
18+
tokens = data.get("results", [])
19+
20+
if not tokens:
21+
return None, "No tokens found"
22+
23+
if len(tokens) > 1:
24+
return None, "Multiple tokens found, expected exactly 1"
25+
26+
token = tokens[0]
27+
expires = token.get("expires")
28+
29+
if expires:
30+
try:
31+
expire_date = datetime.fromisoformat(expires.replace("Z", "+00:00"))
32+
tomorrow = datetime.now(tz=expire_date.tzinfo) + timedelta(days=1)
33+
if expire_date <= tomorrow:
34+
return token, "Token expiring within 1 day"
35+
except ValueError:
36+
return token, f"Invalid expiration date format: {expires}"
37+
38+
return token, None
39+
40+
41+
def create_new_token(
42+
base_url, username, password, user_token, description="ansible-created-token"
43+
):
44+
"""Create a new Nautobot token using Basic Auth."""
45+
tokens_url = f"{base_url}/api/users/tokens/"
46+
headers = {"Content-Type": "application/json", "Accept": "application/json"}
47+
payload = {"key": user_token, "description": description, "write_enabled": True}
48+
49+
try:
50+
response = requests.post(
51+
tokens_url, headers=headers, json=payload, auth=(username, password)
52+
)
53+
response.raise_for_status()
54+
except requests.exceptions.RequestException as e:
55+
return None, f"Failed to create new token: {e}"
56+
57+
return response.json(), None
58+
59+
60+
def run_module():
61+
module_args = dict(
62+
base_url=dict(type="str", required=True),
63+
username=dict(type="str", required=True),
64+
password=dict(type="str", required=True, no_log=True),
65+
token=dict(type="str", required=True, no_log=True),
66+
replace_if_expiring=dict(type="bool", default=True),
67+
create_if_notfound=dict(type="bool", default=True),
68+
token_description=dict(type="str", default="ansible-created-token"),
69+
)
70+
71+
module = AnsibleModule(argument_spec=module_args, supports_check_mode=True)
72+
result = dict(changed=False, token=None, message="")
73+
74+
base_url = module.params["base_url"].rstrip("/")
75+
username = module.params["username"]
76+
password = module.params["password"]
77+
user_token = module.params["token"]
78+
replace_if_expiring = module.params["replace_if_expiring"]
79+
create_if_notfound = module.params["create_if_notfound"]
80+
token_description = module.params["token_description"]
81+
82+
if module.check_mode:
83+
module.exit_json(**result)
84+
85+
# Check existing token
86+
token, warning = check_existing_token(base_url, username, password)
87+
88+
if token:
89+
# If token is expiring and replace_if_expiring=True → create new token
90+
if warning and replace_if_expiring:
91+
new_token, err = create_new_token(
92+
base_url, username, password, user_token, token_description
93+
)
94+
if err:
95+
module.fail_json(msg=err)
96+
result.update(
97+
changed=True,
98+
message=f"Old token expiring, created new token for {username}",
99+
)
100+
module.exit_json(**result)
101+
102+
# Token is valid → return metadata only
103+
result.update(
104+
changed=False,
105+
message=f"Found valid token for {username}",
106+
token=dict(
107+
id=str(token.get("id")),
108+
display=str(token.get("display")),
109+
created=str(token.get("created")),
110+
expires=str(token.get("expires")),
111+
write_enabled=bool(token.get("write_enabled")),
112+
description=str(token.get("description", "No description")),
113+
),
114+
)
115+
module.exit_json(**result)
116+
117+
# No token found → create new if allowed
118+
if create_if_notfound:
119+
new_token, err = create_new_token(
120+
base_url, username, password, user_token, token_description
121+
)
122+
if err:
123+
module.fail_json(msg=err)
124+
result.update(
125+
changed=True,
126+
message=f"No token found, created new token for {username}",
127+
)
128+
module.exit_json(**result)
129+
130+
# No token and not allowed to create → fail
131+
module.fail_json(msg=f"No token found for {username} and creation disabled")
132+
133+
134+
def main():
135+
run_module()
136+
137+
138+
if __name__ == "__main__":
139+
main()
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
- name: Query user in Nautobot
3+
ansible.builtin.uri:
4+
url: "https://{{ nautobot_data.hostname }}/api/users/users/{{ nautobot_data.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_data.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_data.username }}"
22+
password: "{{ nautobot_data.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_data.hostname }}"
31+
username: "{{ nautobot_data.username }}"
32+
password: "{{ nautobot_data.password }}"
33+
token: "{{ nautobot_data.token }}"
34+
replace_if_expiring: true
35+
register: nautobot_token_result
36+
# no_log: true # optionally hide token in logs

apps/kustomization.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ resources:
99
- appsets/appset-understack-global.yaml
1010
- appsets/appset-understack-site.yaml
1111
- appsets/appset-understack-openstack.yaml
12+
- appsets/appset-understack-site-only.yaml
1213

1314
# you can do something like below to allow your deployment repo
1415
# to define the exact versions that you want to use
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
component: nautobot-secrets
3+
componentNamespace: nautobot
4+
sources:
5+
- ref: understack
6+
path: 'components/secretstore-gen-secrets'
7+
helm:
8+
releaseName: nautobot-secrets
9+
valueFiles:
10+
- $deploy/{{.name}}/helm-configs/secretstore-nautobot-secrets.yaml
11+
ignoreMissingValueFiles: true
12+
- ref: deploy
13+
path: '{{.name}}/manifests/secret-store'
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Patterns to ignore when building packages.
2+
# This supports shell glob matching, relative path matching, and
3+
# negation (prefixed with !). Only one pattern per line.
4+
.DS_Store
5+
# Common VCS dirs
6+
.git/
7+
.gitignore
8+
.bzr/
9+
.bzrignore
10+
.hg/
11+
.hgignore
12+
.svn/
13+
# Common backup files
14+
*.swp
15+
*.bak
16+
*.tmp
17+
*.orig
18+
*~
19+
# Various IDEs
20+
.project
21+
.idea/
22+
*.tmproj
23+
.vscode/
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
apiVersion: v2
2+
name: site-secrets
3+
description: Orchestrating secrets across kubernetes clusters (global-site) using External SecretStore
4+
type: application
5+
version: 0.1.0
6+
appVersion: "1.0"
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{{- $site := .Values.site }}
2+
{{- $secretStore := $site.secretStore }}
3+
{{- range $site.secrets }}
4+
---
5+
apiVersion: external-secrets.io/v1
6+
kind: ExternalSecret
7+
metadata:
8+
name: {{ .name }}
9+
{{- if .externalLinkAnnotationTemplate }}
10+
annotations:
11+
link.argocd.argoproj.io/external-link: {{ tpl .externalLinkAnnotationTemplate . }}
12+
{{- end }}
13+
{{- with .labels }}
14+
labels:
15+
{{ toYaml . | indent 4 }}
16+
{{- end }}
17+
spec:
18+
refreshInterval: {{ .refreshInterval | default "1h" }}
19+
secretStoreRef:
20+
kind: {{ $secretStore.kind }}
21+
name: {{ $secretStore.name }}
22+
target:
23+
name: {{ .name }}
24+
creationPolicy: Owner
25+
template:
26+
engineVersion: v2
27+
type: {{ .templateType | default "Opaque" }}
28+
{{- with .labels }}
29+
metadata:
30+
labels:
31+
{{ toYaml . | indent 10 }}
32+
{{- end }}
33+
{{- if .templateData }}
34+
data:
35+
{{- range $k, $v := .templateData }}
36+
{{ $k }}: {{ $v | quote }}
37+
{{- end }}
38+
{{- end }}
39+
{{- if .data }}
40+
data:
41+
{{- range .data }}
42+
- secretKey: {{ .secretKey }}
43+
remoteRef:
44+
key: {{ .remoteRef.key }}
45+
property: {{ .remoteRef.property }}
46+
conversionStrategy: {{ .remoteRef.conversionStrategy | default "Default" }}
47+
decodingStrategy: {{ .remoteRef.decodingStrategy | default "None" }}
48+
metadataPolicy: {{ .remoteRef.metadataPolicy | default "None" }}
49+
{{- end }}
50+
{{- end }}
51+
{{- if .dataFrom }}
52+
dataFrom:
53+
{{- toYaml .dataFrom | nindent 4 }}
54+
{{- end }}
55+
{{- end }}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
site:
2+
name: uc-iad3-dev
3+
partition: uc-dev
4+
env: dev
5+
role: aio
6+
secretStore:
7+
kind: SecretStore
8+
name: vault

0 commit comments

Comments
 (0)