Compare commits
13 Commits
358f6b0067
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
5b46ba7c74
|
|||
|
7df35b9dd4
|
|||
|
c164cb2598
|
|||
|
085654d017
|
|||
|
5fe7b4edeb
|
|||
|
df1dd39197
|
|||
|
1862f20074
|
|||
|
d31b14cd72
|
|||
|
d981b69669
|
|||
|
995b7c4070
|
|||
|
d11167b345
|
|||
|
7a7c57d0bc
|
|||
|
7e75fa0199
|
34
.ansible-lint
Normal file
34
.ansible-lint
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
profile: basic
|
||||
|
||||
# Paths to exclude from linting
|
||||
exclude_paths:
|
||||
- .ansible/
|
||||
- collections/ansible_collections/
|
||||
- roles/geerlingguy.java/
|
||||
- roles/oatakan.rhel_ovirt_template/
|
||||
- roles/oatakan.rhel_template_build/
|
||||
- roles/oatakan.windows_template_build/
|
||||
- roles/oatakan.windows_update/
|
||||
- roles/oatakan.windows_virtio/
|
||||
- roles/ikke_t.container_image_cleanup/
|
||||
- roles/ikke_t.podman_container_systemd/
|
||||
- roles/sage905.mineos/
|
||||
- roles/sage905.waterfall/
|
||||
|
||||
# Warn rather than fail on these during initial adoption
|
||||
warn_list:
|
||||
- yaml[line-length]
|
||||
- name[casing]
|
||||
- fqcn[action-core]
|
||||
- no-changed-when
|
||||
|
||||
# Use progressive mode: only flag new violations on changed files
|
||||
# (useful for gradual adoption in existing projects)
|
||||
# progressive: true
|
||||
|
||||
mock_modules:
|
||||
- community.proxmox.proxmox_kvm
|
||||
- community.proxmox.proxmox_vm_info
|
||||
|
||||
mock_roles: []
|
||||
11
.claude/agents/ansible-idempotency-reviewer.md
Normal file
11
.claude/agents/ansible-idempotency-reviewer.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
name: ansible-idempotency-reviewer
|
||||
description: Reviews Ansible playbooks for idempotency issues. Use when adding new tasks or before running playbooks against production. Flags POST-only API calls missing 409 handling, uri tasks without state checks, shell/command tasks without creates/removes/changed_when, and non-idempotent register/when patterns.
|
||||
---
|
||||
|
||||
You are an Ansible idempotency expert. When given a playbook or task list:
|
||||
1. Identify tasks that will fail or produce unintended side effects on re-runs
|
||||
2. For `ansible.builtin.uri` POST calls, check for `status_code: [201, 409]` or equivalent guard
|
||||
3. Flag `ansible.builtin.shell`/`command` tasks lacking `creates:`, `removes:`, or `changed_when: false`
|
||||
4. Suggest idempotent alternatives for each flagged task
|
||||
5. Note tasks that are inherently non-idempotent and require manual intervention
|
||||
11
.claude/commands/handoff.md
Normal file
11
.claude/commands/handoff.md
Normal file
@@ -0,0 +1,11 @@
|
||||
Write a session handoff file for the current session.
|
||||
|
||||
Steps:
|
||||
1. Determine handoff type:
|
||||
- **Light Handoff (Template 4A)**: quick task, single session, or output is self-explanatory
|
||||
- **Full Handoff (Template 4B)**: sustained work, multi-phase project, or significant decisions were made
|
||||
2. Read `templates/claude-templates.md` and find the appropriate template.
|
||||
3. Fill in every field based on what was accomplished this session. Include exact file paths for every output, exact numbers, and any conditional logic established.
|
||||
4. Write the handoff to `./docs/summaries/handoff-[today's date]-[topic].md`.
|
||||
5. If a previous handoff file exists in `./docs/summaries/`, move it to `./docs/archive/handoffs/`.
|
||||
6. Tell me the file path of the new handoff and summarize what it contains.
|
||||
13
.claude/commands/process-doc.md
Normal file
13
.claude/commands/process-doc.md
Normal file
@@ -0,0 +1,13 @@
|
||||
Process an input document into a structured source summary.
|
||||
|
||||
Steps:
|
||||
1. Read `templates/claude-templates.md` and find the Source Document Summary template (Template 1).
|
||||
2. Read the document at: $ARGUMENTS
|
||||
3. Extract all information into the template format. Pay special attention to:
|
||||
- EXACT numbers — do not round or paraphrase
|
||||
- Requirements in IF/THEN/BUT/EXCEPT format
|
||||
- Decisions with rationale and rejected alternatives
|
||||
- Open questions marked as OPEN, ASSUMED, or MISSING
|
||||
4. Write the summary to `./docs/summaries/source-[filename].md`.
|
||||
5. Move the original document to `./docs/archive/`.
|
||||
6. Tell me: what was extracted, what's unclear, and what needs follow-up.
|
||||
10
.claude/commands/status.md
Normal file
10
.claude/commands/status.md
Normal file
@@ -0,0 +1,10 @@
|
||||
Report on the current project state.
|
||||
|
||||
Steps:
|
||||
1. Find and read the latest `handoff-*.md` file in `./docs/summaries/` for current state.
|
||||
2. List all files in `./docs/summaries/` to understand what's been processed.
|
||||
3. Report:
|
||||
- **Last session:** what was accomplished (from the latest handoff)
|
||||
- **Next steps:** what the next session should do (from the latest handoff)
|
||||
- **Open questions:** anything unresolved
|
||||
- **Summary file count:** how many files in docs/summaries/ (warn if approaching 15)
|
||||
7
.claude/settings.json
Normal file
7
.claude/settings.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(du:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -107,10 +107,18 @@ venv.bak/
|
||||
|
||||
# Ansible
|
||||
*.retry
|
||||
ansible-navigator.log
|
||||
.ansible/
|
||||
|
||||
# Vendor roles (install via roles/requirements.yml)
|
||||
roles/geerlingguy.*
|
||||
roles/oatakan.*
|
||||
roles/ikke_t.*
|
||||
roles/sage905.*
|
||||
|
||||
.vscode/
|
||||
keys/
|
||||
collections/ansible_collections/
|
||||
.vaultpw
|
||||
context/
|
||||
ansible-navigator.yml
|
||||
|
||||
@@ -3,3 +3,26 @@ repos:
|
||||
rev: v8.18.2
|
||||
hooks:
|
||||
- id: gitleaks
|
||||
|
||||
- repo: https://github.com/adrienverge/yamllint
|
||||
rev: v1.35.1
|
||||
hooks:
|
||||
- id: yamllint
|
||||
args: [--config-file, .yamllint]
|
||||
exclude: |
|
||||
(?x)^(
|
||||
roles/geerlingguy\..*/|
|
||||
roles/oatakan\..*/|
|
||||
roles/ikke_t\..*/|
|
||||
roles/sage905\..*/|
|
||||
\.ansible/|
|
||||
collections/ansible_collections/
|
||||
)
|
||||
|
||||
- repo: https://github.com/ansible/ansible-lint
|
||||
rev: v25.1.3
|
||||
hooks:
|
||||
- id: ansible-lint
|
||||
# ansible-lint reads .ansible-lint for configuration
|
||||
additional_dependencies:
|
||||
- ansible-core>=2.15
|
||||
|
||||
39
.yamllint
Normal file
39
.yamllint
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
extends: default
|
||||
|
||||
rules:
|
||||
# Allow longer lines for readability in tasks
|
||||
line-length:
|
||||
max: 160
|
||||
level: warning
|
||||
|
||||
# Allow both true/false and yes/no boolean styles
|
||||
truthy:
|
||||
allowed-values: ['true', 'false', 'yes', 'no']
|
||||
check-keys: false
|
||||
|
||||
# Ansible uses double-bracket Jinja2 - allow in strings
|
||||
braces:
|
||||
min-spaces-inside: 0
|
||||
max-spaces-inside: 1
|
||||
|
||||
# Allow some indentation flexibility for Ansible block style
|
||||
indentation:
|
||||
spaces: 2
|
||||
indent-sequences: true
|
||||
check-multi-line-strings: false
|
||||
|
||||
# Comments should have a space after #
|
||||
comments:
|
||||
min-spaces-from-content: 1
|
||||
|
||||
# Don't require document-start marker on every file
|
||||
document-start: disable
|
||||
|
||||
ignore: |
|
||||
roles/geerlingguy.*
|
||||
roles/oatakan.*
|
||||
roles/ikke_t.*
|
||||
roles/sage905.*
|
||||
.ansible/
|
||||
collections/ansible_collections/
|
||||
317
OPA_POLICY_ENFORCEMENT.md
Normal file
317
OPA_POLICY_ENFORCEMENT.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# OPA Policy Enforcement for AAP
|
||||
|
||||
This deploys an [Open Policy Agent](https://www.openpolicyagent.org/) (OPA) server on the
|
||||
OpenShift cluster and connects Ansible Automation Platform 2.7 to it. Once connected, AAP
|
||||
asks OPA whether each job may run before launching it.
|
||||
|
||||
- Playbook: [playbooks/deploy_opa.yml](playbooks/deploy_opa.yml)
|
||||
- Role: [roles/opa_server/](roles/opa_server/)
|
||||
- Policies: [roles/opa_server/files/policies/](roles/opa_server/files/policies/)
|
||||
- Policy tests: [roles/opa_server/files/tests/](roles/opa_server/files/tests/)
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
job launch ──► AAP controller ──HTTPS + bearer token──► OPA (opa.opa.svc:8443)
|
||||
│ │
|
||||
│ POST /v1/data/<query path> │ Rego policies
|
||||
│ {"input": <serialized job>} │ + data.aap_policy_config
|
||||
◄──── {"allowed": bool, "violations": [...]}
|
||||
```
|
||||
|
||||
1. A job launches for a job template. That template, its inventory, or its organization has an
|
||||
**OPA query path** set (for example `aap/policy/decision`).
|
||||
2. AAP sends the job's details to OPA once for each of those resources that has a query path.
|
||||
3. OPA evaluates the Rego policy at that path and returns `allowed` plus a list of
|
||||
`violations`.
|
||||
4. If any policy denies the job, AAP blocks it and shows the violation messages on the job.
|
||||
|
||||
**AAP fails closed.** Once a resource has a query path, the job is also blocked when:
|
||||
- OPA can't be reached or times out;
|
||||
- the path doesn't exist in OPA;
|
||||
- the response isn't in the `{allowed, violations}` shape.
|
||||
|
||||
Resources with no query path are never checked.
|
||||
|
||||
### What gets deployed
|
||||
|
||||
| Resource | Purpose |
|
||||
|---|---|
|
||||
| Namespace `opa` | Holds the OPA server |
|
||||
| Deployment `opa` | OPA v1.21.1 (`docker.io/openpolicyagent/opa:1.21.1-static`), non-root, read-only filesystem |
|
||||
| Service `opa` (port 8443) | HTTPS, using a certificate issued by OpenShift's internal service CA |
|
||||
| ConfigMap `opa-policies` | Every `*.rego` file in the policy directory, plus `aap_policy_config.json` |
|
||||
| Secret `opa-authz` | The bearer token, which the server's authorization policy checks |
|
||||
| AAP setting changes | The `OPA_*` settings under **Settings → Policy as Code** |
|
||||
|
||||
### Security model
|
||||
|
||||
- **TLS:** OPA serves TLS with a certificate from OpenShift's internal service CA. AAP is given
|
||||
that CA bundle so it can verify the server.
|
||||
- **Authentication:** every request except `GET /health` needs the bearer token. This is
|
||||
enforced by `opa run --authentication=token --authorization=basic` together with
|
||||
[authz.rego](roles/opa_server/files/policies/authz.rego).
|
||||
- **Token storage:** the token is generated on the first run and stored in Vault at
|
||||
`kv/toallab/opa-server` (key `token`). Later runs reuse it.
|
||||
- **Decision logs:** every decision is written to the OPA pod log for auditing, including the
|
||||
job's extra vars. Turn this off with `opa_server_decision_logs: false`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- AAP is deployed on the cluster ([deploy_aap.yml](playbooks/deploy_aap.yml)).
|
||||
- The `aap` host has `aap_gateway_url` and `aap_k8s_api_url` set in the inventory.
|
||||
- `vault_aap_deployer_token` resolves, and the `aap-deployer` ServiceAccount has the current
|
||||
permissions. The playbook needs to create ConfigMaps, Services, and Deployments, which were
|
||||
added for this. Re-run this after any change to those permissions:
|
||||
|
||||
```bash
|
||||
ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_service_accounts
|
||||
```
|
||||
|
||||
## Deploy
|
||||
|
||||
```bash
|
||||
source ~/.venv/ansible/bin/activate
|
||||
op run --env-file=$HOME/.ansible.zshenv -- \
|
||||
ansible-navigator run playbooks/deploy_opa.yml --mode stdout
|
||||
```
|
||||
|
||||
The playbook:
|
||||
|
||||
1. Reads the token from Vault, or generates and stores one.
|
||||
2. Applies the namespace, policy ConfigMap, token Secret, Service, and Deployment.
|
||||
3. Waits for the TLS certificate and for the pod to become available.
|
||||
4. Updates AAP's `OPA_*` settings.
|
||||
5. Attaches any query paths listed in `opa_server_aap_query_paths`. This list is empty by
|
||||
default.
|
||||
|
||||
Re-running is safe. Pods restart only when the policies, policy config, or token change.
|
||||
|
||||
### Verify the deployment
|
||||
|
||||
```bash
|
||||
oc get pods -n opa
|
||||
oc logs -n opa deploy/opa | head
|
||||
|
||||
# Query OPA directly
|
||||
oc port-forward -n opa svc/opa 8443:8443 &
|
||||
TOKEN=$(vault kv get -field=token kv/toallab/opa-server)
|
||||
|
||||
curl -sk https://localhost:8443/health # {} (no token needed)
|
||||
curl -sk -o /dev/null -w '%{http_code}\n' https://localhost:8443/v1/data # 401
|
||||
curl -sk -H "Authorization: Bearer $TOKEN" \
|
||||
-d '{"input":{"job_type":"run","execution_environment":{"image":"quay.io/x/ee:latest"},"extra_vars":{}}}' \
|
||||
https://localhost:8443/v1/data/aap/policy/decision
|
||||
```
|
||||
|
||||
In AAP, open **Settings → Policy as Code**. You should see:
|
||||
- OPA host `opa.opa.svc`
|
||||
- port `8443`
|
||||
- SSL enabled
|
||||
- auth type `Token`
|
||||
|
||||
## Use it
|
||||
|
||||
### Available query paths
|
||||
|
||||
| Query path | Behavior |
|
||||
|---|---|
|
||||
| `aap/smoke/allow_all` | Always allows. Use it to attach safely and confirm AAP can reach OPA. |
|
||||
| `aap/smoke/deny_all` | Always denies. Use it to confirm enforcement is active. |
|
||||
| `aap/policy/decision` | The baseline policy described below. |
|
||||
|
||||
### Baseline policy (`aap/policy/decision`)
|
||||
|
||||
[aap_policy.rego](roles/opa_server/files/policies/aap_policy.rego) reports these violations:
|
||||
|
||||
| Rule | Denies when | Setting in `opa_server_policy_config` |
|
||||
|---|---|---|
|
||||
| Approved registries | The execution environment image doesn't start with an allowed prefix | `allowed_ee_registries` (default: `aap.toal.ca/`, `registry.redhat.io/`) |
|
||||
| No credentials in extra vars | A launch-time extra var matches a forbidden name (case-insensitive) | `forbidden_extra_vars` (default: `ansible_password`, `ansible_become_password`, `ansible_ssh_pass`, `ansible_become_pass`) |
|
||||
| Change freeze | `change_freeze` is true and the job runs in `run` mode (not check mode), unless the launcher is a superuser or in an exempt team | `change_freeze`, `change_freeze_exempt_teams` |
|
||||
|
||||
An empty `allowed_ee_registries` list turns the registry check off.
|
||||
|
||||
Scheduled jobs have no launching user, so a change freeze blocks them too.
|
||||
|
||||
The **Cloud Services Execution Environment** (`quay.io/scottharwell/cloud-ee`) fails the
|
||||
registry rule as configured. That makes it a convenient demo.
|
||||
|
||||
### Attach a policy
|
||||
|
||||
Attach the smoke test first. Then switch to the real policy once AAP shows it reaching OPA.
|
||||
|
||||
1. Confirm AAP can reach OPA. The job should run normally:
|
||||
|
||||
```bash
|
||||
ansible-navigator run playbooks/deploy_opa.yml --mode stdout -e '{
|
||||
"opa_server_aap_query_paths": [
|
||||
{"type": "job_templates", "name": "Demo Job Template", "query_path": "aap/smoke/allow_all"}
|
||||
]}'
|
||||
```
|
||||
|
||||
2. Confirm enforcement. Switch the path to `aap/smoke/deny_all`, then launch the template.
|
||||
The job should fail with
|
||||
`aap.smoke.deny_all: policy enforcement is active`.
|
||||
|
||||
3. Turn on the real policy by switching the path to `aap/policy/decision`.
|
||||
|
||||
`type` is one of `organizations`, `inventories`, or `job_templates`. `name` must match
|
||||
exactly one resource.
|
||||
|
||||
To keep the attachments permanently, put `opa_server_aap_query_paths` in the `aap` host's
|
||||
`host_vars` instead of passing `-e`. You can also set the path by hand on the resource's edit form in the AAP UI, or with the
|
||||
API field `opa_query_path` on `/api/controller/v2/{organizations,inventories,job_templates}/<id>/`.
|
||||
|
||||
### Detach a policy
|
||||
|
||||
Set the query path to an empty string:
|
||||
|
||||
```bash
|
||||
-e '{"opa_server_aap_query_paths": [{"type": "job_templates", "name": "Demo Job Template", "query_path": ""}]}'
|
||||
```
|
||||
|
||||
Removing an entry from `opa_server_aap_query_paths` does **not** detach it. The role only
|
||||
sets the paths it's given.
|
||||
|
||||
### Change the policy config
|
||||
|
||||
Override `opa_server_policy_config` in `host_vars` or with `-e`, then re-run the playbook.
|
||||
For example, to start a change freeze:
|
||||
|
||||
```yaml
|
||||
opa_server_policy_config:
|
||||
allowed_ee_registries: [aap.toal.ca/, registry.redhat.io/]
|
||||
forbidden_extra_vars: [ansible_password, ansible_become_password, ansible_ssh_pass, ansible_become_pass]
|
||||
change_freeze: true
|
||||
change_freeze_exempt_teams: [Platform]
|
||||
```
|
||||
|
||||
Lists replace the defaults rather than merging with them, so give the whole object.
|
||||
|
||||
## Write your own policies
|
||||
|
||||
AAP needs the rule at the query path to return exactly this shape:
|
||||
|
||||
```json
|
||||
{"allowed": true, "violations": []}
|
||||
```
|
||||
|
||||
`violations` is a list of strings shown to the user. It's ignored when `allowed` is true.
|
||||
|
||||
### The input AAP sends
|
||||
|
||||
`input` is the serialized job ([AWX policy.py](https://github.com/ansible/awx/blob/devel/awx/main/tasks/policy.py)):
|
||||
|
||||
| Field | Contents |
|
||||
|---|---|
|
||||
| `id`, `name`, `created`, `job_type` (`run`/`check`), `launch_type`, `launched_by` | Job basics |
|
||||
| `limit`, `forks`, `playbook`, `scm_branch`, `scm_revision`, `hosts_count` | Run details |
|
||||
| `extra_vars` | Launch extra vars (survey passwords masked) |
|
||||
| `created_by` | `{id, username, is_superuser, teams: [{id, name}]}`; `null` for scheduled jobs |
|
||||
| `execution_environment` | `{id, name, image, pull}` |
|
||||
| `inventory` | `{id, name, kind, total_hosts, has_active_failures, inventory_sources, ...}` |
|
||||
| `job_template` | `{id, name, job_type}` |
|
||||
| `organization` | `{id, name}` |
|
||||
| `project` | `{id, name, scm_type, scm_url, scm_branch, ...}` |
|
||||
| `credentials` | `[{id, name, credential_type, kind, cloud, kubernetes, organization, ...}]` |
|
||||
| `labels` | `[{id, name, organization}]` |
|
||||
| `instance_group` | `{id, name, capacity, max_forks, ...}` |
|
||||
| `workflow_job`, `workflow_job_template` | `{id, name, ...}`, or `null` if not run from a workflow |
|
||||
|
||||
### Add a policy
|
||||
|
||||
1. Add a `.rego` file to [roles/opa_server/files/policies/](roles/opa_server/files/policies/).
|
||||
Use `import rego.v1`; OPA 1.x requires Rego v1 syntax. The package determines the query
|
||||
path: `package aap.network` maps to `aap/network/<rule>`.
|
||||
|
||||
```rego
|
||||
package aap.network
|
||||
|
||||
import rego.v1
|
||||
|
||||
decision := {"allowed": count(violations) == 0, "violations": [m | some m in violations]}
|
||||
|
||||
violations contains "Production jobs require a --limit" if {
|
||||
input.inventory.name == "Production"
|
||||
input.limit == ""
|
||||
}
|
||||
```
|
||||
|
||||
2. Add tests to [roles/opa_server/files/tests/](roles/opa_server/files/tests/) and run them:
|
||||
|
||||
```bash
|
||||
podman run --rm -v ./roles/opa_server/files:/files:Z \
|
||||
docker.io/openpolicyagent/opa:1.21.1-static test /files/policies /files/tests -v
|
||||
|
||||
podman run --rm -v ./roles/opa_server/files:/files:Z \
|
||||
docker.io/openpolicyagent/opa:1.21.1-static check --strict /files/policies
|
||||
```
|
||||
|
||||
3. Re-run the playbook. The ConfigMap is replaced in full, so deleted `.rego` files are
|
||||
removed too, and the pod restarts to load the changes.
|
||||
|
||||
Reference data you need in a policy through `data.aap_policy_config`. To read data, define a
|
||||
defaulted rule such as `default user_config := {}` and assign `data.aap_policy_config` to it.
|
||||
Don't use `object.get(data, ...)`: referencing all of `data` from inside a policy is treated
|
||||
as recursion.
|
||||
|
||||
## Configuration reference
|
||||
|
||||
All variables are in [roles/opa_server/defaults/main.yml](roles/opa_server/defaults/main.yml).
|
||||
[meta/argument_specs.yml](roles/opa_server/meta/argument_specs.yml) documents each one.
|
||||
|
||||
| Variable | Default | Notes |
|
||||
|---|---|---|
|
||||
| `opa_server_namespace` | `opa` | |
|
||||
| `opa_server_image` | `docker.io/openpolicyagent/opa:1.21.1-static` | Pin exact versions |
|
||||
| `opa_server_replicas` | `1` | Increase for availability (AAP fails closed) |
|
||||
| `opa_server_port` | `8443` | |
|
||||
| `opa_server_decision_logs` | `true` | Logs inputs, including extra vars |
|
||||
| `opa_server_policy_dir` | `files/policies` in the role | |
|
||||
| `opa_server_policy_config` | see above | Exposed as `data.aap_policy_config` |
|
||||
| `opa_server_vault_path` | `toallab/opa-server` | Mount `kv` |
|
||||
| `opa_server_configure_aap` | `true` | `false` deploys OPA only |
|
||||
| `opa_server_aap_request_timeout` / `_retries` | `1.5` / `2` | AAP's `OPA_REQUEST_TIMEOUT` / `OPA_REQUEST_RETRIES` |
|
||||
| `opa_server_aap_query_paths` | `[]` | Attachments |
|
||||
|
||||
## Operations
|
||||
|
||||
### Rotate the token
|
||||
|
||||
```bash
|
||||
vault kv delete kv/toallab/opa-server
|
||||
ansible-navigator run playbooks/deploy_opa.yml --mode stdout
|
||||
```
|
||||
|
||||
The playbook generates a new token, updates the Secret (which restarts the pod), and updates
|
||||
AAP's `OPA_AUTH_TOKEN`.
|
||||
|
||||
> **Warning:** for a few seconds AAP and OPA hold different tokens. Jobs on resources with a
|
||||
> policy attached fail during that window, so rotate outside busy periods.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
| Symptom on the AAP job | Cause |
|
||||
|---|---|
|
||||
| `Call to OPA failed ... 401` | AAP's token doesn't match the Secret. Re-run the playbook. If that doesn't fix it, delete the Vault key to force a new token. |
|
||||
| `Call to OPA failed ... Connection` or a timeout | The OPA pod is down. Check `oc get pods -n opa`. |
|
||||
| `did not return a "result" property` | The query path doesn't exist. Check the package and rule name, and use `/` or `.` as separators. |
|
||||
| `OPA policy returned invalid result` | The rule doesn't return `{allowed: bool, violations: [string]}`. |
|
||||
| A job runs when it should be blocked | No query path is attached to its organization, inventory, or template, or the rule returned `allowed: true`. |
|
||||
|
||||
See exactly what OPA decided:
|
||||
|
||||
```bash
|
||||
oc logs -n opa deploy/opa | grep '"decision_id"' | tail -5
|
||||
```
|
||||
|
||||
### Remove
|
||||
|
||||
Detach every query path first. Otherwise jobs on those resources fail once OPA is gone.
|
||||
|
||||
```bash
|
||||
# In AAP: Settings → Policy as Code → clear OPA host (an empty host disables evaluation)
|
||||
oc delete namespace opa
|
||||
vault kv metadata delete kv/toallab/opa-server
|
||||
```
|
||||
372
RHOAI_MAAS.md
Normal file
372
RHOAI_MAAS.md
Normal file
@@ -0,0 +1,372 @@
|
||||
# OpenShift AI Models-as-a-Service
|
||||
|
||||
This deploys Red Hat OpenShift AI (RHOAI) 3.5 with **Models-as-a-Service (MaaS)** and
|
||||
publishes the existing `llama-coder` server (llama.cpp running Qwen3.5-9B) through it.
|
||||
|
||||
MaaS puts one authenticated, rate-limited, OpenAI-compatible endpoint in front of the
|
||||
cluster's models:
|
||||
- users get their own API keys;
|
||||
- token quotas apply per user;
|
||||
- access is granted by group.
|
||||
|
||||
Nothing about llama-coder itself changes. Clients that call it directly keep working.
|
||||
|
||||
- Playbook: [playbooks/deploy_rhoai_maas.yml](playbooks/deploy_rhoai_maas.yml)
|
||||
- Role: [roles/rhoai_maas/](roles/rhoai_maas/) (database: [roles/cloudnativepg_operator/](roles/cloudnativepg_operator/))
|
||||
- Official docs: [Govern LLM access with Models-as-a-Service (RHOAI 3.5)](https://docs.redhat.com/en/documentation/red_hat_openshift_ai_self-managed/3.5/pdf/govern_llm_access_with_models-as-a-service/Red_Hat_OpenShift_AI_Self-Managed-3.5-Govern_LLM_access_with_Models-as-a-Service-en-US.pdf),
|
||||
plus the [companion guide](https://github.com/jharmison-redhat/rhoai-maas-guide)
|
||||
|
||||
> **Support status:** MaaS itself is GA in RHOAI 3.5. **External models**, which this uses
|
||||
> to front llama.cpp instead of a KServe `LLMInferenceService`, are **Technology Preview**.
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
client ──HTTPS──► Route maas.apps.openshift.toal.ca (passthrough)
|
||||
(sk-oai-… key) │
|
||||
▼
|
||||
Gateway maas-default-gateway (openshift-ingress, Istio, ClusterIP)
|
||||
│ Authorino: validate API key + MaaSAuthPolicy
|
||||
│ Limitador: enforce MaaSSubscription token limits
|
||||
▼
|
||||
ExternalModel qwen35-9b-coder ──HTTP:8080 + llama-coder API key──►
|
||||
Service llama-coder (llm-coding-assistant) ──► llama.cpp on the desktop GPU node
|
||||
```
|
||||
|
||||
1. A user creates a MaaS API key (prefix `sk-oai-`) for a **subscription**.
|
||||
2. The client sends OpenAI-format requests to `https://maas.apps.openshift.toal.ca/v1/...`,
|
||||
using the `model` field to pick the model.
|
||||
3. The gateway checks three things:
|
||||
- the key is valid;
|
||||
- a **MaaSAuthPolicy** grants the user's groups access to that model;
|
||||
- the user's subscription still has **token quota** left.
|
||||
4. MaaS forwards the request to llama-coder over in-cluster HTTP, adding llama-coder's own API
|
||||
key. Users never see that key.
|
||||
|
||||
### What gets deployed
|
||||
|
||||
| Layer | Resources |
|
||||
|---|---|
|
||||
| Operators (OLM) | Red Hat Connectivity Link 1.4 (Kuadrant, Authorino, Limitador, DNS) in `openshift-operators`, Leader Worker Set in `openshift-lws-operator`, RHOAI `stable-3.5` in `redhat-ods-operator` |
|
||||
| Monitoring | User Workload Monitoring turned on (`cluster-monitoring-config`). MaaS reports *Degraded* without it. |
|
||||
| Gateway | GatewayClass `openshift-default` (makes the cluster install Service Mesh 3/Istio), Certificate `maas-default-gateway-cert` (Let's Encrypt, `maas.apps…` only), Gateway `maas-default-gateway` using it, Route `maas-default-gateway-https` (passthrough) |
|
||||
| Auth and limits | `Kuadrant` and `Authorino` in `kuadrant-system`, with Authorino TLS using a certificate from OpenShift's internal service CA |
|
||||
| Database | Single-instance CloudNativePG cluster `maas-postgres` in `rhoai-maas-db`, plus the `maas-db-config` secret in `redhat-ai-gateway-infra` |
|
||||
| RHOAI | `DSCInitialization default-dsci` and `DataScienceCluster default-dsc` (minimal component set, below), with the dashboard's MaaS and external-models views turned on |
|
||||
| Model | ExternalProvider `llama-coder-provider`, ExternalModel and MaaSModelRef `qwen35-9b-coder` in `llm-coding-assistant` |
|
||||
| Access | MaaSAuthPolicy `llama-coder-access` and MaaSSubscription `llama-coder-default` in `models-as-a-service` |
|
||||
|
||||
**DataScienceCluster components:**
|
||||
- **Managed:** `dashboard`, `kserve`, `aigateway` (with `modelsAsAService`), and `ogx` (Gen AI Studio).
|
||||
- **Removed:** everything else (pipelines, workbenches, model registry, Ray, TrustyAI, MLflow,
|
||||
Feast, Kueue, trainer, training operator).
|
||||
|
||||
### Why an external model, not KServe
|
||||
|
||||
The cluster has one GPU: an RTX 4060 with 8 GB of VRAM and no sharing, already in use by
|
||||
llama-coder. Serving through KServe/vLLM would mean shutting down llama.cpp and dropping to a
|
||||
much smaller model. Using `ExternalModel` with `maas.opendatahub.io/tls: "false"` and
|
||||
`maas.opendatahub.io/port: "8080"` lets MaaS govern the existing server in place.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- OpenShift 4.19.9 or later (the cluster runs 4.22) with cert-manager installed.
|
||||
- The `letsencrypt-production` ClusterIssuer (DNS-01). The role issues a dedicated cert for
|
||||
`maas.apps.openshift.toal.ca`. Don't put the `*.apps` wildcard on the gateway: browsers reuse an
|
||||
HTTP/2 connection for any host its certificate covers, so after you visit MaaS, the console
|
||||
and other `*.apps` sites would be sent to the MaaS gateway and get 404s.
|
||||
- llama-coder is running in `llm-coding-assistant`, with secret `llama-coder-api-key` (key `api-key`).
|
||||
- A `rhoai-deployer` ServiceAccount, provisioned with the permissions in
|
||||
[deploy_openshift.yml](playbooks/deploy_openshift.yml). Re-run this after any change to those
|
||||
permissions:
|
||||
|
||||
```bash
|
||||
ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_service_accounts
|
||||
```
|
||||
|
||||
- Its token is referenced in the inventory at
|
||||
`host_vars/sno.openshift.toal.ca/hashivault_secrets.yml`:
|
||||
|
||||
```yaml
|
||||
vault_rhoai_deployer_token: "{{ lookup('community.hashi_vault.hashi_vault', 'kv/data/toallab/rhoai-deployer:token') }}"
|
||||
```
|
||||
|
||||
## Deploy
|
||||
|
||||
```bash
|
||||
source ~/.venv/ansible/bin/activate
|
||||
op run --env-file=$HOME/.ansible.zshenv -- \
|
||||
ansible-navigator run playbooks/deploy_rhoai_maas.yml --mode stdout
|
||||
```
|
||||
|
||||
A first run takes roughly 15–25 minutes, mostly waiting on operators. It runs these steps in
|
||||
order:
|
||||
|
||||
1. **CloudNativePG:** creates the `maas-postgres` cluster and its `maas` database. The password
|
||||
is generated once and stored in Vault at `kv/cloudnativepg/maas-postgres`.
|
||||
2. **Operators:** installs Connectivity Link, Leader Worker Set, and RHOAI, waiting for each
|
||||
to finish installing.
|
||||
3. **Platform:**
|
||||
- turns on User Workload Monitoring;
|
||||
- creates the GatewayClass, Gateway, and Route;
|
||||
- creates Kuadrant and restarts its operator once Istio exists;
|
||||
- turns on Authorino TLS.
|
||||
4. **Database secret:** builds `maas-db-config` from the CloudNativePG credentials.
|
||||
5. **RHOAI:**
|
||||
- applies the DSCInitialization and DataScienceCluster;
|
||||
- waits for the MaaS resource types to be registered and for `maas-api` to become available;
|
||||
- turns on the dashboard's MaaS views.
|
||||
6. **Models:**
|
||||
- labels the model namespace and API-key secret;
|
||||
- creates the ExternalProvider, ExternalModel, and MaaSModelRef;
|
||||
- applies the auth policy and subscription.
|
||||
|
||||
Each step is safe to re-run. If a run fails, fix the cause and run the playbook again; it
|
||||
continues from where things stand.
|
||||
|
||||
### Verify the deployment
|
||||
|
||||
```bash
|
||||
oc get datasciencecluster default-dsc -o jsonpath='{.status.phase}{"\n"}' # Ready
|
||||
oc get kuadrant kuadrant -n kuadrant-system \
|
||||
-o jsonpath='{.status.conditions[?(@.type=="Ready")].status}{"\n"}' # True
|
||||
oc get gateway maas-default-gateway -n openshift-ingress # PROGRAMMED True
|
||||
oc get deploy maas-api -n redhat-ai-gateway-infra # 1/1
|
||||
oc get externalprovider,externalmodel,maasmodelref -n llm-coding-assistant
|
||||
oc get maasauthpolicy,maassubscription -n models-as-a-service
|
||||
curl -s -o /dev/null -w '%{http_code}\n' https://maas.apps.openshift.toal.ca/maas-api/health
|
||||
```
|
||||
|
||||
## Use it
|
||||
|
||||
### 1. Get an API key
|
||||
|
||||
You must belong to a group named in a MaaSSubscription. The default subscription covers every
|
||||
authenticated user.
|
||||
|
||||
**From the dashboard:**
|
||||
|
||||
1. Open **OpenShift AI → Gen AI studio → API keys → Create API key**.
|
||||
2. Pick the `llama-coder default` subscription and an expiry (the default is 90 days).
|
||||
3. Copy the key. It's shown **only once**.
|
||||
|
||||
**From the CLI**, using your OpenShift login token:
|
||||
|
||||
```bash
|
||||
MAAS=https://maas.apps.openshift.toal.ca
|
||||
curl -s -X POST "$MAAS/maas-api/v1/api-keys" \
|
||||
-H "Authorization: Bearer $(oc whoami -t)" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "laptop", "expiresIn": "720h", "subscription": "llama-coder-default"}' | jq -r .key
|
||||
```
|
||||
|
||||
An API key records your group memberships **when it is created**. If your groups change,
|
||||
revoke the key and create a new one.
|
||||
|
||||
### 2. Call the model
|
||||
|
||||
The endpoint is OpenAI-compatible. Use the model name `qwen35-9b-coder`.
|
||||
|
||||
```bash
|
||||
export MAAS_URL=https://maas.apps.openshift.toal.ca
|
||||
export MAAS_API_KEY=sk-oai-...
|
||||
|
||||
curl -s "$MAAS_URL/v1/models" -H "Authorization: Bearer $MAAS_API_KEY"
|
||||
|
||||
curl -s "$MAAS_URL/v1/chat/completions" \
|
||||
-H "Authorization: Bearer $MAAS_API_KEY" -H "Content-Type: application/json" \
|
||||
-d '{"model": "qwen35-9b-coder", "messages": [{"role": "user", "content": "Write a haiku about Ansible"}]}'
|
||||
```
|
||||
|
||||
Any OpenAI SDK or coding assistant works with these values:
|
||||
|
||||
| Setting | Value |
|
||||
|---|---|
|
||||
| Base URL | `https://maas.apps.openshift.toal.ca/v1` |
|
||||
| API key | your `sk-oai-…` key |
|
||||
| Model | `qwen35-9b-coder` |
|
||||
|
||||
```python
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(base_url="https://maas.apps.openshift.toal.ca/v1", api_key="sk-oai-...")
|
||||
print(client.chat.completions.create(
|
||||
model="qwen35-9b-coder",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
).choices[0].message.content)
|
||||
```
|
||||
|
||||
### Limits
|
||||
|
||||
- The default subscription allows each user **200,000 tokens per hour**. A user who exceeds
|
||||
it gets HTTP 429 until the hour window resets.
|
||||
- Token limits are counted only for OpenAI Chat Completions requests (`openai-chat`).
|
||||
- llama.cpp handles one model with a 48k-token context and queues requests on a single GPU.
|
||||
MaaS adds governance, not capacity.
|
||||
|
||||
## Administer
|
||||
|
||||
All access settings live in role variables. Change them in the inventory or with `-e`, then
|
||||
re-run the playbook. The role creates and updates resources but never deletes them; see
|
||||
[Remove a model or policy](#remove-a-model-or-policy).
|
||||
|
||||
### Grant access to specific groups
|
||||
|
||||
The OpenShift groups come from the Keycloak OIDC login. For example, to replace "all
|
||||
authenticated users" with two tiers:
|
||||
|
||||
```yaml
|
||||
rhoai_maas_auth_policies:
|
||||
- name: llama-coder-access
|
||||
groups: [developers, platform-admins]
|
||||
model_refs:
|
||||
- {name: qwen35-9b-coder, namespace: llm-coding-assistant}
|
||||
|
||||
rhoai_maas_subscriptions:
|
||||
- name: llama-coder-standard
|
||||
display_name: Standard
|
||||
priority: 10
|
||||
groups: [developers]
|
||||
model_refs:
|
||||
- name: qwen35-9b-coder
|
||||
namespace: llm-coding-assistant
|
||||
tokenRateLimits: [{limit: 100000, window: 1h}]
|
||||
- name: llama-coder-premium
|
||||
display_name: Premium
|
||||
priority: 20 # wins for users in both groups
|
||||
groups: [platform-admins]
|
||||
model_refs:
|
||||
- name: qwen35-9b-coder
|
||||
namespace: llm-coding-assistant
|
||||
tokenRateLimits: [{limit: 1000000, window: 1h}]
|
||||
```
|
||||
|
||||
- An **auth policy** decides *who may call* a model.
|
||||
- A **subscription** decides *how many tokens* they get.
|
||||
|
||||
A user needs both. `users:` (a list of usernames) works alongside `groups:` in both resources.
|
||||
|
||||
### Add another model
|
||||
|
||||
Any OpenAI-compatible endpoint can be added to `rhoai_maas_external_models`. That includes
|
||||
another in-cluster server or a cloud provider. Then reference it from an auth policy and a
|
||||
subscription.
|
||||
|
||||
```yaml
|
||||
rhoai_maas_external_models:
|
||||
- name: qwen35-9b-coder
|
||||
# ... existing entry ...
|
||||
- name: gpt-4o-mini
|
||||
namespace: llm-coding-assistant
|
||||
display_name: GPT-4o mini (OpenAI)
|
||||
provider_name: openai-provider
|
||||
provider_type: openai # openai | anthropic | azure | vertex | bedrock
|
||||
endpoint: api.openai.com # hostname only, no scheme
|
||||
port: 443
|
||||
tls: true
|
||||
api_key_secret: openai-api-key # create first: key "api-key"
|
||||
target_model: gpt-4o-mini
|
||||
api_format: openai-chat
|
||||
path: /v1/chat/completions
|
||||
```
|
||||
|
||||
Rules for model entries:
|
||||
- **Names:** must be DNS-safe and must not collide with an existing Service in the namespace.
|
||||
The controller creates a Service for each provider and model. That's why the provider is
|
||||
called `llama-coder-provider` rather than `llama-coder`.
|
||||
- **Secrets:** the API-key secret must exist in the model's namespace before you run the
|
||||
playbook. The role only adds the `inference.llm-d.ai/ipp-managed` label to it.
|
||||
- **Shared provider limits:** for a cloud provider, every user shares the provider's key, so
|
||||
its rate limits apply to all MaaS users combined.
|
||||
|
||||
### Revoke keys
|
||||
|
||||
Use **Gen AI studio → API keys** in the dashboard. Administrators can revoke one key or all of
|
||||
a user's keys. Revoke a user's keys after removing them from a group, because existing keys
|
||||
keep the old memberships.
|
||||
|
||||
### Remove a model or policy
|
||||
|
||||
The role never deletes resources. Delete them by hand, then remove the entry from the
|
||||
variables:
|
||||
|
||||
```bash
|
||||
oc delete maassubscription <name> -n models-as-a-service
|
||||
oc delete maasauthpolicy <name> -n models-as-a-service
|
||||
oc delete maasmodelref,externalmodel <model> -n llm-coding-assistant
|
||||
oc delete externalprovider <provider> -n llm-coding-assistant
|
||||
```
|
||||
|
||||
## Configuration reference
|
||||
|
||||
All variables are in [roles/rhoai_maas/defaults/main.yml](roles/rhoai_maas/defaults/main.yml).
|
||||
[meta/argument_specs.yml](roles/rhoai_maas/meta/argument_specs.yml) documents each one.
|
||||
|
||||
| Variable | Default | Notes |
|
||||
|---|---|---|
|
||||
| `rhoai_maas_operators` | RHCL `stable-1.4`, LWS `stable-v1.0`, RHOAI `stable-3.5` | RHOAI is pinned because the MaaS settings layout changed between 3.4 and 3.5 |
|
||||
| `rhoai_maas_hostname` | `""` → `maas.<apps domain>` | |
|
||||
| `rhoai_maas_gateway_cert_secret` | `maas-gateway-tls` | Single-host cert in `openshift-ingress`; never the `*.apps` wildcard |
|
||||
| `rhoai_maas_gateway_cert_issuer` | `letsencrypt-production` | ClusterIssuer for that cert; `""` to bring your own secret |
|
||||
| `rhoai_maas_gateway_service_type` | `ClusterIP` | Use `LoadBalancer` if MetalLB is added |
|
||||
| `rhoai_maas_gateway_create_route` | `true` | Passthrough Route on the default router |
|
||||
| `rhoai_maas_db_secret_name` / `_namespace` | `maas-postgres-secret` / `rhoai-maas-db` | Basic-auth secret from CloudNativePG |
|
||||
| `rhoai_maas_db_sslmode` | `require` | |
|
||||
| `rhoai_maas_dsc_managed_components` / `_removed_components` | see above | |
|
||||
| `rhoai_maas_external_models` | llama-coder | |
|
||||
| `rhoai_maas_auth_policies` | all authenticated users | |
|
||||
| `rhoai_maas_subscriptions` | 200k tokens/h per user | |
|
||||
| `rhoai_maas_wait_timeout` | `900` | Seconds, per wait |
|
||||
|
||||
The database is sized in the playbook's `cloudnativepg_operator` variables: 1 instance, 5 Gi,
|
||||
100 connections.
|
||||
|
||||
## Operations
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
| Symptom | Cause and fix |
|
||||
|---|---|
|
||||
| Run hangs at **Wait for MaaS CRDs**, and the DSC shows `ModelsAsAServiceReady=False … Removed` | MaaS isn't turned on. In 3.5 the field is `aigateway.modelsAsAService` (note "AsA"). The older `modelsAsService` spelling is silently dropped by the API server. Check with `oc get dsc default-dsc -o jsonpath='{.spec.components.aigateway}'`. |
|
||||
| Run stops at **Wait for Kuadrant to be ready**, and Kuadrant shows `MissingDependency` | The Kuadrant operator started before Istio existed. The role restarts it automatically. To fix it by hand: `oc delete pod -n openshift-operators -l app=kuadrant,control-plane=controller-manager` |
|
||||
| Gateway not `Programmed` | Check `oc describe gateway maas-default-gateway -n openshift-ingress`. `LoadBalancerPending` and `DNSReady=False` are expected with ClusterIP plus a Route and do no harm. |
|
||||
| `maas-api` never appears | Check `oc get datasciencecluster default-dsc -o yaml` (status conditions) and that `maas-db-config` exists in `redhat-ai-gateway-infra`. |
|
||||
| DSC shows *Degraded*, mentioning monitoring | User Workload Monitoring is off. Check `cluster-monitoring-config`. |
|
||||
| `401` from `/v1/...` | The key is missing, expired, or revoked, or you used an OpenShift token instead of an `sk-oai-` key. |
|
||||
| `403` | No MaaSAuthPolicy grants your groups that model. Check that `model_refs` names and namespaces match. |
|
||||
| `429` | Subscription token limit reached. Wait for the window to reset, or raise `tokenRateLimits`. |
|
||||
| `ExternalProvider` not `Ready` | Check the API-key secret exists with key `api-key`, and that the Service, ServiceEntry, and DestinationRule were created in the model namespace. |
|
||||
| Long prompts fail or are cut off | Requests over 16 KB can be truncated on Service Mesh before 3.3.1. Check the installed version with `oc get csv -n openshift-operators \| grep servicemesh`. |
|
||||
| Model timeouts under load | llama.cpp is serving on one GPU. Check `oc logs -n llm-coding-assistant deploy/llama-coder`. |
|
||||
|
||||
### Usage metrics
|
||||
|
||||
MaaS exports token and request metrics to User Workload Monitoring. Query them under
|
||||
**Observe → Metrics** in the console, or use the dashboard's MaaS views. The full
|
||||
observability stack (Tempo, OpenTelemetry, Loki) isn't deployed here. See phase 7 of the
|
||||
[companion guide](https://github.com/jharmison-redhat/rhoai-maas-guide).
|
||||
|
||||
### Upgrades
|
||||
|
||||
RHOAI follows the `stable-3.5` channel with Automatic approval, so 3.5.z updates install on
|
||||
their own. Moving to 3.6 or later means changing the channel in `rhoai_maas_operators`,
|
||||
after checking the release notes. MaaS settings have changed between minor versions before:
|
||||
3.4 configured MaaS under `kserve`, 3.5 moved it to `aigateway`.
|
||||
|
||||
### Remove
|
||||
|
||||
Removing MaaS deletes the database and every API key. Work in this order:
|
||||
|
||||
```bash
|
||||
oc delete maassubscription,maasauthpolicy --all -n models-as-a-service
|
||||
oc delete maasmodelref,externalmodel,externalprovider --all -n llm-coding-assistant
|
||||
oc delete datasciencecluster default-dsc
|
||||
oc delete dscinitialization default-dsci
|
||||
oc delete gateway maas-default-gateway -n openshift-ingress
|
||||
oc delete route maas-default-gateway-https -n openshift-ingress
|
||||
oc delete kuadrant kuadrant -n kuadrant-system
|
||||
oc delete cluster.postgresql.cnpg.io maas-postgres -n rhoai-maas-db
|
||||
# then uninstall the operators (Subscriptions + CSVs) from the console
|
||||
```
|
||||
|
||||
Leave the `openshift-default` GatewayClass in place if anything else uses Gateway API.
|
||||
2
TODO.txt
2
TODO.txt
@@ -1 +1 @@
|
||||
- Replace alvaroaleman.freeipa-client with https://galaxy.ansible.com/freeipa/ansible_freeipa
|
||||
- Setup Grafana to use Keycloak
|
||||
63
ansible-navigator.yml.old
Normal file
63
ansible-navigator.yml.old
Normal file
@@ -0,0 +1,63 @@
|
||||
# cspell:ignore cmdline, workdir
|
||||
---
|
||||
ansible-navigator:
|
||||
ansible:
|
||||
config:
|
||||
help: false
|
||||
# Inventory is set in ansible.cfg. Override at runtime with -i if needed.
|
||||
|
||||
execution-environment:
|
||||
container-engine: podman
|
||||
enabled: true
|
||||
image: aap.toal.ca/ee-demo:latest
|
||||
pull:
|
||||
policy: missing
|
||||
|
||||
environment-variables:
|
||||
pass:
|
||||
- OP_SERVICE_ACCOUNT_TOKEN # 1Password service account (vault)
|
||||
- OP_CONNECT_HOST # 1Password Connect server (alternative)
|
||||
- OP_CONNECT_TOKEN
|
||||
- CONTROLLER_HOST # AAP / AWX controller
|
||||
- CONTROLLER_OAUTH_TOKEN
|
||||
- CONTROLLER_USERNAME
|
||||
- CONTROLLER_PASSWORD
|
||||
- AAP_HOSTNAME # Newer AAP naming (same controller)
|
||||
- AAP_USERNAME
|
||||
- AAP_PASSWORD
|
||||
- SATELLITE_SERVER_URL
|
||||
- SATELLITE_USERNAME
|
||||
- SATELLITE_PASSWORD
|
||||
- SATELLITE_VALIDATE_CERTS
|
||||
- NETBOX_API
|
||||
- NETBOX_API_TOKEN
|
||||
- NETBOX_TOKEN
|
||||
|
||||
# Volume mounts are not merged across config files - all required mounts
|
||||
# must be listed here when a project config is present.
|
||||
volume-mounts:
|
||||
# 1Password SSH agent socket (required for vault-id-from-op-client.sh)
|
||||
- src: "/home/ptoal/.1password/agent.sock"
|
||||
dest: "/root/.1password/agent.sock"
|
||||
options: "Z"
|
||||
# Ansible utilities
|
||||
- src: "/home/ptoal/.ansible/utils/"
|
||||
dest: "/root/.ansible/utils"
|
||||
options: "Z"
|
||||
# Project-local collections (toallab.infra and others not in the EE image)
|
||||
- src: "collections"
|
||||
dest: "/runner/project/collections"
|
||||
options: "Z"
|
||||
- src: "~/.kube/config"
|
||||
dest: "/root/.kube/config"
|
||||
options: "ro"
|
||||
|
||||
|
||||
logging:
|
||||
level: warning
|
||||
file: /tmp/ansible-navigator.log
|
||||
|
||||
mode: stdout
|
||||
|
||||
playbook-artifact:
|
||||
enable: false
|
||||
46
ansible.cfg
Normal file
46
ansible.cfg
Normal file
@@ -0,0 +1,46 @@
|
||||
[defaults]
|
||||
# Inventory - override with -i or ANSIBLE_INVENTORY env var
|
||||
inventory = /home/ptoal/Dev/inventories/toallab-inventory/static.yml
|
||||
jinja2_native = True
|
||||
|
||||
|
||||
# Role and collection paths
|
||||
roles_path = roles
|
||||
collections_path = ./collections:/workspaces/collections:~/.ansible/collections:/usr/share/ansible/collections
|
||||
|
||||
# Interpreter discovery
|
||||
interpreter_python = auto_silent
|
||||
|
||||
# Performance
|
||||
gathering = smart
|
||||
fact_caching = jsonfile
|
||||
fact_caching_connection = /tmp/ansible_fact_cache
|
||||
fact_caching_timeout = 3600
|
||||
|
||||
# Output
|
||||
stdout_callback = yaml
|
||||
bin_ansible_callbacks = True
|
||||
callbacks_enabled = profile_tasks
|
||||
|
||||
# SSH settings
|
||||
host_key_checking = False
|
||||
timeout = 30
|
||||
|
||||
# Vault
|
||||
vault_password_file = vault-id-from-op-client.sh
|
||||
|
||||
# Misc
|
||||
retry_files_enabled = False
|
||||
nocows = True
|
||||
|
||||
[inventory]
|
||||
# Enable inventory plugins
|
||||
enable_plugins = host_list, yaml, ini, auto, toml
|
||||
|
||||
[privilege_escalation]
|
||||
become = False
|
||||
become_method = sudo
|
||||
|
||||
[ssh_connection]
|
||||
pipelining = True
|
||||
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no
|
||||
10
collections/requirements.old
Normal file
10
collections/requirements.old
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
collections:
|
||||
- name: davidban77.gns3
|
||||
- name: netbox.netbox
|
||||
- name: freeipa.ansible_freeipa
|
||||
- name: redhat.satellite
|
||||
- name: community.general
|
||||
- name: redhat.satellite
|
||||
- name: community.crypto
|
||||
- name: onepassword.connect
|
||||
@@ -1,10 +1,16 @@
|
||||
---
|
||||
collections:
|
||||
- name: davidban77.gns3
|
||||
- name: community.general
|
||||
- name: community.proxmox
|
||||
- name: community.crypto
|
||||
- name: netbox.netbox
|
||||
- name: freeipa.ansible_freeipa
|
||||
- name: redhat.satellite
|
||||
- name: community.general
|
||||
- name: redhat.satellite
|
||||
- name: community.crypto
|
||||
- name: onepassword.connect
|
||||
- name: davidban77.gns3
|
||||
- name: oxlorg.opnsense
|
||||
source: https://github.com/O-X-L/ansible-opnsense
|
||||
type: git
|
||||
version: latest
|
||||
- name: middleware_automation.keycloak
|
||||
- name: infra.aap_configuration
|
||||
|
||||
54
docs/summaries/2026-02-26-aap-keycloak-oidc.md
Normal file
54
docs/summaries/2026-02-26-aap-keycloak-oidc.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Session Summary: AAP Keycloak OIDC Configuration
|
||||
Date: 2026-02-26
|
||||
|
||||
## Work Done
|
||||
Added Keycloak OIDC authentication support for AAP 2.6 using the correct approach:
|
||||
`infra.aap_configuration.gateway_authenticators` (AAP Gateway API) instead of CR extra_settings (wrong for 2.6).
|
||||
|
||||
## Files Changed
|
||||
- `collections/requirements.yml` — Added `infra.aap_configuration`
|
||||
- `playbooks/deploy_aap.yml` — Full rewrite:
|
||||
- Play 0 (`aap_configure_keycloak`): Creates Keycloak OIDC client with correct callback URI `/accounts/profile/callback/`
|
||||
- Play 1: Unchanged (installs AAP via `aap_operator` role)
|
||||
- Play 2 (`aap_configure_oidc`): Fetches admin password from K8s secret, calls `infra.aap_configuration.gateway_authenticators`
|
||||
- `roles/aap_operator/defaults/main.yml` — Removed OIDC vars (not role responsibility)
|
||||
- `roles/aap_operator/meta/argument_specs.yml` — Removed OIDC var docs
|
||||
- `roles/aap_operator/tasks/main.yml` — Removed OIDC include task (was wrong approach)
|
||||
- `roles/aap_operator/tasks/configure_oidc.yml` — Replaced with redirect comment
|
||||
|
||||
## Key Decisions
|
||||
- **OIDC must be configured via AAP Gateway API** (not CR extra_settings). AAP 2.5+ Gateway uses Django-based auth with `ansible_base.authentication` plugins.
|
||||
- **authenticator type**: `ansible_base.authentication.authenticator_plugins.generic_oidc`
|
||||
- **Callback URL**: `{aap_gateway_url}/accounts/profile/callback/` (not `/social/complete/oidc/`)
|
||||
- **Admin password**: Fetched dynamically from K8s secret `{platform_name}-admin-password` (not stored separately in vault)
|
||||
- **OIDC not in `aap_operator` role**: Kept as a separate playbook play (post-install concern)
|
||||
|
||||
## Variables Required in `aap` host_vars
|
||||
```yaml
|
||||
aap_gateway_url: "https://aap.apps.<cluster>.<domain>"
|
||||
aap_oidc_issuer: "https://keycloak.toal.ca/realms/<realm>"
|
||||
aap_oidc_client_id: aap # optional, default: aap
|
||||
```
|
||||
|
||||
## Vault Variables
|
||||
```
|
||||
vault_aap_oidc_client_secret — OIDC client secret from Keycloak
|
||||
vault_aap_deployer_token — K8s SA token (already required)
|
||||
vault_keycloak_admin_password — required for Play 0
|
||||
```
|
||||
|
||||
## Usage
|
||||
```bash
|
||||
# Step 1: Create Keycloak client (once, idempotent)
|
||||
ansible-navigator run playbooks/deploy_aap.yml --tags aap_configure_keycloak
|
||||
|
||||
# Step 2: Deploy AAP
|
||||
ansible-navigator run playbooks/deploy_aap.yml
|
||||
|
||||
# Step 3: Register OIDC authenticator in AAP Gateway
|
||||
ansible-navigator run playbooks/deploy_aap.yml --tags aap_configure_oidc
|
||||
```
|
||||
|
||||
## Open Items
|
||||
- ASSUMED: `infra.aap_configuration` + its dependency `ansible.platform` are available or installable in `aap.toal.ca/ee-demo:latest`. If not, a custom EE rebuild is needed.
|
||||
- The `aap-deployer` SA has `get` on secrets in `aap` namespace — confirmed via RBAC in `deploy_openshift.yml` Play 9.
|
||||
@@ -1,718 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: dnsmadeeasy
|
||||
version_added: "1.3"
|
||||
short_description: Interface with dnsmadeeasy.com (a DNS hosting service).
|
||||
description:
|
||||
- >
|
||||
Manages DNS records via the v2 REST API of the DNS Made Easy service. It handles records only; there is no manipulation of domains or
|
||||
monitor/account support yet. See: U(https://www.dnsmadeeasy.com/integration/restapi/)
|
||||
options:
|
||||
account_key:
|
||||
description:
|
||||
- Account API Key.
|
||||
required: true
|
||||
|
||||
account_secret:
|
||||
description:
|
||||
- Account Secret Key.
|
||||
required: true
|
||||
|
||||
domain:
|
||||
description:
|
||||
- Domain to work with. Can be the domain name (e.g. "mydomain.com") or the numeric ID of the domain in DNS Made Easy (e.g. "839989") for faster
|
||||
resolution
|
||||
required: true
|
||||
|
||||
sandbox:
|
||||
description:
|
||||
- Decides if the sandbox API should be used. Otherwise (default) the production API of DNS Made Easy is used.
|
||||
type: bool
|
||||
default: 'no'
|
||||
version_added: 2.7
|
||||
|
||||
record_name:
|
||||
description:
|
||||
- Record name to get/create/delete/update. If record_name is not specified; all records for the domain will be returned in "result" regardless
|
||||
of the state argument.
|
||||
|
||||
record_type:
|
||||
description:
|
||||
- Record type.
|
||||
choices: [ 'A', 'AAAA', 'CNAME', 'ANAME', 'HTTPRED', 'MX', 'NS', 'PTR', 'SRV', 'TXT' ]
|
||||
|
||||
record_value:
|
||||
description:
|
||||
- >
|
||||
Record value. HTTPRED: <redirection URL>, MX: <priority> <target name>, NS: <name server>, PTR: <target name>,
|
||||
SRV: <priority> <weight> <port> <target name>, TXT: <text value>"
|
||||
- >
|
||||
If record_value is not specified; no changes will be made and the record will be returned in 'result'
|
||||
(in other words, this module can be used to fetch a record's current id, type, and ttl)
|
||||
|
||||
record_ttl:
|
||||
description:
|
||||
- record's "Time to live". Number of seconds the record remains cached in DNS servers.
|
||||
default: 1800
|
||||
|
||||
state:
|
||||
description:
|
||||
- whether the record should exist or not
|
||||
required: true
|
||||
choices: [ 'present', 'absent' ]
|
||||
|
||||
validate_certs:
|
||||
description:
|
||||
- If C(no), SSL certificates will not be validated. This should only be used
|
||||
on personally controlled sites using self-signed certificates.
|
||||
type: bool
|
||||
default: 'yes'
|
||||
version_added: 1.5.1
|
||||
|
||||
monitor:
|
||||
description:
|
||||
- If C(yes), add or change the monitor. This is applicable only for A records.
|
||||
type: bool
|
||||
default: 'no'
|
||||
version_added: 2.4
|
||||
|
||||
systemDescription:
|
||||
description:
|
||||
- Description used by the monitor.
|
||||
required: true
|
||||
default: ''
|
||||
version_added: 2.4
|
||||
|
||||
maxEmails:
|
||||
description:
|
||||
- Number of emails sent to the contact list by the monitor.
|
||||
required: true
|
||||
default: 1
|
||||
version_added: 2.4
|
||||
|
||||
protocol:
|
||||
description:
|
||||
- Protocol used by the monitor.
|
||||
required: true
|
||||
default: 'HTTP'
|
||||
choices: ['TCP', 'UDP', 'HTTP', 'DNS', 'SMTP', 'HTTPS']
|
||||
version_added: 2.4
|
||||
|
||||
port:
|
||||
description:
|
||||
- Port used by the monitor.
|
||||
required: true
|
||||
default: 80
|
||||
version_added: 2.4
|
||||
|
||||
sensitivity:
|
||||
description:
|
||||
- Number of checks the monitor performs before a failover occurs where Low = 8, Medium = 5,and High = 3.
|
||||
required: true
|
||||
default: 'Medium'
|
||||
choices: ['Low', 'Medium', 'High']
|
||||
version_added: 2.4
|
||||
|
||||
contactList:
|
||||
description:
|
||||
- Name or id of the contact list that the monitor will notify.
|
||||
- The default C('') means the Account Owner.
|
||||
required: true
|
||||
default: ''
|
||||
version_added: 2.4
|
||||
|
||||
httpFqdn:
|
||||
description:
|
||||
- The fully qualified domain name used by the monitor.
|
||||
version_added: 2.4
|
||||
|
||||
httpFile:
|
||||
description:
|
||||
- The file at the Fqdn that the monitor queries for HTTP or HTTPS.
|
||||
version_added: 2.4
|
||||
|
||||
httpQueryString:
|
||||
description:
|
||||
- The string in the httpFile that the monitor queries for HTTP or HTTPS.
|
||||
version_added: 2.4
|
||||
|
||||
failover:
|
||||
description:
|
||||
- If C(yes), add or change the failover. This is applicable only for A records.
|
||||
type: bool
|
||||
default: 'no'
|
||||
version_added: 2.4
|
||||
|
||||
autoFailover:
|
||||
description:
|
||||
- If true, fallback to the primary IP address is manual after a failover.
|
||||
- If false, fallback to the primary IP address is automatic after a failover.
|
||||
type: bool
|
||||
default: 'no'
|
||||
version_added: 2.4
|
||||
|
||||
ip1:
|
||||
description:
|
||||
- Primary IP address for the failover.
|
||||
- Required if adding or changing the monitor or failover.
|
||||
version_added: 2.4
|
||||
|
||||
ip2:
|
||||
description:
|
||||
- Secondary IP address for the failover.
|
||||
- Required if adding or changing the failover.
|
||||
version_added: 2.4
|
||||
|
||||
ip3:
|
||||
description:
|
||||
- Tertiary IP address for the failover.
|
||||
version_added: 2.4
|
||||
|
||||
ip4:
|
||||
description:
|
||||
- Quaternary IP address for the failover.
|
||||
version_added: 2.4
|
||||
|
||||
ip5:
|
||||
description:
|
||||
- Quinary IP address for the failover.
|
||||
version_added: 2.4
|
||||
|
||||
notes:
|
||||
- The DNS Made Easy service requires that machines interacting with the API have the proper time and timezone set. Be sure you are within a few
|
||||
seconds of actual time by using NTP.
|
||||
- This module returns record(s) and monitor(s) in the "result" element when 'state' is set to 'present'.
|
||||
These values can be be registered and used in your playbooks.
|
||||
- Only A records can have a monitor or failover.
|
||||
- To add failover, the 'failover', 'autoFailover', 'port', 'protocol', 'ip1', and 'ip2' options are required.
|
||||
- To add monitor, the 'monitor', 'port', 'protocol', 'maxEmails', 'systemDescription', and 'ip1' options are required.
|
||||
- The monitor and the failover will share 'port', 'protocol', and 'ip1' options.
|
||||
|
||||
requirements: [ hashlib, hmac ]
|
||||
author: "Brice Burgess (@briceburg)"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# fetch my.com domain records
|
||||
- dnsmadeeasy:
|
||||
account_key: key
|
||||
account_secret: secret
|
||||
domain: my.com
|
||||
state: present
|
||||
register: response
|
||||
|
||||
# create / ensure the presence of a record
|
||||
- dnsmadeeasy:
|
||||
account_key: key
|
||||
account_secret: secret
|
||||
domain: my.com
|
||||
state: present
|
||||
record_name: test
|
||||
record_type: A
|
||||
record_value: 127.0.0.1
|
||||
|
||||
# update the previously created record
|
||||
- dnsmadeeasy:
|
||||
account_key: key
|
||||
account_secret: secret
|
||||
domain: my.com
|
||||
state: present
|
||||
record_name: test
|
||||
record_value: 192.0.2.23
|
||||
|
||||
# fetch a specific record
|
||||
- dnsmadeeasy:
|
||||
account_key: key
|
||||
account_secret: secret
|
||||
domain: my.com
|
||||
state: present
|
||||
record_name: test
|
||||
register: response
|
||||
|
||||
# delete a record / ensure it is absent
|
||||
- dnsmadeeasy:
|
||||
account_key: key
|
||||
account_secret: secret
|
||||
domain: my.com
|
||||
record_type: A
|
||||
state: absent
|
||||
record_name: test
|
||||
|
||||
# Add a failover
|
||||
- dnsmadeeasy:
|
||||
account_key: key
|
||||
account_secret: secret
|
||||
domain: my.com
|
||||
state: present
|
||||
record_name: test
|
||||
record_type: A
|
||||
record_value: 127.0.0.1
|
||||
failover: True
|
||||
ip1: 127.0.0.2
|
||||
ip2: 127.0.0.3
|
||||
|
||||
- dnsmadeeasy:
|
||||
account_key: key
|
||||
account_secret: secret
|
||||
domain: my.com
|
||||
state: present
|
||||
record_name: test
|
||||
record_type: A
|
||||
record_value: 127.0.0.1
|
||||
failover: True
|
||||
ip1: 127.0.0.2
|
||||
ip2: 127.0.0.3
|
||||
ip3: 127.0.0.4
|
||||
ip4: 127.0.0.5
|
||||
ip5: 127.0.0.6
|
||||
|
||||
# Add a monitor
|
||||
- dnsmadeeasy:
|
||||
account_key: key
|
||||
account_secret: secret
|
||||
domain: my.com
|
||||
state: present
|
||||
record_name: test
|
||||
record_type: A
|
||||
record_value: 127.0.0.1
|
||||
monitor: yes
|
||||
ip1: 127.0.0.2
|
||||
protocol: HTTP # default
|
||||
port: 80 # default
|
||||
maxEmails: 1
|
||||
systemDescription: Monitor Test A record
|
||||
contactList: my contact list
|
||||
|
||||
# Add a monitor with http options
|
||||
- dnsmadeeasy:
|
||||
account_key: key
|
||||
account_secret: secret
|
||||
domain: my.com
|
||||
state: present
|
||||
record_name: test
|
||||
record_type: A
|
||||
record_value: 127.0.0.1
|
||||
monitor: yes
|
||||
ip1: 127.0.0.2
|
||||
protocol: HTTP # default
|
||||
port: 80 # default
|
||||
maxEmails: 1
|
||||
systemDescription: Monitor Test A record
|
||||
contactList: 1174 # contact list id
|
||||
httpFqdn: http://my.com
|
||||
httpFile: example
|
||||
httpQueryString: some string
|
||||
|
||||
# Add a monitor and a failover
|
||||
- dnsmadeeasy:
|
||||
account_key: key
|
||||
account_secret: secret
|
||||
domain: my.com
|
||||
state: present
|
||||
record_name: test
|
||||
record_type: A
|
||||
record_value: 127.0.0.1
|
||||
failover: True
|
||||
ip1: 127.0.0.2
|
||||
ip2: 127.0.0.3
|
||||
monitor: yes
|
||||
protocol: HTTPS
|
||||
port: 443
|
||||
maxEmails: 1
|
||||
systemDescription: monitoring my.com status
|
||||
contactList: emergencycontacts
|
||||
|
||||
# Remove a failover
|
||||
- dnsmadeeasy:
|
||||
account_key: key
|
||||
account_secret: secret
|
||||
domain: my.com
|
||||
state: present
|
||||
record_name: test
|
||||
record_type: A
|
||||
record_value: 127.0.0.1
|
||||
failover: no
|
||||
|
||||
# Remove a monitor
|
||||
- dnsmadeeasy:
|
||||
account_key: key
|
||||
account_secret: secret
|
||||
domain: my.com
|
||||
state: present
|
||||
record_name: test
|
||||
record_type: A
|
||||
record_value: 127.0.0.1
|
||||
monitor: no
|
||||
'''
|
||||
|
||||
# ============================================
|
||||
# DNSMadeEasy module specific support methods.
|
||||
#
|
||||
|
||||
import json
|
||||
import hashlib
|
||||
import hmac
|
||||
from time import strftime, gmtime
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
||||
from ansible.module_utils.six import string_types
|
||||
|
||||
|
||||
class DME2(object):
|
||||
|
||||
def __init__(self, apikey, secret, domain, sandbox, module):
|
||||
self.module = module
|
||||
|
||||
self.api = apikey
|
||||
self.secret = secret
|
||||
|
||||
if sandbox:
|
||||
self.baseurl = 'https://api.sandbox.dnsmadeeasy.com/V2.0/'
|
||||
self.module.warn(warning="Sandbox is enabled. All actions are made against the URL %s" % self.baseurl)
|
||||
else:
|
||||
self.baseurl = 'https://api.dnsmadeeasy.com/V2.0/'
|
||||
|
||||
self.domain = str(domain)
|
||||
self.domain_map = None # ["domain_name"] => ID
|
||||
self.record_map = None # ["record_name"] => ID
|
||||
self.records = None # ["record_ID"] => <record>
|
||||
self.all_records = None
|
||||
self.contactList_map = None # ["contactList_name"] => ID
|
||||
|
||||
# Lookup the domain ID if passed as a domain name vs. ID
|
||||
if not self.domain.isdigit():
|
||||
self.domain = self.getDomainByName(self.domain)['id']
|
||||
|
||||
self.record_url = 'dns/managed/' + str(self.domain) + '/records'
|
||||
self.monitor_url = 'monitor'
|
||||
self.contactList_url = 'contactList'
|
||||
|
||||
def _headers(self):
|
||||
currTime = self._get_date()
|
||||
hashstring = self._create_hash(currTime)
|
||||
headers = {'x-dnsme-apiKey': self.api,
|
||||
'x-dnsme-hmac': hashstring,
|
||||
'x-dnsme-requestDate': currTime,
|
||||
'content-type': 'application/json'}
|
||||
return headers
|
||||
|
||||
def _get_date(self):
|
||||
return strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime())
|
||||
|
||||
def _create_hash(self, rightnow):
|
||||
return hmac.new(self.secret.encode(), rightnow.encode(), hashlib.sha1).hexdigest()
|
||||
|
||||
def query(self, resource, method, data=None):
|
||||
url = self.baseurl + resource
|
||||
if data and not isinstance(data, string_types):
|
||||
data = urlencode(data)
|
||||
|
||||
response, info = fetch_url(self.module, url, data=data, method=method, headers=self._headers())
|
||||
if info['status'] not in (200, 201, 204):
|
||||
self.module.fail_json(msg="%s returned %s, with body: %s" % (url, info['status'], info['msg']))
|
||||
|
||||
try:
|
||||
return json.load(response)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def getDomain(self, domain_id):
|
||||
if not self.domain_map:
|
||||
self._instMap('domain')
|
||||
|
||||
return self.domains.get(domain_id, False)
|
||||
|
||||
def getDomainByName(self, domain_name):
|
||||
if not self.domain_map:
|
||||
self._instMap('domain')
|
||||
|
||||
return self.getDomain(self.domain_map.get(domain_name, 0))
|
||||
|
||||
def getDomains(self):
|
||||
return self.query('dns/managed', 'GET')['data']
|
||||
|
||||
def getRecord(self, record_id):
|
||||
if not self.record_map:
|
||||
self._instMap('record')
|
||||
|
||||
return self.records.get(record_id, False)
|
||||
|
||||
# Try to find a single record matching this one.
|
||||
# How we do this depends on the type of record. For instance, there
|
||||
# can be several MX records for a single record_name while there can
|
||||
# only be a single CNAME for a particular record_name. Note also that
|
||||
# there can be several records with different types for a single name.
|
||||
def getMatchingRecord(self, record_name, record_type, record_value):
|
||||
# Get all the records if not already cached
|
||||
if not self.all_records:
|
||||
self.all_records = self.getRecords()
|
||||
|
||||
if record_type in ["CNAME", "ANAME", "HTTPRED", "PTR"]:
|
||||
for result in self.all_records:
|
||||
if result['name'] == record_name and result['type'] == record_type:
|
||||
return result
|
||||
return False
|
||||
elif record_type in ["A", "AAAA", "MX", "NS", "TXT", "SRV"]:
|
||||
for result in self.all_records:
|
||||
if record_type == "MX":
|
||||
value = record_value.split(" ")[1]
|
||||
elif record_type == "SRV":
|
||||
value = record_value.split(" ")[3]
|
||||
else:
|
||||
value = record_value
|
||||
if result['name'] == record_name and result['type'] == record_type and result['value'] == value:
|
||||
return result
|
||||
return False
|
||||
else:
|
||||
raise Exception('record_type not yet supported')
|
||||
|
||||
def getRecords(self):
|
||||
return self.query(self.record_url, 'GET')['data']
|
||||
|
||||
def _instMap(self, type):
|
||||
# @TODO cache this call so it's executed only once per ansible execution
|
||||
map = {}
|
||||
results = {}
|
||||
|
||||
# iterate over e.g. self.getDomains() || self.getRecords()
|
||||
for result in getattr(self, 'get' + type.title() + 's')():
|
||||
|
||||
map[result['name']] = result['id']
|
||||
results[result['id']] = result
|
||||
|
||||
# e.g. self.domain_map || self.record_map
|
||||
setattr(self, type + '_map', map)
|
||||
setattr(self, type + 's', results) # e.g. self.domains || self.records
|
||||
|
||||
def prepareRecord(self, data):
|
||||
return json.dumps(data, separators=(',', ':'))
|
||||
|
||||
def createRecord(self, data):
|
||||
# @TODO update the cache w/ resultant record + id when impleneted
|
||||
return self.query(self.record_url, 'POST', data)
|
||||
|
||||
def updateRecord(self, record_id, data):
|
||||
# @TODO update the cache w/ resultant record + id when impleneted
|
||||
return self.query(self.record_url + '/' + str(record_id), 'PUT', data)
|
||||
|
||||
def deleteRecord(self, record_id):
|
||||
# @TODO remove record from the cache when impleneted
|
||||
return self.query(self.record_url + '/' + str(record_id), 'DELETE')
|
||||
|
||||
def getMonitor(self, record_id):
|
||||
return self.query(self.monitor_url + '/' + str(record_id), 'GET')
|
||||
|
||||
def updateMonitor(self, record_id, data):
|
||||
return self.query(self.monitor_url + '/' + str(record_id), 'PUT', data)
|
||||
|
||||
def prepareMonitor(self, data):
|
||||
return json.dumps(data, separators=(',', ':'))
|
||||
|
||||
def getContactList(self, contact_list_id):
|
||||
if not self.contactList_map:
|
||||
self._instMap('contactList')
|
||||
|
||||
return self.contactLists.get(contact_list_id, False)
|
||||
|
||||
def getContactlists(self):
|
||||
return self.query(self.contactList_url, 'GET')['data']
|
||||
|
||||
def getContactListByName(self, name):
|
||||
if not self.contactList_map:
|
||||
self._instMap('contactList')
|
||||
|
||||
return self.getContactList(self.contactList_map.get(name, 0))
|
||||
|
||||
# ===========================================
|
||||
# Module execution.
|
||||
#
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
account_key=dict(required=True),
|
||||
account_secret=dict(required=True, no_log=True),
|
||||
domain=dict(required=True),
|
||||
sandbox=dict(default='no', type='bool'),
|
||||
state=dict(required=True, choices=['present', 'absent']),
|
||||
record_name=dict(required=False),
|
||||
record_type=dict(required=False, choices=[
|
||||
'A', 'AAAA', 'CNAME', 'ANAME', 'HTTPRED', 'MX', 'NS', 'PTR', 'SRV', 'TXT']),
|
||||
record_value=dict(required=False),
|
||||
record_ttl=dict(required=False, default=1800, type='int'),
|
||||
monitor=dict(default='no', type='bool'),
|
||||
systemDescription=dict(default=''),
|
||||
maxEmails=dict(default=1, type='int'),
|
||||
protocol=dict(default='HTTP', choices=['TCP', 'UDP', 'HTTP', 'DNS', 'SMTP', 'HTTPS']),
|
||||
port=dict(default=80, type='int'),
|
||||
sensitivity=dict(default='Medium', choices=['Low', 'Medium', 'High']),
|
||||
contactList=dict(default=None),
|
||||
httpFqdn=dict(required=False),
|
||||
httpFile=dict(required=False),
|
||||
httpQueryString=dict(required=False),
|
||||
failover=dict(default='no', type='bool'),
|
||||
autoFailover=dict(default='no', type='bool'),
|
||||
ip1=dict(required=False),
|
||||
ip2=dict(required=False),
|
||||
ip3=dict(required=False),
|
||||
ip4=dict(required=False),
|
||||
ip5=dict(required=False),
|
||||
validate_certs=dict(default='yes', type='bool'),
|
||||
),
|
||||
required_together=[
|
||||
['record_value', 'record_ttl', 'record_type']
|
||||
],
|
||||
required_if=[
|
||||
['failover', True, ['autoFailover', 'port', 'protocol', 'ip1', 'ip2']],
|
||||
['monitor', True, ['port', 'protocol', 'maxEmails', 'systemDescription', 'ip1']]
|
||||
]
|
||||
)
|
||||
|
||||
protocols = dict(TCP=1, UDP=2, HTTP=3, DNS=4, SMTP=5, HTTPS=6)
|
||||
sensitivities = dict(Low=8, Medium=5, High=3)
|
||||
|
||||
DME = DME2(module.params["account_key"], module.params[
|
||||
"account_secret"], module.params["domain"], module.params["sandbox"], module)
|
||||
state = module.params["state"]
|
||||
record_name = module.params["record_name"]
|
||||
record_type = module.params["record_type"]
|
||||
record_value = module.params["record_value"]
|
||||
|
||||
# Follow Keyword Controlled Behavior
|
||||
if record_name is None:
|
||||
domain_records = DME.getRecords()
|
||||
if not domain_records:
|
||||
module.fail_json(
|
||||
msg="The requested domain name is not accessible with this api_key; try using its ID if known.")
|
||||
module.exit_json(changed=False, result=domain_records)
|
||||
|
||||
# Fetch existing record + Build new one
|
||||
current_record = DME.getMatchingRecord(record_name, record_type, record_value)
|
||||
new_record = {'name': record_name}
|
||||
for i in ["record_value", "record_type", "record_ttl"]:
|
||||
if not module.params[i] is None:
|
||||
new_record[i[len("record_"):]] = module.params[i]
|
||||
# Special handling for mx record
|
||||
if new_record["type"] == "MX":
|
||||
new_record["mxLevel"] = new_record["value"].split(" ")[0]
|
||||
new_record["value"] = new_record["value"].split(" ")[1]
|
||||
|
||||
# Special handling for SRV records
|
||||
if new_record["type"] == "SRV":
|
||||
new_record["priority"] = new_record["value"].split(" ")[0]
|
||||
new_record["weight"] = new_record["value"].split(" ")[1]
|
||||
new_record["port"] = new_record["value"].split(" ")[2]
|
||||
new_record["value"] = new_record["value"].split(" ")[3]
|
||||
|
||||
# Fetch existing monitor if the A record indicates it should exist and build the new monitor
|
||||
current_monitor = dict()
|
||||
new_monitor = dict()
|
||||
if current_record and current_record['type'] == 'A':
|
||||
current_monitor = DME.getMonitor(current_record['id'])
|
||||
|
||||
# Build the new monitor
|
||||
for i in ['monitor', 'systemDescription', 'protocol', 'port', 'sensitivity', 'maxEmails',
|
||||
'contactList', 'httpFqdn', 'httpFile', 'httpQueryString',
|
||||
'failover', 'autoFailover', 'ip1', 'ip2', 'ip3', 'ip4', 'ip5']:
|
||||
if module.params[i] is not None:
|
||||
if i == 'protocol':
|
||||
# The API requires protocol to be a numeric in the range 1-6
|
||||
new_monitor['protocolId'] = protocols[module.params[i]]
|
||||
elif i == 'sensitivity':
|
||||
# The API requires sensitivity to be a numeric of 8, 5, or 3
|
||||
new_monitor[i] = sensitivities[module.params[i]]
|
||||
elif i == 'contactList':
|
||||
# The module accepts either the name or the id of the contact list
|
||||
contact_list_id = module.params[i]
|
||||
if not contact_list_id.isdigit() and contact_list_id != '':
|
||||
contact_list = DME.getContactListByName(contact_list_id)
|
||||
if not contact_list:
|
||||
module.fail_json(msg="Contact list {0} does not exist".format(contact_list_id))
|
||||
contact_list_id = contact_list.get('id', '')
|
||||
new_monitor['contactListId'] = contact_list_id
|
||||
else:
|
||||
# The module option names match the API field names
|
||||
new_monitor[i] = module.params[i]
|
||||
|
||||
# Compare new record against existing one
|
||||
record_changed = False
|
||||
if current_record:
|
||||
for i in new_record:
|
||||
if str(current_record[i]) != str(new_record[i]):
|
||||
record_changed = True
|
||||
new_record['id'] = str(current_record['id'])
|
||||
|
||||
monitor_changed = False
|
||||
if current_monitor:
|
||||
for i in new_monitor:
|
||||
if str(current_monitor.get(i)) != str(new_monitor[i]):
|
||||
monitor_changed = True
|
||||
|
||||
# Follow Keyword Controlled Behavior
|
||||
if state == 'present':
|
||||
# return the record if no value is specified
|
||||
if "value" not in new_record:
|
||||
if not current_record:
|
||||
module.fail_json(
|
||||
msg="A record with name '%s' does not exist for domain '%s.'" % (record_name, module.params['domain']))
|
||||
module.exit_json(changed=False, result=dict(record=current_record, monitor=current_monitor))
|
||||
|
||||
# create record and monitor as the record does not exist
|
||||
if not current_record:
|
||||
record = DME.createRecord(DME.prepareRecord(new_record))
|
||||
if module.params['monitor']:
|
||||
monitor = DME.updateMonitor(record['id'], DME.prepareMonitor(new_monitor))
|
||||
module.exit_json(changed=True, result=dict(record=record, monitor=monitor))
|
||||
else:
|
||||
module.exit_json(changed=True, result=dict(record=record))
|
||||
|
||||
# update the record
|
||||
updated = False
|
||||
if record_changed:
|
||||
DME.updateRecord(current_record['id'], DME.prepareRecord(new_record))
|
||||
updated = True
|
||||
if monitor_changed:
|
||||
DME.updateMonitor(current_monitor['recordId'], DME.prepareMonitor(new_monitor))
|
||||
updated = True
|
||||
if updated:
|
||||
module.exit_json(changed=True, result=dict(record=new_record, monitor=new_monitor))
|
||||
|
||||
# return the record (no changes)
|
||||
module.exit_json(changed=False, result=dict(record=current_record, monitor=current_monitor))
|
||||
|
||||
elif state == 'absent':
|
||||
changed = False
|
||||
# delete the record (and the monitor/failover) if it exists
|
||||
if current_record:
|
||||
DME.deleteRecord(current_record['id'])
|
||||
module.exit_json(changed=True)
|
||||
|
||||
# record does not exist, return w/o change.
|
||||
module.exit_json(changed=changed)
|
||||
|
||||
else:
|
||||
module.fail_json(
|
||||
msg="'%s' is an unknown value for the state argument" % state)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
- name: Set up IPA Client
|
||||
hosts: tag_ipa_client
|
||||
become: yes
|
||||
become: true
|
||||
collections:
|
||||
- freeipa.ansible_freeipa
|
||||
pre_tasks:
|
||||
@@ -25,15 +25,9 @@
|
||||
state: present
|
||||
when: ansible_os_family == "RedHat"
|
||||
|
||||
- name: Set up Basic Lab Packages
|
||||
hosts: all
|
||||
become: yes
|
||||
roles:
|
||||
- role: toal-common
|
||||
|
||||
- name: Packages
|
||||
hosts: all
|
||||
become: yes
|
||||
become: true
|
||||
tasks:
|
||||
|
||||
- name: Host Packages
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
- name: VM Provisioning
|
||||
hosts: tag_ansible:&tag_tower
|
||||
connection: local
|
||||
collections:
|
||||
- redhat.rhv
|
||||
|
||||
tasks:
|
||||
- block:
|
||||
- name: Obtain SSO token from username / password credentials
|
||||
ovirt_auth:
|
||||
url: "{{ ovirt_url }}"
|
||||
username: "{{ ovirt_username }}"
|
||||
password: "{{ ovirt_password }}"
|
||||
|
||||
- name: Disks Created
|
||||
ovirt_disk:
|
||||
auth: "{{ ovirt_auth }}"
|
||||
description: "Boot Disk for {{ inventory_hostname }}"
|
||||
interface: virtio
|
||||
size: 120GiB
|
||||
storage_domain: nas_iscsi
|
||||
bootable: True
|
||||
wait: true
|
||||
name: "{{ inventory_hostname }}_disk0"
|
||||
state: present
|
||||
|
||||
- name: VM Created
|
||||
ovirt_vm:
|
||||
|
||||
|
||||
|
||||
- name: Add NIC to VM
|
||||
ovirt_nic:
|
||||
state: present
|
||||
vm:
|
||||
name: mynic
|
||||
interface: e1000
|
||||
mac_address: 00:1a:4a:16:01:56
|
||||
profile: ovirtmgmt
|
||||
network: ovirtmgmt
|
||||
|
||||
- name: Plug NIC to VM
|
||||
redhat.rhv.ovirt_nic:
|
||||
state: plugged
|
||||
vm: myvm
|
||||
name: mynic
|
||||
|
||||
|
||||
always:
|
||||
- name: Always revoke the SSO token
|
||||
ovirt_auth:
|
||||
state: absent
|
||||
ovirt_auth: "{{ ovirt_auth }}"
|
||||
|
||||
|
||||
# - name: VM Configuration
|
||||
# - name: Automation Platform Installer
|
||||
# - name:
|
||||
@@ -1,12 +0,0 @@
|
||||
- name: Create an ovirt windows template
|
||||
hosts: windows_template_base
|
||||
gather_facts: false
|
||||
connection: local
|
||||
become: false
|
||||
|
||||
vars:
|
||||
ansible_python_interpreter: "{{ ansible_playbook_python }}"
|
||||
|
||||
|
||||
roles:
|
||||
- oatakan.windows_ovirt_template
|
||||
@@ -1,233 +0,0 @@
|
||||
# Playbook to build new VMs in RHV Cluste
|
||||
# Currently only builds RHEL VMs
|
||||
|
||||
# Create Host
|
||||
|
||||
- name: Preflight checks
|
||||
hosts: tag_build
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- assert:
|
||||
that:
|
||||
- site == "sagely_dc"
|
||||
- is_virtual
|
||||
|
||||
- name: Ensure Primary IP exists and is in DNS
|
||||
hosts: tag_build
|
||||
gather_facts: false
|
||||
collections:
|
||||
- netbox.netbox
|
||||
- freeipa.ansible_freeipa
|
||||
- redhat.rhv
|
||||
|
||||
tasks:
|
||||
|
||||
- name: Obtain SSO token for RHV
|
||||
ovirt_auth:
|
||||
url: "{{ ovirt_url }}"
|
||||
username: "{{ ovirt_username }}"
|
||||
insecure: true
|
||||
password: "{{ ovirt_password }}"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Get unused IP Address from pool
|
||||
netbox_ip_address:
|
||||
netbox_url: "{{ netbox_api }}"
|
||||
netbox_token: "{{ netbox_token }}"
|
||||
data:
|
||||
prefix: 192.168.16.0/20
|
||||
assigned_object:
|
||||
name: eth0
|
||||
virtual_machine: "{{ inventory_hostname }}"
|
||||
state: new
|
||||
register: new_ip
|
||||
when: primary_ip4 is undefined
|
||||
delegate_to: localhost
|
||||
|
||||
- set_fact:
|
||||
primary_ip4: "{{ new_ip.ip_address.address|ipaddr('address') }}"
|
||||
vm_hostname: "{{ inventory_hostname.split('.')[0] }}"
|
||||
vm_domain: "{{ inventory_hostname.split('.',1)[1] }}"
|
||||
delegate_to: localhost
|
||||
when: primary_ip4 is undefined
|
||||
|
||||
- name: Primary IPv4 Assigned in Netbox
|
||||
netbox_virtual_machine:
|
||||
netbox_url: "{{ netbox_api }}"
|
||||
netbox_token: "{{ netbox_token }}"
|
||||
data:
|
||||
primary_ip4: "{{ primary_ip4 }}"
|
||||
name: "{{ inventory_hostname }}"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Primary IPv4 Address
|
||||
debug:
|
||||
var: primary_ip4
|
||||
|
||||
- name: Ensure IP Address in IdM
|
||||
ipadnsrecord:
|
||||
records:
|
||||
- name: "{{ vm_hostname }}"
|
||||
zone_name: "{{ vm_domain }}"
|
||||
record_type: A
|
||||
record_value:
|
||||
- "{{ new_ip.ip_address.address|ipaddr('address') }}"
|
||||
create_reverse: true
|
||||
ipaadmin_password: "{{ ipaadmin_password }}"
|
||||
delegate_to: idm1.mgmt.toal.ca
|
||||
|
||||
- name: Create VMs
|
||||
hosts: tag_build
|
||||
connection: local
|
||||
gather_facts: no
|
||||
collections:
|
||||
- netbox.netbox
|
||||
- redhat.rhv
|
||||
vars:
|
||||
# Workaround to get correct venv python interpreter
|
||||
ansible_python_interpreter: "{{ ansible_playbook_python }}"
|
||||
|
||||
|
||||
tasks:
|
||||
- name: Basic Disk Profile
|
||||
set_fact:
|
||||
vm_disks:
|
||||
- name: '{{ inventory_hostname }}_boot'
|
||||
bootable: true
|
||||
sparse: true
|
||||
descr: '{{ inventory_hostname }} Boot / Root disk'
|
||||
interface: virtio
|
||||
size: '{{ disk|default(40) }}'
|
||||
state: present
|
||||
storage_domain: "{{ rhv_storage_domain }}"
|
||||
activate: true
|
||||
when: vm_disks is not defined
|
||||
|
||||
- name: Create VM Disks
|
||||
ovirt_disk:
|
||||
auth: '{{ ovirt_auth }}'
|
||||
name: '{{ item.name }}'
|
||||
description: '{{ item.descr }}'
|
||||
interface: '{{ item.interface }}'
|
||||
size: '{{ item.size|int * 1024000 }}'
|
||||
state: '{{ item.state }}'
|
||||
sparse: '{{ item.sparse }}'
|
||||
wait: true
|
||||
storage_domain: '{{ item.storage_domain }}'
|
||||
async: 300
|
||||
poll: 15
|
||||
loop: '{{ vm_disks }}'
|
||||
|
||||
- set_fact:
|
||||
nb_query_filter: "slug={{ platform }}"
|
||||
- debug: msg='{{ query("netbox.netbox.nb_lookup", "platforms", api_filter=nb_query_filter, api_endpoint=netbox_api, token=netbox_token)[0].value.name }}'
|
||||
|
||||
- name: Create VM in RHV
|
||||
ovirt_vm:
|
||||
auth: '{{ ovirt_auth }}'
|
||||
name: '{{ inventory_hostname }}'
|
||||
state: present
|
||||
memory: '{{ memory }}MiB'
|
||||
memory_guaranteed: '{{ (memory / 2)|int }}MiB'
|
||||
disks: '{{ vm_disks }}'
|
||||
cpu_cores: '{{ vcpus }}'
|
||||
cluster: '{{ cluster }}'
|
||||
# This is ugly Can we do better?
|
||||
operating_system: '{{ query("netbox.netbox.nb_lookup", "platforms", api_filter=nb_query_filter, api_endpoint=netbox_api, token=netbox_token)[0].value.name }}'
|
||||
type: server
|
||||
graphical_console:
|
||||
protocol:
|
||||
- vnc
|
||||
- spice
|
||||
boot_devices:
|
||||
- hd
|
||||
async: 300
|
||||
poll: 15
|
||||
notify: PXE Boot
|
||||
register: vm_result
|
||||
|
||||
- name: Assign NIC
|
||||
ovirt_nic:
|
||||
auth: '{{ ovirt_auth }}'
|
||||
interface: virtio
|
||||
mac_address: '{{ item.mac_address|default(omit) }}'
|
||||
name: '{{ item.name }}'
|
||||
profile: '{{ item.untagged_vlan.name }}'
|
||||
network: '{{ item.untagged_vlan.name }}' # This is fragile
|
||||
state: '{{ (item.enabled == True) |ternary("plugged","unplugged") }}'
|
||||
linked: yes
|
||||
vm: '{{ inventory_hostname }}'
|
||||
loop: '{{ interfaces }}'
|
||||
register: interface_result
|
||||
|
||||
- debug: var=interface_result
|
||||
|
||||
- name: Host configured in Satellite
|
||||
redhat.satellite.host:
|
||||
username: "{{ satellite_admin_user }}"
|
||||
password: "{{ satellite_admin_pass }}"
|
||||
server_url: "{{ satellite_url }}"
|
||||
name: "{{ inventory_hostname }}"
|
||||
hostgroup: "RHEL8/RHEL8 Sandbox"
|
||||
organization: Toal.ca
|
||||
location: Lab
|
||||
ip: "{{ primary_ip4 }}"
|
||||
mac: "{{ interface_result.results[0].nic.mac.address }}" #fragile
|
||||
build: "{{ vm_result.changed |ternary(true,false) }}"
|
||||
validate_certs: no
|
||||
|
||||
- name: Assign interface MACs to Netbox
|
||||
netbox_vm_interface:
|
||||
netbox_url: "{{ netbox_api }}"
|
||||
netbox_token: "{{ netbox_token }}"
|
||||
data:
|
||||
name: "{{ item.nic.name }}"
|
||||
mac_address: "{{ item.nic.mac.address }}"
|
||||
virtual_machine: "{{ inventory_hostname }}"
|
||||
loop: "{{ interface_result.results }}"
|
||||
|
||||
handlers:
|
||||
- name: PXE Boot
|
||||
ovirt_vm:
|
||||
auth: "{{ ovirt_auth }}"
|
||||
name: "{{ inventory_hostname }}"
|
||||
boot_devices:
|
||||
- network
|
||||
state: running
|
||||
register: vm_build_result
|
||||
|
||||
- name: Ensure VM is running and reachable
|
||||
hosts: tag_build
|
||||
gather_facts: no
|
||||
connection: local
|
||||
collections:
|
||||
- redhat.rhv
|
||||
vars:
|
||||
# Hack to work around virtualenv python interpreter
|
||||
ansible_python_interpreter: "{{ ansible_playbook_python }}"
|
||||
|
||||
tasks:
|
||||
- name: VM is running
|
||||
ovirt_vm:
|
||||
auth: "{{ ovirt_auth }}"
|
||||
name: "{{ inventory_hostname }}"
|
||||
state: running
|
||||
boot_devices:
|
||||
- hd
|
||||
|
||||
- name: Wait for SSH to be ready
|
||||
wait_for_connection:
|
||||
timeout: 1800
|
||||
sleep: 5
|
||||
|
||||
# - name: Ensure IP address is correct in Netbox
|
||||
# netbox_virtual_machine:
|
||||
# data:
|
||||
# name: "{{ inventory_hostname }}"
|
||||
# primary_ip4: "{{ primary_ip4 }}"
|
||||
# netbox_url: "{{ netbox_api }}"
|
||||
# netbox_token: "{{ netbox_token }}"
|
||||
# state: present
|
||||
# delegate_to: localhost
|
||||
|
||||
#TODO: Clear Build tag
|
||||
31
playbooks/create_gitea.yml
Normal file
31
playbooks/create_gitea.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
- name: Create Gitea Server
|
||||
hosts: gitea
|
||||
gather_facts: false
|
||||
|
||||
vars:
|
||||
dnsmadeeasy_hostname: "{{ service_dns_name.split('.') | first }}"
|
||||
dnsmadeeasy_domain: "{{ service_dns_name.split('.',1) |last }}"
|
||||
dnsmadeeasy_record_type: CNAME
|
||||
dnsmadeeasy_record_value: gate.toal.ca.
|
||||
dnsmadeeasy_record_ttl: 600
|
||||
opnsense_service_hostname: "{{ dnsmadeeasy_hostname }}"
|
||||
opnsense_service_domain: "{{ dnsmadeeasy_domain }}"
|
||||
|
||||
tasks:
|
||||
- name: Configure DNS
|
||||
ansible.builtin.import_role:
|
||||
name: toallab.infra.dnsmadeeasy
|
||||
tasks_from: provision.yml
|
||||
|
||||
- name: Configure Service
|
||||
ansible.builtin.import_role:
|
||||
name: toallab.infra.opnsense_service
|
||||
tasks_from: provision.yml
|
||||
module_defaults:
|
||||
group/oxlorg.opnsense.all:
|
||||
firewall: "{{ opnsense_host }}"
|
||||
api_key: "{{ opnsense_api_key }}"
|
||||
api_secret: "{{ opnsense_api_secret }}"
|
||||
ssl_verify: "{{ opnsense_ssl_verify }}"
|
||||
api_port: "{{ opnsense_api_port|default(omit) }}"
|
||||
@@ -1,46 +0,0 @@
|
||||
- name: Publish CVs
|
||||
hosts: satellite1.mgmt.toal.ca
|
||||
vars:
|
||||
sat_env_name: Library
|
||||
sat_org: Toal.ca
|
||||
sat_publish_description: Automated CV Update
|
||||
|
||||
tasks:
|
||||
- name: Pre-tasks | Find all CVs
|
||||
redhat.satellite.resource_info:
|
||||
username: "{{ satellite_admin_user }}"
|
||||
password: "{{ satellite_admin_pass }}"
|
||||
server_url: "{{ satellite_url }}"
|
||||
organization: "{{ sat_org }}"
|
||||
resource: content_views
|
||||
validate_certs: no
|
||||
register: raw_list_cvs
|
||||
|
||||
- name: Pre-tasks | Get resource information
|
||||
set_fact:
|
||||
list_all_cvs: "{{ raw_list_cvs['resources'] | json_query(jmesquery) | list }}"
|
||||
vars:
|
||||
jmesquery: "[*].{name: name, composite: composite, id: id}"
|
||||
|
||||
- name: Pre-tasks | Extract list of content views
|
||||
set_fact:
|
||||
sat6_content_views_list: "{{ sat6_content_views_list|default([]) }} + ['{{ item.name }}' ]"
|
||||
loop: "{{ list_all_cvs | reject('search', 'Default Organization View') | list }}"
|
||||
when: item.composite == false
|
||||
|
||||
- name: Publish content
|
||||
redhat.satellite.content_view_version:
|
||||
username: "{{ satellite_admin_user }}"
|
||||
password: "{{ satellite_admin_pass }}"
|
||||
server_url: "{{ satellite_url }}"
|
||||
organization: "{{ sat_org }}"
|
||||
content_view: "{{ item }}"
|
||||
validate_certs: no
|
||||
description: "{{ sat_publish_description }}"
|
||||
lifecycle_environments:
|
||||
- Library
|
||||
- "{{ sat_env_name }}"
|
||||
loop: "{{ sat6_content_views_list | list }}"
|
||||
loop_control:
|
||||
loop_var: "item"
|
||||
register: cv_publish_sleeper
|
||||
223
playbooks/deploy_aap.yml
Normal file
223
playbooks/deploy_aap.yml
Normal file
@@ -0,0 +1,223 @@
|
||||
---
|
||||
# Deploy Ansible Automation Platform on OpenShift
|
||||
#
|
||||
# Authenticates via the aap-deployer ServiceAccount token (not kubeadmin).
|
||||
# The token is stored in 1Password and loaded via vault_aap_deployer_token.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - OpenShift cluster deployed (deploy_openshift.yml)
|
||||
# - aap-deployer ServiceAccount provisioned:
|
||||
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_service_accounts
|
||||
# - SA token saved to 1Password as vault_aap_deployer_token
|
||||
#
|
||||
# Keycloak OIDC prerequisites (--tags aap_configure_keycloak,aap_configure_oidc):
|
||||
# - Keycloak realm exists (configured via deploy_openshift.yml)
|
||||
# - vault_aap_oidc_client_secret in 1Password (or it will be generated and displayed)
|
||||
# - In host_vars for the aap host:
|
||||
# aap_gateway_url: "https://aap.apps.<cluster>.<domain>"
|
||||
# aap_oidc_client_id: aap
|
||||
# aap_oidc_issuer: "https://keycloak.example.com/realms/<realm>"
|
||||
# aap_oidc_public_key: "<RS256 public key from Keycloak realm Keys tab>"
|
||||
#
|
||||
# Play order:
|
||||
# Play 0: aap_configure_keycloak — Create Keycloak OIDC client for AAP Gateway
|
||||
# Play 1: (default) — Install AAP via aap_operator role
|
||||
# Play 2: aap_configure_oidc — Configure OIDC Authentication Method in AAP Gateway
|
||||
#
|
||||
# Usage:
|
||||
# ansible-navigator run playbooks/deploy_aap.yml
|
||||
# ansible-navigator run playbooks/deploy_aap.yml --tags aap_configure_keycloak
|
||||
# ansible-navigator run playbooks/deploy_aap.yml --tags aap_configure_oidc
|
||||
# ansible-navigator run playbooks/deploy_aap.yml --tags aap_configure_keycloak,aap_configure_oidc
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Play 0: Create Keycloak OIDC client for AAP (optional)
|
||||
# Runs on openshift hosts to access keycloak_url/keycloak_realm host vars.
|
||||
# Creates the OIDC client in Keycloak with the correct AAP Gateway callback URI.
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Configure Keycloak OIDC client for AAP
|
||||
hosts: openshift
|
||||
gather_facts: false
|
||||
connection: local
|
||||
|
||||
tags:
|
||||
- never
|
||||
- aap_configure_keycloak
|
||||
|
||||
vars:
|
||||
__aap_keycloak_api_url: "{{ keycloak_url }}{{ keycloak_context | default('') }}"
|
||||
__aap_oidc_client_id: "{{ aap_oidc_client_id | default('aap') }}"
|
||||
# AAP operator generates the Gateway route as {platform_name}-{namespace}.apps.{cluster}.{domain}
|
||||
# e.g. platform 'aap' in namespace 'aap' → aap-aap.apps.openshift.toal.ca
|
||||
__aap_platform_name: "{{ aap_operator_platform_name | default('aap') }}"
|
||||
__aap_namespace: "{{ aap_operator_namespace | default('aap') }}"
|
||||
# Use custom gateway hostname if set, otherwise fall back to auto-generated route
|
||||
__aap_gateway_host: >-
|
||||
{{ aap_operator_gateway_route_host
|
||||
| default(__aap_platform_name + '-' + __aap_namespace + '.apps.' + ocp_cluster_name + '.' + ocp_base_domain) }}
|
||||
__aap_oidc_redirect_uris:
|
||||
- "https://{{ __aap_gateway_host }}/accounts/profile/callback/"
|
||||
|
||||
module_defaults:
|
||||
middleware_automation.keycloak.keycloak_client:
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: "{{ __aap_keycloak_api_url }}"
|
||||
auth_realm: master
|
||||
auth_username: "{{ keycloak_admin_user }}"
|
||||
auth_password: "{{ vault_keycloak_admin_password }}"
|
||||
validate_certs: "{{ keycloak_validate_certs | default(true) }}"
|
||||
|
||||
tasks:
|
||||
- name: Set AAP OIDC client secret (vault value or generated)
|
||||
ansible.builtin.set_fact:
|
||||
__aap_oidc_client_secret: "{{ vault_aap_oidc_client_secret | default(lookup('community.general.random_string', length=32, special=false)) }}"
|
||||
__aap_oidc_secret_generated: "{{ vault_aap_oidc_client_secret is not defined }}"
|
||||
no_log: true
|
||||
|
||||
- name: Create AAP OIDC client in Keycloak
|
||||
middleware_automation.keycloak.keycloak_client:
|
||||
realm: "{{ keycloak_realm }}"
|
||||
client_id: "{{ __aap_oidc_client_id }}"
|
||||
name: "Ansible Automation Platform"
|
||||
description: "OIDC client for AAP Gateway on {{ ocp_cluster_name }}.{{ ocp_base_domain }}"
|
||||
enabled: true
|
||||
protocol: openid-connect
|
||||
public_client: false
|
||||
standard_flow_enabled: true
|
||||
implicit_flow_enabled: false
|
||||
direct_access_grants_enabled: false
|
||||
service_accounts_enabled: false
|
||||
secret: "{{ __aap_oidc_client_secret }}"
|
||||
redirect_uris: "{{ __aap_oidc_redirect_uris }}"
|
||||
web_origins:
|
||||
- "+"
|
||||
protocol_mappers:
|
||||
- name: groups
|
||||
protocol: openid-connect
|
||||
protocolMapper: oidc-group-membership-mapper
|
||||
config:
|
||||
full.path: "false"
|
||||
id.token.claim: "true"
|
||||
access.token.claim: "true"
|
||||
userinfo.token.claim: "true"
|
||||
claim.name: groups
|
||||
state: present
|
||||
no_log: "{{ keycloak_no_log | default(true) }}"
|
||||
|
||||
- name: Display generated client secret (save this to vault!)
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "*** GENERATED AAP OIDC CLIENT SECRET — SAVE THIS TO VAULT ***"
|
||||
- "vault_aap_oidc_client_secret: {{ __aap_oidc_client_secret }}"
|
||||
- ""
|
||||
- "Save to 1Password and reference as vault_aap_oidc_client_secret."
|
||||
when: __aap_oidc_secret_generated | bool
|
||||
|
||||
- name: Display Keycloak AAP OIDC configuration summary
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Keycloak AAP OIDC client configured:"
|
||||
- " Realm : {{ keycloak_realm }}"
|
||||
- " Client : {{ __aap_oidc_client_id }}"
|
||||
- " Issuer : {{ __aap_keycloak_api_url }}/realms/{{ keycloak_realm }}"
|
||||
- " Redirect : {{ __aap_oidc_redirect_uris | join(', ') }}"
|
||||
- ""
|
||||
- "Set in host_vars for the aap host:"
|
||||
- " aap_gateway_url: https://{{ __aap_gateway_host }}"
|
||||
- " aap_oidc_issuer: {{ __aap_keycloak_api_url }}/realms/{{ keycloak_realm }}"
|
||||
- ""
|
||||
- "Then run: --tags aap_configure_oidc to register the authenticator in AAP."
|
||||
verbosity: 1
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Play 1: Install Ansible Automation Platform
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Install Ansible Automation Platform
|
||||
hosts: aap
|
||||
gather_facts: false
|
||||
connection: local
|
||||
|
||||
pre_tasks:
|
||||
- name: Verify aap-deployer token is available
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- vault_aap_deployer_token is defined
|
||||
- vault_aap_deployer_token | length > 0
|
||||
fail_msg: >-
|
||||
vault_aap_deployer_token is not set. Provision the ServiceAccount with:
|
||||
ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_service_accounts
|
||||
Then save the displayed token to 1Password as vault_aap_deployer_token.
|
||||
|
||||
# environment:
|
||||
# K8S_AUTH_HOST: "{{ aap_k8s_api_url }}"
|
||||
# K8S_AUTH_API_KEY: "{{ vault_aap_deployer_token }}"
|
||||
|
||||
roles:
|
||||
- role: aap_operator
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Play 2: Configure Keycloak OIDC Authentication Method in AAP Gateway (optional)
|
||||
# Uses infra.aap_configuration.gateway_authenticators to register the OIDC
|
||||
# provider via the AAP Gateway API. Run after Play 1 (AAP must be Running).
|
||||
#
|
||||
# Requires in host_vars for the aap host:
|
||||
# aap_gateway_url: "https://aap.apps.<cluster>.<domain>"
|
||||
# aap_oidc_issuer: "https://keycloak.example.com/realms/<realm>"
|
||||
# aap_oidc_client_id: aap (optional, default: aap)
|
||||
# aap_oidc_public_key: "<RS256 public key from Keycloak realm Keys tab>"
|
||||
# Vault:
|
||||
# vault_aap_oidc_client_secret — OIDC client secret from Keycloak
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Configure Keycloak OIDC Authentication in AAP Gateway
|
||||
hosts: aap
|
||||
gather_facts: false
|
||||
connection: local
|
||||
|
||||
tags:
|
||||
- never
|
||||
- aap_configure_oidc
|
||||
|
||||
vars:
|
||||
__aap_namespace: "{{ aap_operator_namespace | default('aap') }}"
|
||||
__aap_platform_name: "{{ aap_operator_platform_name | default('aap') }}"
|
||||
|
||||
environment:
|
||||
K8S_AUTH_HOST: "{{ aap_k8s_api_url }}"
|
||||
K8S_AUTH_API_KEY: "{{ vault_aap_deployer_token }}"
|
||||
|
||||
pre_tasks:
|
||||
- name: Fetch AAP admin password from K8s secret
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: v1
|
||||
kind: Secret
|
||||
namespace: "{{ __aap_namespace }}"
|
||||
name: "{{ __aap_platform_name }}-admin-password"
|
||||
register: __aap_admin_secret
|
||||
no_log: false
|
||||
|
||||
- name: Set AAP admin password fact
|
||||
ansible.builtin.set_fact:
|
||||
__aap_admin_password: "{{ __aap_admin_secret.resources[0].data.password | b64decode }}"
|
||||
no_log: true
|
||||
|
||||
tasks:
|
||||
- name: Configure Keycloak OIDC authenticator in AAP Gateway
|
||||
ansible.builtin.include_role:
|
||||
name: infra.aap_configuration.gateway_authenticators
|
||||
vars:
|
||||
aap_hostname: "{{ aap_gateway_url }}"
|
||||
aap_username: "{{ aap_operator_admin_user | default('admin') }}"
|
||||
aap_password: "{{ __aap_admin_password }}"
|
||||
gateway_authenticators:
|
||||
- name: Keycloak
|
||||
type: ansible_base.authentication.authenticator_plugins.keycloak
|
||||
slug: keycloak
|
||||
enabled: true
|
||||
configuration:
|
||||
KEY: "{{ aap_oidc_client_id | default('aap') }}"
|
||||
SECRET: "{{ vault_aap_oidc_client_secret }}"
|
||||
PUBLIC_KEY: "{{ aap_oidc_public_key }}"
|
||||
ACCESS_TOKEN_URL: "{{ aap_oidc_issuer }}/protocol/openid-connect/token"
|
||||
AUTHORIZATION_URL: "{{ aap_oidc_issuer }}/protocol/openid-connect/auth"
|
||||
GROUPS_CLAIM: "groups"
|
||||
state: present
|
||||
84
playbooks/deploy_automation_orchestrator.yml
Normal file
84
playbooks/deploy_automation_orchestrator.yml
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
# Deploy Automation Orchestrator on OpenShift via its OLM operator, backed by a
|
||||
# CloudNativePG-provisioned PostgreSQL cluster.
|
||||
#
|
||||
# WARNING: CloudNativePG here is a single-instance, operator-provisioned PostgreSQL
|
||||
# cluster intended for demo/dev use — not production. For production, disable the
|
||||
# cloudnativepg_operator role below and point automation_orchestrator_operator's
|
||||
# postgres_* variables at a customer-managed PostgreSQL instance instead.
|
||||
#
|
||||
# Authenticates via the same aap-deployer ServiceAccount token used by deploy_aap.yml
|
||||
# (same cluster). The token is stored in 1Password as vault_aap_deployer_token.
|
||||
#
|
||||
# Modeled on https://github.com/ansible/aapctl (aap-plus install path).
|
||||
#
|
||||
# Prerequisites:
|
||||
# - OpenShift cluster deployed (deploy_openshift.yml)
|
||||
# - aap-deployer ServiceAccount provisioned (re-run after RBAC changes):
|
||||
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_service_accounts
|
||||
# - CloudNativePG uses Manual install-plan approval; the role approves its
|
||||
# InstallPlan. Automation Orchestrator installs from redhat-operators and
|
||||
# adopts an existing operator install when its namespace is set in inventory.
|
||||
# - Pre-release AO builds additionally need a custom CatalogSource and an
|
||||
# ImageDigestMirrorSet (see automation_orchestrator_operator defaults).
|
||||
#
|
||||
# Keycloak OIDC (--tags ao_configure_oidc, run after the install):
|
||||
# - Keycloak settings come from the openshift host (keycloak_url, keycloak_realm,
|
||||
# keycloak_admin_user, vault_keycloak_admin_password).
|
||||
# - automation_orchestrator_oidc_group_mappings in host_vars for the aap host.
|
||||
# - The client secret is generated by Keycloak and read back each run (not vaulted).
|
||||
#
|
||||
# Usage:
|
||||
# ansible-navigator run playbooks/deploy_automation_orchestrator.yml --mode stdout
|
||||
# ansible-navigator run playbooks/deploy_automation_orchestrator.yml --mode stdout --tags ao_configure_oidc
|
||||
|
||||
- name: Install Automation Orchestrator
|
||||
hosts: aap
|
||||
gather_facts: false
|
||||
connection: local
|
||||
|
||||
environment:
|
||||
K8S_AUTH_HOST: "{{ aap_k8s_api_url }}"
|
||||
K8S_AUTH_API_KEY: "{{ vault_aap_deployer_token }}"
|
||||
|
||||
pre_tasks:
|
||||
- name: Verify aap-deployer token is available
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- vault_aap_deployer_token is defined
|
||||
- vault_aap_deployer_token | length > 0
|
||||
fail_msg: >-
|
||||
vault_aap_deployer_token is not set. Provision the ServiceAccount with:
|
||||
ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_service_accounts
|
||||
Then save the displayed token to 1Password as vault_aap_deployer_token.
|
||||
|
||||
roles:
|
||||
- role: cloudnativepg_operator
|
||||
- role: automation_orchestrator_operator
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configure Keycloak OIDC login (optional, run after the install)
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Configure Keycloak OIDC for Automation Orchestrator
|
||||
hosts: aap
|
||||
gather_facts: false
|
||||
connection: local
|
||||
|
||||
tags:
|
||||
- never
|
||||
- ao_configure_oidc
|
||||
|
||||
vars:
|
||||
__ao_keycloak_host: "{{ hostvars[groups['openshift'][0]] }}"
|
||||
automation_orchestrator_oidc_keycloak_url: "{{ __ao_keycloak_host.keycloak_url }}{{ __ao_keycloak_host.keycloak_context | default('') }}"
|
||||
automation_orchestrator_oidc_keycloak_realm: "{{ __ao_keycloak_host.keycloak_realm }}"
|
||||
automation_orchestrator_oidc_keycloak_admin_user: "{{ __ao_keycloak_host.keycloak_admin_user }}"
|
||||
automation_orchestrator_oidc_keycloak_admin_password: "{{ __ao_keycloak_host.vault_keycloak_admin_password }}"
|
||||
automation_orchestrator_oidc_keycloak_validate_certs: "{{ __ao_keycloak_host.keycloak_validate_certs | default(true) }}"
|
||||
|
||||
environment:
|
||||
K8S_AUTH_HOST: "{{ aap_k8s_api_url }}"
|
||||
K8S_AUTH_API_KEY: "{{ vault_aap_deployer_token }}"
|
||||
|
||||
roles:
|
||||
- role: automation_orchestrator_oidc
|
||||
49
playbooks/deploy_opa.yml
Normal file
49
playbooks/deploy_opa.yml
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
# Deploy an Open Policy Agent server on OpenShift and wire it into AAP's
|
||||
# policy enforcement (policy as code).
|
||||
#
|
||||
# Authenticates via the aap-deployer ServiceAccount token used by deploy_aap.yml
|
||||
# (same cluster). The token is stored in 1Password as vault_aap_deployer_token.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - AAP deployed (deploy_aap.yml) and aap_gateway_url set for the aap host
|
||||
# - aap-deployer ServiceAccount RBAC current (re-run after RBAC changes):
|
||||
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_service_accounts
|
||||
#
|
||||
# Policies live in roles/opa_server/files/policies; test them with:
|
||||
# podman run --rm -v ./roles/opa_server/files:/files:Z docker.io/openpolicyagent/opa:1.21.1-static \
|
||||
# test /files/policies /files/tests -v
|
||||
#
|
||||
# Attaching a policy (AAP fails closed — start with aap/smoke/allow_all):
|
||||
# -e '{"opa_server_aap_query_paths": [{"type": "job_templates", "name": "Demo Job Template",
|
||||
# "query_path": "aap/policy/decision"}]}'
|
||||
#
|
||||
# Usage:
|
||||
# ansible-navigator run playbooks/deploy_opa.yml --mode stdout
|
||||
|
||||
- name: Deploy OPA server for AAP policy enforcement
|
||||
hosts: aap
|
||||
gather_facts: false
|
||||
connection: local
|
||||
|
||||
environment:
|
||||
K8S_AUTH_HOST: "{{ aap_k8s_api_url }}"
|
||||
K8S_AUTH_API_KEY: "{{ vault_aap_deployer_token }}"
|
||||
|
||||
pre_tasks:
|
||||
- name: Verify aap-deployer token and AAP URL are available
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- vault_aap_deployer_token is defined
|
||||
- vault_aap_deployer_token | length > 0
|
||||
- aap_gateway_url is defined
|
||||
fail_msg: >-
|
||||
vault_aap_deployer_token or aap_gateway_url is not set. Provision the ServiceAccount with:
|
||||
ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_service_accounts
|
||||
|
||||
roles:
|
||||
- role: opa_server
|
||||
vars:
|
||||
opa_server_aap_namespace: "{{ aap_operator_namespace | default('aap') }}"
|
||||
opa_server_aap_admin_secret: "{{ aap_operator_platform_name | default('aap') }}-admin-password"
|
||||
opa_server_aap_username: "{{ aap_operator_admin_user | default('admin') }}"
|
||||
550
playbooks/deploy_openshift.yml
Normal file
550
playbooks/deploy_openshift.yml
Normal file
@@ -0,0 +1,550 @@
|
||||
---
|
||||
# Deploy and configure Single Node OpenShift (SNO) on Proxmox
|
||||
#
|
||||
# Prerequisites:
|
||||
# ansible-galaxy collection install -r collections/requirements.yml
|
||||
# openshift-install is downloaded automatically during the sno_deploy_install play
|
||||
#
|
||||
# Inventory requirements:
|
||||
# sno.openshift.toal.ca - in 'openshift' group
|
||||
# host_vars: ocp_cluster_name, ocp_base_domain, ocp_version, sno_ip,
|
||||
# sno_gateway, sno_nameserver, sno_prefix_length, sno_machine_network,
|
||||
# sno_vm_name, sno_vnet, sno_storage_ip, sno_storage_ip_prefix_length,
|
||||
# sno_storage_vnet, proxmox_node, keycloak_url, keycloak_realm,
|
||||
# oidc_admin_groups, sno_deploy_letsencrypt_email, ...
|
||||
# secrets: vault_ocp_pull_secret, vault_keycloak_admin_password,
|
||||
# vault_oidc_client_secret (optional)
|
||||
# optional: ocp_kubeconfig (defaults to ~/.kube/config; set to
|
||||
# sno_install_dir/auth/kubeconfig for fresh installs)
|
||||
# proxmox_api - inventory host (ansible_host, ansible_port)
|
||||
# proxmox_host - inventory host (ansible_host, ansible_connection: ssh)
|
||||
# gate.toal.ca - in 'opnsense' group
|
||||
# host_vars: opnsense_host, opnsense_api_key, opnsense_api_secret,
|
||||
# opnsense_api_port, haproxy_public_ip
|
||||
# group_vars/all: dme_account_key, dme_account_secret
|
||||
#
|
||||
# Play order (intentional — DNS must precede VM boot):
|
||||
# Play 1: sno_deploy_vm — Create SNO VM
|
||||
# Play 2: opnsense — Configure OPNsense local DNS overrides
|
||||
# Play 3: dns — Configure public DNS records in DNS Made Easy
|
||||
# Play 4: sno_deploy_install — Generate ISO, boot VM, wait for install
|
||||
# Play 5: keycloak — Configure Keycloak OIDC client
|
||||
# Play 6: sno_deploy_oidc / sno_deploy_certmanager / sno_deploy_delete_kubeadmin
|
||||
# Play 7: sno_deploy_lvms — Install LVM Storage for persistent volumes
|
||||
# Play 8: sno_deploy_nfs — Deploy in-cluster NFS provisioner (RWX StorageClass)
|
||||
# Play 9: sno_deploy_service_accounts — Provision ServiceAccounts for app deployers
|
||||
# Play 10: sno_deploy_alert_tuning — Drop single-control-plane false-positive alerts
|
||||
#
|
||||
# Recurring maintenance (separate playbook, schedule in AAP): maintain_sno_etcd.yml
|
||||
#
|
||||
# AAP deployment is in a separate playbook: deploy_aap.yml
|
||||
#
|
||||
# Usage:
|
||||
# ansible-navigator run playbooks/deploy_openshift.yml
|
||||
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_vm
|
||||
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_install
|
||||
# ansible-navigator run playbooks/deploy_openshift.yml --tags opnsense,dns
|
||||
# ansible-navigator run playbooks/deploy_openshift.yml --tags keycloak,sno_deploy_oidc
|
||||
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_certmanager
|
||||
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_lvms
|
||||
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_nfs
|
||||
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_service_accounts
|
||||
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_alert_tuning
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Play 1: Create SNO VM in Proxmox
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Create SNO VM in Proxmox
|
||||
hosts: sno.openshift.toal.ca
|
||||
gather_facts: false
|
||||
connection: local
|
||||
tags: sno_deploy_vm
|
||||
|
||||
roles:
|
||||
- role: proxmox_vm
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Play 2: Configure OPNsense - Local DNS Overrides
|
||||
# Must run BEFORE booting the VM so that api.openshift.toal.ca resolves
|
||||
# from within the SNO node during bootstrap.
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Configure OPNsense DNS overrides for OpenShift
|
||||
hosts: gate.toal.ca
|
||||
gather_facts: false
|
||||
connection: local
|
||||
|
||||
module_defaults:
|
||||
group/oxlorg.opnsense.all:
|
||||
firewall: "{{ opnsense_host }}"
|
||||
api_key: "{{ opnsense_api_key }}"
|
||||
api_secret: "{{ opnsense_api_secret }}"
|
||||
ssl_verify: "{{ opnsense_ssl_verify | default(false) }}"
|
||||
api_port: "{{ opnsense_api_port | default(omit) }}"
|
||||
|
||||
vars:
|
||||
__deploy_ocp_cluster_name: "{{ hostvars['sno.openshift.toal.ca']['ocp_cluster_name'] }}"
|
||||
__deploy_ocp_base_domain: "{{ hostvars['sno.openshift.toal.ca']['ocp_base_domain'] }}"
|
||||
__deploy_sno_ip: "{{ hostvars['sno.openshift.toal.ca']['sno_ip'] }}"
|
||||
|
||||
tags: opnsense
|
||||
|
||||
roles:
|
||||
- role: opnsense_dns_override
|
||||
opnsense_dns_override_entries:
|
||||
- hostname: "api.{{ __deploy_ocp_cluster_name }}"
|
||||
domain: "{{ __deploy_ocp_base_domain }}"
|
||||
value: "{{ __deploy_sno_ip }}"
|
||||
type: host
|
||||
- hostname: "api-int.{{ __deploy_ocp_cluster_name }}"
|
||||
domain: "{{ __deploy_ocp_base_domain }}"
|
||||
value: "{{ __deploy_sno_ip }}"
|
||||
type: host
|
||||
- domain: "apps.{{ __deploy_ocp_cluster_name }}.{{ __deploy_ocp_base_domain }}"
|
||||
value: "{{ __deploy_sno_ip }}"
|
||||
type: forward
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Play 3: Configure Public DNS Records in DNS Made Easy
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Configure public DNS records for OpenShift
|
||||
hosts: sno.openshift.toal.ca
|
||||
gather_facts: false
|
||||
connection: local
|
||||
|
||||
vars:
|
||||
__deploy_public_ip: "{{ hostvars['gate.toal.ca']['haproxy_public_ip'] }}"
|
||||
|
||||
tags: dns
|
||||
|
||||
roles:
|
||||
- role: dnsmadeeasy_record
|
||||
dnsmadeeasy_record_account_key: "{{ dme_account_key }}"
|
||||
dnsmadeeasy_record_account_secret: "{{ dme_account_secret }}"
|
||||
dnsmadeeasy_record_entries:
|
||||
- domain: "{{ ocp_base_domain }}"
|
||||
record_name: "api.{{ ocp_cluster_name }}"
|
||||
record_type: A
|
||||
record_value: "{{ __deploy_public_ip }}"
|
||||
record_ttl: "{{ ocp_dns_ttl }}"
|
||||
- domain: "{{ ocp_base_domain }}"
|
||||
record_name: "*.apps.{{ ocp_cluster_name }}"
|
||||
record_type: A
|
||||
record_value: "{{ __deploy_public_ip }}"
|
||||
record_ttl: "{{ ocp_dns_ttl }}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Play 4: Generate Agent ISO and Deploy SNO
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Generate Agent ISO and Deploy SNO
|
||||
hosts: sno.openshift.toal.ca
|
||||
gather_facts: false
|
||||
connection: local
|
||||
tags: sno_deploy_install
|
||||
|
||||
tasks:
|
||||
- name: Install SNO
|
||||
ansible.builtin.include_role:
|
||||
name: sno_deploy
|
||||
tasks_from: install.yml
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Play 5: Configure Keycloak OIDC client for OpenShift
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Configure Keycloak OIDC client for OpenShift
|
||||
hosts: openshift
|
||||
gather_facts: false
|
||||
connection: local
|
||||
|
||||
tags: keycloak
|
||||
|
||||
vars:
|
||||
keycloak_context: ""
|
||||
oidc_client_id: openshift
|
||||
oidc_redirect_uri: "https://oauth-openshift.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}/oauth2callback/{{ oidc_provider_name }}"
|
||||
__oidc_keycloak_api_url: "{{ keycloak_url }}{{ keycloak_context }}"
|
||||
|
||||
module_defaults:
|
||||
middleware_automation.keycloak.keycloak_realm:
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: "{{ __oidc_keycloak_api_url }}"
|
||||
auth_realm: master
|
||||
auth_username: "{{ keycloak_admin_user }}"
|
||||
auth_password: "{{ vault_keycloak_admin_password }}"
|
||||
validate_certs: "{{ keycloak_validate_certs | default(true) }}"
|
||||
middleware_automation.keycloak.keycloak_client:
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: "{{ __oidc_keycloak_api_url }}"
|
||||
auth_realm: master
|
||||
auth_username: "{{ keycloak_admin_user }}"
|
||||
auth_password: "{{ vault_keycloak_admin_password }}"
|
||||
validate_certs: "{{ keycloak_validate_certs | default(true) }}"
|
||||
|
||||
tasks:
|
||||
|
||||
- name: Set OIDC client secret (use vault value or generate random)
|
||||
ansible.builtin.set_fact:
|
||||
__oidc_client_secret: "{{ vault_oidc_client_secret | default(lookup('community.general.random_string', length=32, special=false)) }}"
|
||||
__oidc_secret_generated: "{{ vault_oidc_client_secret is not defined }}"
|
||||
no_log: true
|
||||
|
||||
- name: Ensure Keycloak realm exists
|
||||
middleware_automation.keycloak.keycloak_realm:
|
||||
realm: "{{ keycloak_realm }}"
|
||||
id: "{{ keycloak_realm }}"
|
||||
display_name: "{{ keycloak_realm_display_name | default(keycloak_realm | title) }}"
|
||||
enabled: true
|
||||
state: present
|
||||
no_log: "{{ keycloak_no_log | default(true) }}"
|
||||
|
||||
- name: Create OpenShift OIDC client in Keycloak
|
||||
middleware_automation.keycloak.keycloak_client:
|
||||
realm: "{{ keycloak_realm }}"
|
||||
client_id: "{{ oidc_client_id }}"
|
||||
name: "OpenShift - {{ ocp_cluster_name }}"
|
||||
description: "OIDC client for OpenShift cluster {{ ocp_cluster_name }}.{{ ocp_base_domain }}"
|
||||
enabled: true
|
||||
protocol: openid-connect
|
||||
public_client: false
|
||||
standard_flow_enabled: true
|
||||
implicit_flow_enabled: false
|
||||
direct_access_grants_enabled: false
|
||||
service_accounts_enabled: false
|
||||
secret: "{{ __oidc_client_secret }}"
|
||||
redirect_uris:
|
||||
- "{{ oidc_redirect_uri }}"
|
||||
web_origins:
|
||||
- "+"
|
||||
protocol_mappers:
|
||||
- name: groups
|
||||
protocol: openid-connect
|
||||
protocolMapper: oidc-group-membership-mapper
|
||||
config:
|
||||
full.path: "false"
|
||||
id.token.claim: "true"
|
||||
access.token.claim: "true"
|
||||
userinfo.token.claim: "true"
|
||||
claim.name: groups
|
||||
state: present
|
||||
no_log: "{{ keycloak_no_log | default(true) }}"
|
||||
|
||||
- name: Display generated client secret (save this to vault!)
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "*** GENERATED OIDC CLIENT SECRET — SAVE THIS TO VAULT ***"
|
||||
- "vault_oidc_client_secret: {{ __oidc_client_secret }}"
|
||||
- ""
|
||||
- "Set this in host_vars or pass as --extra-vars on future runs."
|
||||
when: __oidc_secret_generated | bool
|
||||
|
||||
- name: Display Keycloak configuration summary
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Keycloak OIDC client configured:"
|
||||
- " Realm : {{ keycloak_realm }}"
|
||||
- " Client : {{ oidc_client_id }}"
|
||||
- " Issuer : {{ keycloak_url }}{{ keycloak_context }}/realms/{{ keycloak_realm }}"
|
||||
- " Redirect: {{ oidc_redirect_uri }}"
|
||||
verbosity: 1
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Play 6: Post-install OpenShift configuration
|
||||
# Configure OIDC OAuth, cert-manager, and delete kubeadmin
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Configure OpenShift post-install
|
||||
hosts: sno.openshift.toal.ca
|
||||
gather_facts: false
|
||||
connection: local
|
||||
|
||||
environment:
|
||||
KUBECONFIG: "{{ ocp_kubeconfig | default('~/.kube/config') }}"
|
||||
K8S_AUTH_VERIFY_SSL: "false"
|
||||
|
||||
tags:
|
||||
- sno_deploy_oidc
|
||||
- sno_deploy_certmanager
|
||||
- sno_deploy_delete_kubeadmin
|
||||
|
||||
tasks:
|
||||
- name: Configure OpenShift OAuth with OIDC
|
||||
ansible.builtin.include_role:
|
||||
name: sno_deploy
|
||||
tasks_from: configure_oidc.yml
|
||||
tags: sno_deploy_oidc
|
||||
|
||||
- name: Configure cert-manager and LetsEncrypt certificates
|
||||
ansible.builtin.include_role:
|
||||
name: sno_deploy
|
||||
tasks_from: configure_certmanager.yml
|
||||
tags: sno_deploy_certmanager
|
||||
|
||||
- name: Delete kubeadmin user
|
||||
ansible.builtin.include_role:
|
||||
name: sno_deploy
|
||||
tasks_from: delete_kubeadmin.yml
|
||||
tags:
|
||||
- never
|
||||
- sno_deploy_delete_kubeadmin
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Play 7: Install LVM Storage for persistent volumes
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Configure LVM Storage for persistent volumes
|
||||
hosts: sno.openshift.toal.ca
|
||||
gather_facts: false
|
||||
connection: local
|
||||
tags: sno_deploy_lvms
|
||||
|
||||
environment:
|
||||
KUBECONFIG: "{{ ocp_kubeconfig | default('~/.kube/config') }}"
|
||||
K8S_AUTH_VERIFY_SSL: "false"
|
||||
|
||||
roles:
|
||||
- role: lvms_operator
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Play 8: Deploy NFS provisioner for ReadWriteMany storage
|
||||
# Set nfs_provisioner_external_server / nfs_provisioner_external_path to use
|
||||
# a pre-existing NFS share (e.g. 192.168.129.100:/mnt/BIGPOOL/NoBackups/OCPNFS).
|
||||
# When those are unset, an in-cluster NFS server is deployed; LVMS (Play 7) must
|
||||
# have run first to provide the backing RWO PVC.
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Deploy in-cluster NFS provisioner
|
||||
hosts: sno.openshift.toal.ca
|
||||
gather_facts: false
|
||||
connection: local
|
||||
tags: sno_deploy_nfs
|
||||
|
||||
environment:
|
||||
KUBECONFIG: "{{ ocp_kubeconfig | default('~/.kube/config') }}"
|
||||
K8S_AUTH_VERIFY_SSL: "false"
|
||||
|
||||
roles:
|
||||
- role: nfs_provisioner
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Play 9: Provision ServiceAccounts for application deployers
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Provision OpenShift service accounts
|
||||
hosts: sno.openshift.toal.ca
|
||||
gather_facts: false
|
||||
connection: local
|
||||
|
||||
environment:
|
||||
KUBECONFIG: "{{ ocp_kubeconfig | default('~/.kube/config') }}"
|
||||
K8S_AUTH_VERIFY_SSL: "false"
|
||||
|
||||
tags:
|
||||
- never
|
||||
- sno_deploy_service_accounts
|
||||
|
||||
tasks:
|
||||
# ---- supabase-deployer ----
|
||||
- name: Provision supabase-deployer ServiceAccount
|
||||
ansible.builtin.include_role:
|
||||
name: ocp_service_account
|
||||
vars:
|
||||
ocp_service_account_name: supabase-deployer
|
||||
ocp_service_account_namespace: supabase
|
||||
ocp_service_account_cluster_role_rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["namespaces"]
|
||||
verbs: ["get", "list", "create", "patch", "delete"]
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets", "configmaps", "services", "serviceaccounts",
|
||||
"persistentvolumeclaims", "pods", "endpoints"]
|
||||
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments", "statefulsets", "replicasets"]
|
||||
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||
- apiGroups: ["rbac.authorization.k8s.io"]
|
||||
resources: ["clusterroles", "clusterrolebindings", "roles", "rolebindings"]
|
||||
verbs: ["get", "list", "watch", "create", "update", "patch", "delete", "escalate", "bind"]
|
||||
- apiGroups: ["security.openshift.io"]
|
||||
resources: ["securitycontextconstraints"]
|
||||
verbs: ["get", "list", "use"]
|
||||
- apiGroups: ["route.openshift.io"]
|
||||
resources: ["routes", "routes/custom-host"]
|
||||
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||
- apiGroups: ["cert-manager.io"]
|
||||
resources: ["certificates", "clusterissuers", "issuers"]
|
||||
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||
- apiGroups: ["networking.k8s.io"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||
- apiGroups: ["storage.k8s.io"]
|
||||
resources: ["storageclasses"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
- name: Store supabase-deployer token in Vault
|
||||
community.hashi_vault.vault_kv2_write:
|
||||
path: toallab/supabase-deployer
|
||||
engine_mount_point: kv
|
||||
data:
|
||||
token: "{{ __ocp_service_account_token }}"
|
||||
no_log: true
|
||||
|
||||
# ---- aap-deployer ----
|
||||
- name: Provision aap-deployer ServiceAccount
|
||||
ansible.builtin.include_role:
|
||||
name: ocp_service_account
|
||||
vars:
|
||||
ocp_service_account_name: aap-deployer
|
||||
ocp_service_account_namespace: aap
|
||||
ocp_service_account_cluster_role_rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["namespaces"]
|
||||
verbs: ["get", "list", "create", "patch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["get", "list", "watch", "create", "patch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["serviceaccounts"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
# OPA server (deploy_opa.yml): policy ConfigMap, Service, service CA bundle
|
||||
- apiGroups: [""]
|
||||
resources: ["configmaps", "services"]
|
||||
verbs: ["get", "list", "watch", "create", "patch"]
|
||||
# patch: Automation Orchestrator backend env (APP_OIDC_ALLOW_PRIVATE_NETWORKS)
|
||||
# create: OPA server Deployment
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments"]
|
||||
verbs: ["get", "list", "watch", "create", "patch"]
|
||||
- apiGroups: ["route.openshift.io"]
|
||||
resources: ["routes"]
|
||||
verbs: ["get", "list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["endpoints"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["operators.coreos.com"]
|
||||
resources: ["operatorgroups", "subscriptions", "clusterserviceversions",
|
||||
"catalogsources"]
|
||||
verbs: ["get", "list", "create", "patch", "watch"]
|
||||
- apiGroups: ["operators.coreos.com"]
|
||||
resources: ["installplans"]
|
||||
verbs: ["get", "list", "patch", "watch"]
|
||||
- apiGroups: ["apiextensions.k8s.io"]
|
||||
resources: ["customresourcedefinitions"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["aap.ansible.com"]
|
||||
resources: ["ansibleautomationplatforms", "automationorchestrators"]
|
||||
verbs: ["get", "list", "create", "patch", "watch"]
|
||||
# CloudNativePG Cluster/Database CRs backing Automation Orchestrator
|
||||
- apiGroups: ["postgresql.cnpg.io"]
|
||||
resources: ["clusters", "databases"]
|
||||
verbs: ["get", "list", "create", "patch", "watch"]
|
||||
# Automation Orchestrator pre-release image mirror (IDMS) + MCP rollout wait
|
||||
- apiGroups: ["config.openshift.io"]
|
||||
resources: ["imagedigestmirrorsets"]
|
||||
verbs: ["get", "list", "create", "patch"]
|
||||
- apiGroups: ["machineconfiguration.openshift.io"]
|
||||
resources: ["machineconfigpools"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
- name: Store aap-deployer token in Vault
|
||||
community.hashi_vault.vault_kv2_write:
|
||||
path: toallab/aap-deployer
|
||||
engine_mount_point: kv
|
||||
data:
|
||||
token: "{{ __ocp_service_account_token }}"
|
||||
no_log: true
|
||||
|
||||
# ---- rhoai-deployer (deploy_rhoai_maas.yml) ----
|
||||
- name: Provision rhoai-deployer ServiceAccount
|
||||
ansible.builtin.include_role:
|
||||
name: ocp_service_account
|
||||
vars:
|
||||
ocp_service_account_name: rhoai-deployer
|
||||
ocp_service_account_namespace: redhat-ods-operator
|
||||
ocp_service_account_cluster_role_rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["namespaces"]
|
||||
verbs: ["get", "list", "create", "patch"]
|
||||
# configmaps: cluster-monitoring-config (UWM), gateway options
|
||||
# services: Authorino serving-cert annotation
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets", "configmaps", "services"]
|
||||
verbs: ["get", "list", "watch", "create", "patch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["serviceaccounts", "endpoints"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
# delete: restart the Kuadrant operator after Istio is installed
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "list", "watch", "delete"]
|
||||
# patch: Authorino service-CA trust env
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments"]
|
||||
verbs: ["get", "list", "watch", "patch"]
|
||||
- apiGroups: ["route.openshift.io"]
|
||||
resources: ["routes", "routes/custom-host"]
|
||||
verbs: ["get", "list", "create", "patch"]
|
||||
- apiGroups: ["config.openshift.io"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get", "list"]
|
||||
- apiGroups: ["operators.coreos.com"]
|
||||
resources: ["operatorgroups", "subscriptions", "clusterserviceversions",
|
||||
"catalogsources"]
|
||||
verbs: ["get", "list", "create", "patch", "watch"]
|
||||
- apiGroups: ["operators.coreos.com"]
|
||||
resources: ["installplans"]
|
||||
verbs: ["get", "list", "patch", "watch"]
|
||||
- apiGroups: ["apiextensions.k8s.io"]
|
||||
resources: ["customresourcedefinitions"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
# CloudNativePG Cluster/Database CRs backing the MaaS database
|
||||
- apiGroups: ["postgresql.cnpg.io"]
|
||||
resources: ["clusters", "databases"]
|
||||
verbs: ["get", "list", "create", "patch", "watch"]
|
||||
- apiGroups: ["gateway.networking.k8s.io"]
|
||||
resources: ["gatewayclasses", "gateways"]
|
||||
verbs: ["get", "list", "create", "patch", "watch"]
|
||||
# Dedicated MaaS gateway certificate (avoids HTTP/2 coalescing on the wildcard)
|
||||
- apiGroups: ["cert-manager.io"]
|
||||
resources: ["certificates"]
|
||||
verbs: ["get", "list", "create", "patch", "watch"]
|
||||
- apiGroups: ["kuadrant.io"]
|
||||
resources: ["kuadrants"]
|
||||
verbs: ["get", "list", "create", "patch", "watch"]
|
||||
- apiGroups: ["operator.authorino.kuadrant.io"]
|
||||
resources: ["authorinos"]
|
||||
verbs: ["get", "list", "patch", "watch"]
|
||||
- apiGroups: ["dscinitialization.opendatahub.io"]
|
||||
resources: ["dscinitializations"]
|
||||
verbs: ["get", "list", "create", "patch", "watch"]
|
||||
- apiGroups: ["datasciencecluster.opendatahub.io"]
|
||||
resources: ["datascienceclusters"]
|
||||
verbs: ["get", "list", "create", "patch", "watch"]
|
||||
- apiGroups: ["opendatahub.io"]
|
||||
resources: ["odhdashboardconfigs"]
|
||||
verbs: ["get", "list", "patch"]
|
||||
- apiGroups: ["inference.opendatahub.io"]
|
||||
resources: ["externalproviders", "externalmodels"]
|
||||
verbs: ["get", "list", "create", "patch", "watch"]
|
||||
- apiGroups: ["maas.opendatahub.io"]
|
||||
resources: ["maasmodelrefs", "maasauthpolicies", "maassubscriptions"]
|
||||
verbs: ["get", "list", "create", "patch", "watch"]
|
||||
|
||||
- name: Store rhoai-deployer token in Vault
|
||||
community.hashi_vault.vault_kv2_write:
|
||||
path: toallab/rhoai-deployer
|
||||
engine_mount_point: kv
|
||||
data:
|
||||
token: "{{ __ocp_service_account_token }}"
|
||||
no_log: true
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Play 10: Drop platform alerts that are false positives on a single
|
||||
# control-plane cluster (AlertRelabelConfig in openshift-monitoring)
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Tune platform alerts for single-node OpenShift
|
||||
hosts: sno.openshift.toal.ca
|
||||
gather_facts: false
|
||||
connection: local
|
||||
tags: sno_deploy_alert_tuning
|
||||
|
||||
environment:
|
||||
KUBECONFIG: "{{ ocp_kubeconfig | default('~/.kube/config') }}"
|
||||
K8S_AUTH_VERIFY_SSL: "false"
|
||||
|
||||
roles:
|
||||
- role: sno_alert_tuning
|
||||
55
playbooks/deploy_rhoai_maas.yml
Normal file
55
playbooks/deploy_rhoai_maas.yml
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
# Deploy Red Hat OpenShift AI 3.5 Models-as-a-Service and publish the existing
|
||||
# llama-coder (llama.cpp) server through it as an ExternalModel.
|
||||
#
|
||||
# WARNING: the MaaS database is a single-instance CloudNativePG cluster intended
|
||||
# for demo/dev use — not production.
|
||||
#
|
||||
# Technology Preview: MaaS is GA in 3.5, but ExternalModel (used here to front
|
||||
# llama.cpp instead of a KServe LLMInferenceService) is Technology Preview.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - OpenShift cluster deployed (deploy_openshift.yml) with cert-manager
|
||||
# - rhoai-deployer ServiceAccount provisioned (re-run after RBAC changes):
|
||||
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_service_accounts
|
||||
# - vault_rhoai_deployer_token set in host_vars (see hashivault_secrets.yml)
|
||||
# - llama-coder running in llm-coding-assistant with secret llama-coder-api-key
|
||||
#
|
||||
# Usage:
|
||||
# ansible-navigator run playbooks/deploy_rhoai_maas.yml --mode stdout
|
||||
|
||||
- name: Deploy OpenShift AI Models-as-a-Service
|
||||
hosts: sno.openshift.toal.ca
|
||||
gather_facts: false
|
||||
connection: local
|
||||
|
||||
environment:
|
||||
K8S_AUTH_HOST: "{{ rhoai_maas_k8s_api_url | default('https://api.' ~ ocp_cluster_name ~ '.' ~ ocp_base_domain ~ ':6443') }}"
|
||||
K8S_AUTH_API_KEY: "{{ vault_rhoai_deployer_token }}"
|
||||
|
||||
pre_tasks:
|
||||
- name: Verify rhoai-deployer token is available
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- vault_rhoai_deployer_token is defined
|
||||
- vault_rhoai_deployer_token | length > 0
|
||||
fail_msg: >-
|
||||
vault_rhoai_deployer_token is not set. Provision the ServiceAccount with:
|
||||
ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_service_accounts
|
||||
Then reference kv/data/toallab/rhoai-deployer:token as vault_rhoai_deployer_token.
|
||||
|
||||
roles:
|
||||
- role: cloudnativepg_operator
|
||||
vars:
|
||||
cloudnativepg_operator_cluster_namespace: rhoai-maas-db
|
||||
cloudnativepg_operator_cluster_name: maas-postgres
|
||||
cloudnativepg_operator_owner_user: maas
|
||||
cloudnativepg_operator_max_connections: 100
|
||||
cloudnativepg_operator_databases:
|
||||
- name: maas
|
||||
dbname: maas
|
||||
cloudnativepg_operator_secrets:
|
||||
- name: maas-postgres-secret
|
||||
database: maas
|
||||
cloudnativepg_operator_vault_path: cloudnativepg/maas-postgres
|
||||
- role: rhoai_maas
|
||||
44
playbooks/deploy_supabase.yml
Normal file
44
playbooks/deploy_supabase.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
# Deploy Supabase on OpenShift (SNO) via the supabase-community Helm chart.
|
||||
#
|
||||
# Chart: https://github.com/supabase-community/supabase-kubernetes
|
||||
#
|
||||
# Prerequisites:
|
||||
# - cert-manager installed (deploy_openshift.yml --tags sno_deploy_certmanager)
|
||||
# - NFS StorageClass available (deploy_openshift.yml --tags sno_deploy_nfs)
|
||||
# - supabase-deployer ServiceAccount provisioned and token saved to 1Password:
|
||||
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_service_accounts
|
||||
# - vault_supabase_deployer_token set in host_vars (see hashivault_secrets.yml)
|
||||
#
|
||||
# All Supabase secrets (JWT, DB password, etc.) are auto-generated on first run
|
||||
# and stored in Vault at supabase_vault_path. No manual secret setup required.
|
||||
#
|
||||
# Usage:
|
||||
# ansible-navigator run playbooks/deploy_supabase.yml --mode stdout
|
||||
# ansible-navigator run playbooks/deploy_supabase.yml --mode stdout -e supabase_chart_version=0.0.9
|
||||
# ansible-navigator run playbooks/deploy_supabase.yml --mode stdout -e supabase_db_storage_size=20Gi
|
||||
|
||||
- name: Deploy Supabase on OpenShift
|
||||
hosts: sno.openshift.toal.ca
|
||||
gather_facts: false
|
||||
connection: local
|
||||
|
||||
environment:
|
||||
K8S_AUTH_HOST: "{{ supabase_k8s_api_url }}"
|
||||
K8S_AUTH_API_KEY: "{{ vault_supabase_deployer_token }}"
|
||||
K8S_AUTH_VERIFY_SSL: "false"
|
||||
|
||||
pre_tasks:
|
||||
- name: Verify supabase-deployer token is available
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- vault_supabase_deployer_token is defined
|
||||
- vault_supabase_deployer_token | length > 0
|
||||
fail_msg: >-
|
||||
vault_supabase_deployer_token is not set.
|
||||
Provision the ServiceAccount with:
|
||||
ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_service_accounts
|
||||
Then save the displayed token to 1Password and reference it as vault_supabase_deployer_token.
|
||||
|
||||
roles:
|
||||
- role: supabase
|
||||
151
playbooks/deploy_vault.yml
Normal file
151
playbooks/deploy_vault.yml
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
# Deploy and configure HashiCorp Vault CE on TrueNAS Scale.
|
||||
#
|
||||
# Vault is deployed as a TrueNAS custom app (Docker Compose).
|
||||
# This playbook handles post-deploy configuration only — it does NOT install Vault.
|
||||
# See: docs/ for the TrueNAS compose YAML and vault.hcl required before running.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Vault running on TrueNAS and accessible at vault_url
|
||||
# - vault host/group in inventory with vault_url and vault_oidc_issuer set
|
||||
#
|
||||
# Keycloak OIDC prerequisites (--tags vault_configure_keycloak,vault_configure_oidc):
|
||||
# - Keycloak realm exists (configured via deploy_openshift.yml)
|
||||
# - vault_vault_oidc_client_secret in 1Password (or it will be generated and displayed)
|
||||
# - In host_vars for the vault host:
|
||||
# vault_url: "http://nas.lan.toal.ca:8200"
|
||||
# vault_oidc_issuer: "https://keycloak.apps.<cluster>.<domain>/realms/<realm>"
|
||||
#
|
||||
# Play order:
|
||||
# Play 0: vault_configure_keycloak — Create Keycloak OIDC client for Vault
|
||||
# Play 1: vault_init — Initialize Vault, display keys for 1Password
|
||||
# Play 2: (default) — Unseal + configure OIDC authentication
|
||||
#
|
||||
# Usage:
|
||||
# ansible-navigator run playbooks/deploy_vault.yml --tags vault_configure_keycloak
|
||||
# ansible-navigator run playbooks/deploy_vault.yml --tags vault_init
|
||||
# ansible-navigator run playbooks/deploy_vault.yml
|
||||
# ansible-navigator run playbooks/deploy_vault.yml --tags vault_configure_keycloak,vault_init
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Play 0: Create Keycloak OIDC client for Vault (optional)
|
||||
# Runs on openshift hosts to access keycloak_url/keycloak_realm host vars.
|
||||
# Creates the OIDC client in Keycloak with the correct Vault callback URIs.
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Configure Keycloak OIDC client for Vault
|
||||
hosts: openshift
|
||||
gather_facts: false
|
||||
connection: local
|
||||
|
||||
tags:
|
||||
- never
|
||||
- vault_configure_keycloak
|
||||
|
||||
vars:
|
||||
__vault_keycloak_api_url: "{{ keycloak_url }}{{ keycloak_context | default('') }}"
|
||||
__vault_oidc_client_id: "{{ vault_oidc_client_id | default('vault') }}"
|
||||
__vault_url: "{{ hostvars[groups['vault'][0]]['vault_url'] | default('http://nas.lan.toal.ca:8200') }}"
|
||||
|
||||
module_defaults:
|
||||
middleware_automation.keycloak.keycloak_client:
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: "{{ __vault_keycloak_api_url }}"
|
||||
auth_realm: master
|
||||
auth_username: "{{ keycloak_admin_user }}"
|
||||
auth_password: "{{ vault_keycloak_admin_password }}"
|
||||
validate_certs: "{{ keycloak_validate_certs | default(true) }}"
|
||||
|
||||
tasks:
|
||||
- name: Set Vault OIDC client secret (vault value or generated)
|
||||
ansible.builtin.set_fact:
|
||||
__vault_oidc_client_secret: "{{ vault_vault_oidc_client_secret | default(lookup('community.general.random_string', length=32, special=false)) }}"
|
||||
__vault_oidc_secret_generated: "{{ vault_vault_oidc_client_secret is not defined }}"
|
||||
no_log: true
|
||||
|
||||
- name: Create Vault OIDC client in Keycloak
|
||||
middleware_automation.keycloak.keycloak_client:
|
||||
realm: "{{ keycloak_realm }}"
|
||||
client_id: "{{ __vault_oidc_client_id }}"
|
||||
name: "HashiCorp Vault"
|
||||
description: "OIDC client for Vault on TrueNAS"
|
||||
enabled: true
|
||||
protocol: openid-connect
|
||||
public_client: false
|
||||
standard_flow_enabled: true
|
||||
implicit_flow_enabled: false
|
||||
direct_access_grants_enabled: false
|
||||
service_accounts_enabled: false
|
||||
secret: "{{ __vault_oidc_client_secret }}"
|
||||
redirect_uris:
|
||||
- "{{ __vault_url }}/ui/vault/auth/oidc/oidc/callback"
|
||||
- "http://localhost:8250/oidc/callback"
|
||||
web_origins:
|
||||
- "+"
|
||||
protocol_mappers:
|
||||
- name: groups
|
||||
protocol: openid-connect
|
||||
protocolMapper: oidc-group-membership-mapper
|
||||
config:
|
||||
full.path: "false"
|
||||
id.token.claim: "true"
|
||||
access.token.claim: "true"
|
||||
userinfo.token.claim: "true"
|
||||
claim.name: groups
|
||||
state: present
|
||||
no_log: "{{ keycloak_no_log | default(true) }}"
|
||||
|
||||
- name: Display generated client secret (save this to vault!)
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "*** GENERATED VAULT OIDC CLIENT SECRET — SAVE THIS TO 1PASSWORD ***"
|
||||
- "vault_vault_oidc_client_secret: {{ __vault_oidc_client_secret }}"
|
||||
- ""
|
||||
- "Save to 1Password and reference as vault_vault_oidc_client_secret."
|
||||
when: __vault_oidc_secret_generated | bool
|
||||
|
||||
- name: Display Keycloak Vault OIDC configuration summary
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Keycloak Vault OIDC client configured:"
|
||||
- " Realm : {{ keycloak_realm }}"
|
||||
- " Client : {{ __vault_oidc_client_id }}"
|
||||
- " Issuer : {{ __vault_keycloak_api_url }}/realms/{{ keycloak_realm }}"
|
||||
- ""
|
||||
- "Set in host_vars for the vault host:"
|
||||
- " vault_oidc_issuer: {{ __vault_keycloak_api_url }}/realms/{{ keycloak_realm }}"
|
||||
- ""
|
||||
- "Then run: --tags vault_init (if not done) then the default play."
|
||||
verbosity: 1
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Play 1: Initialize Vault (optional, one-time)
|
||||
# Initializes Vault and displays root token + unseal keys for saving to 1Password.
|
||||
# Fails after init intentionally — save credentials then run the default play.
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Initialize Vault
|
||||
hosts: vault
|
||||
gather_facts: false
|
||||
connection: local
|
||||
|
||||
tags:
|
||||
- never
|
||||
- vault_init
|
||||
|
||||
tasks:
|
||||
- name: Run Vault init tasks
|
||||
ansible.builtin.include_role:
|
||||
name: vault_setup
|
||||
tasks_from: init.yml
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Play 2: Unseal and configure Vault OIDC authentication (default)
|
||||
# Requires vault_vault_root_token and vault_vault_oidc_client_secret in 1Password.
|
||||
# Optionally unseals if vault_unseal_keys is provided and Vault is sealed.
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Configure Vault OIDC authentication
|
||||
hosts: vault
|
||||
gather_facts: false
|
||||
connection: local
|
||||
|
||||
roles:
|
||||
- role: vault_setup
|
||||
23
playbooks/maintain_sno_etcd.yml
Normal file
23
playbooks/maintain_sno_etcd.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
# Defragment etcd on the single-node OpenShift cluster.
|
||||
#
|
||||
# OpenShift disables automatic etcd defragmentation on single-control-plane
|
||||
# clusters, so run this on a schedule (e.g. weekly AAP schedule, off-hours).
|
||||
# It only defragments when fragmentation exceeds the thresholds of the
|
||||
# etcdDatabaseHighFragmentationRatio alert; otherwise it reports and exits.
|
||||
#
|
||||
# Usage:
|
||||
# ansible-navigator run playbooks/maintain_sno_etcd.yml --mode stdout
|
||||
# ansible-navigator run playbooks/maintain_sno_etcd.yml --mode stdout -e sno_etcd_defrag_force=true
|
||||
|
||||
- name: Defragment etcd on single-node OpenShift
|
||||
hosts: sno.openshift.toal.ca
|
||||
gather_facts: false
|
||||
connection: local
|
||||
|
||||
environment:
|
||||
KUBECONFIG: "{{ ocp_kubeconfig | default('~/.kube/config') }}"
|
||||
K8S_AUTH_VERIFY_SSL: "false"
|
||||
|
||||
roles:
|
||||
- role: sno_etcd_defrag
|
||||
@@ -1,32 +1,64 @@
|
||||
---
|
||||
- name: Get info on the existing host entries
|
||||
hosts: localhost
|
||||
- name: Configure DHCP
|
||||
hosts: opnsense
|
||||
gather_facts: false
|
||||
module_defaults:
|
||||
group/ansibleguy.opnsense.all:
|
||||
firewall: '{{ lookup("env","OPNSENSE_HOST") }}'
|
||||
api_key: '{{ lookup("env","OPNSENSE_API_KEY") }}'
|
||||
api_secret: '{{ lookup("env","OPNSENSE_API_SECRET") }}'
|
||||
api_port: 8443
|
||||
|
||||
ansibleguy.opnsense.unbound_host:
|
||||
match_fields: ['description']
|
||||
|
||||
ansibleguy.opnsense.list:
|
||||
target: 'unbound_host'
|
||||
group/oxlorg.opnsense.all:
|
||||
firewall: "{{ opnsense_host }}"
|
||||
api_key: "{{ opnsense_api_key }}"
|
||||
api_secret: "{{ opnsense_api_secret }}"
|
||||
ssl_verify: false
|
||||
api_port: "{{ opnsense_api_port|default(omit) }}"
|
||||
|
||||
tasks:
|
||||
- name: Listing hosts # noqa args[module]
|
||||
ansibleguy.opnsense.list:
|
||||
target: 'unbound_host'
|
||||
register: existing_entries
|
||||
- name: Install packages
|
||||
oxlorg.opnsense.package:
|
||||
name:
|
||||
- os-acme-client
|
||||
action: install
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Printing entries
|
||||
ansible.builtin.debug:
|
||||
var: existing_entries.data
|
||||
- name: Setup ACME Client
|
||||
ansible.builtin.include_role:
|
||||
name: toallab.infra.opnsense_service
|
||||
tasks_from: setup.yml
|
||||
|
||||
- name: Generate csv from template
|
||||
ansible.builtin.template:
|
||||
src: ../templates/hosts.j2
|
||||
mode: "0644"
|
||||
dest: "/data/output.csv"
|
||||
- name: Configure KEA DHCP Server
|
||||
oxlorg.opnsense.dhcp_general:
|
||||
enabled: "{{ dhcp_enabled }}"
|
||||
interfaces: "{{ dhcp_interfaces }}"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Add subnet
|
||||
oxlorg.opnsense.dhcp_subnet:
|
||||
subnet: "{{ item.subnet }}"
|
||||
pools: "{{ item.pools }}"
|
||||
auto_options: false
|
||||
gateway: '{{ item.gateway }}'
|
||||
dns: '{{ item.dns }}'
|
||||
domain: '{{ item.domain }}'
|
||||
reload: false
|
||||
delegate_to: localhost
|
||||
loop: "{{ dhcp_subnets }}"
|
||||
|
||||
- name: Get all dhcp_reservations_* variables from hostvars
|
||||
ansible.builtin.set_fact:
|
||||
all_dhcp_reservations: >-
|
||||
{{
|
||||
hostvars[inventory_hostname] | dict2items
|
||||
| selectattr('key', 'match', '^dhcp_reservations_')
|
||||
| map(attribute='value')
|
||||
| flatten
|
||||
| selectattr('type', 'match', 'static')
|
||||
}}
|
||||
|
||||
- name: Add DHCP Reservations
|
||||
oxlorg.opnsense.dhcp_reservation:
|
||||
hostname: "{{ item.hostname }}"
|
||||
mac: "{{ item.mac }}"
|
||||
ip: "{{ item.address }}"
|
||||
subnet: "{{ item.address | ansible.utils.ipsubnet(24) }}"
|
||||
description: "{{ item.description | default('') }}"
|
||||
reload: false
|
||||
delegate_to: localhost
|
||||
loop: "{{ all_dhcp_reservations }}"
|
||||
|
||||
@@ -272,12 +272,6 @@
|
||||
|
||||
#TODO Automatically set up DNS GSSAPI per: https://access.redhat.com/documentation/en-us/red_hat_satellite/6.8/html/installing_satellite_server_from_a_connected_network/configuring-external-services#configuring-external-idm-dns_satellite
|
||||
|
||||
- name: Set up Basic Lab Packages
|
||||
hosts: "{{ vm_name }}"
|
||||
become: yes
|
||||
roles:
|
||||
- role: toal-common
|
||||
|
||||
- name: Install Satellite Servers
|
||||
hosts: "{{ vm_name }}"
|
||||
become: true
|
||||
|
||||
@@ -6,10 +6,9 @@
|
||||
|
||||
- name: linux-system-roles.network
|
||||
when: network_connections is defined
|
||||
- name: toal-common
|
||||
|
||||
- name: Set Network OS from Netbox info.
|
||||
gather_facts: no
|
||||
gather_facts: false
|
||||
hosts: switch01
|
||||
tasks:
|
||||
- name: Set network os type for Cisco
|
||||
@@ -20,14 +19,14 @@
|
||||
hosts: switch01
|
||||
become_method: enable
|
||||
connection: network_cli
|
||||
gather_facts: no
|
||||
gather_facts: false
|
||||
|
||||
roles:
|
||||
- toallab.infrastructure
|
||||
|
||||
- name: DHCP Server
|
||||
hosts: service_dhcp
|
||||
become: yes
|
||||
become: true
|
||||
|
||||
pre_tasks:
|
||||
# - name: Gather interfaces for dhcp service
|
||||
@@ -52,7 +51,7 @@
|
||||
# domain_name_servers: 10.0.2.3
|
||||
# routers: 192.168.222.129
|
||||
roles:
|
||||
- name: sage905.netbox-to-dhcp
|
||||
- sage905.netbox-to-dhcp
|
||||
|
||||
- name: Include Minecraft tasks
|
||||
import_playbook: minecraft.yml
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
- name: Create 1Password Secret
|
||||
hosts: localhost
|
||||
tasks:
|
||||
- onepassword.connect.generic_item:
|
||||
vault_id: "e63n3krpqx7qpohuvlyqpn6m34"
|
||||
title: Lab Secrets Test
|
||||
state: created
|
||||
fields:
|
||||
- label: Codeword
|
||||
value: "hunter2"
|
||||
section: "Personal Info"
|
||||
field_type: concealed
|
||||
# no_log: true
|
||||
register: op_item
|
||||
@@ -1,16 +0,0 @@
|
||||
- name: Create Windows AD Server
|
||||
hosts: WinAD
|
||||
gather_facts: false
|
||||
connection: local
|
||||
become: false
|
||||
|
||||
vars:
|
||||
ansible_python_interpreter: "{{ ansible_playbook_python }}"
|
||||
|
||||
roles:
|
||||
- oatakan.ansible-role-ovirt
|
||||
|
||||
- name: Configure AD Controller
|
||||
hosts: WinAD
|
||||
become: false
|
||||
- oatakan.ansible-role-windows-ad-controller
|
||||
32
roles/aap_operator/defaults/main.yml
Normal file
32
roles/aap_operator/defaults/main.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
# --- OLM subscription ---
|
||||
aap_operator_namespace: aap
|
||||
aap_operator_channel: "stable-2.6"
|
||||
aap_operator_source: redhat-operators
|
||||
aap_operator_name: ansible-automation-platform-operator
|
||||
aap_operator_wait_timeout: 1800
|
||||
|
||||
# --- AnsibleAutomationPlatform CR ---
|
||||
aap_operator_platform_name: aap
|
||||
|
||||
# --- Components (set disabled: true to skip) ---
|
||||
aap_operator_controller_disabled: false
|
||||
aap_operator_hub_disabled: false
|
||||
aap_operator_eda_disabled: false
|
||||
|
||||
# --- Storage ---
|
||||
# RWO StorageClass for PostgreSQL (all components)
|
||||
aap_operator_storage_class: lvms-vg-data
|
||||
# RWX StorageClass for Hub file/artifact storage
|
||||
aap_operator_hub_file_storage_class: nfs-client
|
||||
aap_operator_hub_file_storage_size: 10Gi
|
||||
|
||||
# --- Admin ---
|
||||
aap_operator_admin_user: admin
|
||||
|
||||
# --- Routing (optional) ---
|
||||
# Set to a custom hostname to override the auto-generated Gateway route (primary UI/API entry point)
|
||||
# aap_operator_gateway_route_host: aap.example.com
|
||||
# Set to a custom hostname to override the auto-generated Controller route
|
||||
# aap_operator_controller_route_host: controller.example.com
|
||||
|
||||
75
roles/aap_operator/meta/argument_specs.yml
Normal file
75
roles/aap_operator/meta/argument_specs.yml
Normal file
@@ -0,0 +1,75 @@
|
||||
---
|
||||
argument_specs:
|
||||
main:
|
||||
short_description: Install AAP via OpenShift OLM operator (AnsibleAutomationPlatform CR)
|
||||
description:
|
||||
- Installs the Ansible Automation Platform operator via OLM and creates a
|
||||
single AnsibleAutomationPlatform CR that manages Controller, Hub, and EDA.
|
||||
options:
|
||||
aap_operator_namespace:
|
||||
description: Namespace for the AAP operator and platform instance.
|
||||
type: str
|
||||
default: aap
|
||||
aap_operator_channel:
|
||||
description: OLM subscription channel.
|
||||
type: str
|
||||
default: "stable-2.6"
|
||||
aap_operator_source:
|
||||
description: OLM catalog source name.
|
||||
type: str
|
||||
default: redhat-operators
|
||||
aap_operator_name:
|
||||
description: Operator package name in the catalog.
|
||||
type: str
|
||||
default: ansible-automation-platform-operator
|
||||
aap_operator_wait_timeout:
|
||||
description: Seconds to wait for operator and platform to become ready.
|
||||
type: int
|
||||
default: 1800
|
||||
aap_operator_platform_name:
|
||||
description: Name of the AnsibleAutomationPlatform CR.
|
||||
type: str
|
||||
default: aap
|
||||
aap_operator_controller_disabled:
|
||||
description: Set true to skip deploying Automation Controller.
|
||||
type: bool
|
||||
default: false
|
||||
aap_operator_hub_disabled:
|
||||
description: Set true to skip deploying Automation Hub.
|
||||
type: bool
|
||||
default: false
|
||||
aap_operator_eda_disabled:
|
||||
description: Set true to skip deploying Event-Driven Ansible.
|
||||
type: bool
|
||||
default: false
|
||||
aap_operator_storage_class:
|
||||
description: StorageClass for PostgreSQL persistent volumes (RWO).
|
||||
type: str
|
||||
default: lvms-vg-data
|
||||
aap_operator_hub_file_storage_class:
|
||||
description: StorageClass for Hub file/artifact storage (RWX).
|
||||
type: str
|
||||
default: nfs-client
|
||||
aap_operator_hub_file_storage_size:
|
||||
description: Size of the Hub file storage PVC.
|
||||
type: str
|
||||
default: 10Gi
|
||||
aap_operator_admin_user:
|
||||
description: Admin username for the platform.
|
||||
type: str
|
||||
default: admin
|
||||
aap_operator_gateway_route_host:
|
||||
description: >
|
||||
Custom hostname for the AAP Gateway Route (primary UI/API entry point in AAP 2.5+).
|
||||
When set, overrides the auto-generated gateway route (e.g. aap.example.com).
|
||||
Leave unset to use the default apps subdomain route.
|
||||
type: str
|
||||
required: false
|
||||
aap_operator_controller_route_host:
|
||||
description: >
|
||||
Custom hostname for the Automation Controller Route.
|
||||
When set, overrides the auto-generated controller route hostname.
|
||||
Leave unset to use the default apps subdomain route.
|
||||
type: str
|
||||
required: false
|
||||
|
||||
18
roles/aap_operator/meta/main.yml
Normal file
18
roles/aap_operator/meta/main.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: ptoal
|
||||
description: Install Ansible Automation Platform via OpenShift OLM operator
|
||||
license: MIT
|
||||
min_ansible_version: "2.16"
|
||||
platforms:
|
||||
- name: GenericLinux
|
||||
versions:
|
||||
- all
|
||||
galaxy_tags:
|
||||
- openshift
|
||||
- aap
|
||||
- operator
|
||||
- olm
|
||||
- ansible
|
||||
|
||||
dependencies: []
|
||||
4
roles/aap_operator/tasks/configure_oidc.yml
Normal file
4
roles/aap_operator/tasks/configure_oidc.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
# OIDC is configured via the AAP Gateway API, not via this role.
|
||||
# See: playbooks/deploy_aap.yml --tags aap_configure_keycloak,aap_configure_oidc
|
||||
# Uses: infra.aap_configuration.gateway_authenticators
|
||||
211
roles/aap_operator/tasks/main.yml
Normal file
211
roles/aap_operator/tasks/main.yml
Normal file
@@ -0,0 +1,211 @@
|
||||
---
|
||||
# Install Ansible Automation Platform via OpenShift OLM operator.
|
||||
#
|
||||
# Deploys the AAP operator, then creates a single AnsibleAutomationPlatform
|
||||
# CR that manages Controller, Hub, and EDA as a unified platform.
|
||||
# All tasks are idempotent (kubernetes.core.k8s state: present).
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 1: Install AAP operator via OLM
|
||||
# ------------------------------------------------------------------
|
||||
- name: Create AAP namespace
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: "{{ aap_operator_namespace }}"
|
||||
|
||||
- name: Read global pull secret
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: v1
|
||||
kind: Secret
|
||||
namespace: openshift-config
|
||||
name: pull-secret
|
||||
register: __aap_operator_global_pull_secret
|
||||
|
||||
- name: Copy pull secret to AAP namespace
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: redhat-operators-pull-secret
|
||||
namespace: "{{ aap_operator_namespace }}"
|
||||
type: kubernetes.io/dockerconfigjson
|
||||
data:
|
||||
.dockerconfigjson: "{{ __aap_operator_global_pull_secret.resources[0].data['.dockerconfigjson'] }}"
|
||||
no_log: false
|
||||
|
||||
- name: Create OperatorGroup for AAP
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: operators.coreos.com/v1
|
||||
kind: OperatorGroup
|
||||
metadata:
|
||||
name: "{{ aap_operator_name }}"
|
||||
namespace: "{{ aap_operator_namespace }}"
|
||||
spec:
|
||||
targetNamespaces:
|
||||
- "{{ aap_operator_namespace }}"
|
||||
upgradeStrategy: Default
|
||||
|
||||
- name: Subscribe to AAP operator
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: operators.coreos.com/v1alpha1
|
||||
kind: Subscription
|
||||
metadata:
|
||||
name: "{{ aap_operator_name }}"
|
||||
namespace: "{{ aap_operator_namespace }}"
|
||||
spec:
|
||||
channel: "{{ aap_operator_channel }}"
|
||||
installPlanApproval: Automatic
|
||||
name: "{{ aap_operator_name }}"
|
||||
source: "{{ aap_operator_source }}"
|
||||
sourceNamespace: openshift-marketplace
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 2: Wait for operator to be ready
|
||||
# ------------------------------------------------------------------
|
||||
- name: Wait for AnsibleAutomationPlatform CRD to be available
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
name: ansibleautomationplatforms.aap.ansible.com
|
||||
register: __aap_operator_crd
|
||||
until: __aap_operator_crd.resources | length > 0
|
||||
retries: "{{ __aap_operator_wait_retries }}"
|
||||
delay: 10
|
||||
|
||||
- name: Wait for AAP operator deployments to be ready
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: apps/v1
|
||||
kind: Deployment
|
||||
namespace: "{{ aap_operator_namespace }}"
|
||||
label_selectors:
|
||||
- "operators.coreos.com/{{ aap_operator_name }}.{{ aap_operator_namespace }}"
|
||||
register: __aap_operator_deploy
|
||||
until: >-
|
||||
__aap_operator_deploy.resources | length > 0 and
|
||||
(__aap_operator_deploy.resources
|
||||
| rejectattr('status.readyReplicas', 'undefined')
|
||||
| selectattr('status.readyReplicas', '>=', 1)
|
||||
| list | length) == (__aap_operator_deploy.resources | length)
|
||||
retries: "{{ __aap_operator_wait_retries }}"
|
||||
delay: 10
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 3: Deploy the unified AnsibleAutomationPlatform
|
||||
# ------------------------------------------------------------------
|
||||
- name: Create AnsibleAutomationPlatform
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: aap.ansible.com/v1alpha1
|
||||
kind: AnsibleAutomationPlatform
|
||||
metadata:
|
||||
name: "{{ aap_operator_platform_name }}"
|
||||
namespace: "{{ aap_operator_namespace }}"
|
||||
spec:
|
||||
admin_user: "{{ aap_operator_admin_user }}"
|
||||
# PostgreSQL storage for all components (RWO)
|
||||
database:
|
||||
postgres_storage_class: "{{ aap_operator_storage_class }}"
|
||||
# Gateway is the primary UI/API entry point in AAP 2.5+
|
||||
gateway:
|
||||
route_host: "{{ aap_operator_gateway_route_host | default(omit) }}"
|
||||
# Component toggles and per-component config
|
||||
controller:
|
||||
disabled: "{{ aap_operator_controller_disabled | bool }}"
|
||||
route_host: "{{ aap_operator_controller_route_host | default(omit) }}"
|
||||
hub:
|
||||
disabled: "{{ aap_operator_hub_disabled | bool }}"
|
||||
# Hub file/artifact storage (RWX) — must be under hub:
|
||||
storage_type: file
|
||||
file_storage_storage_class: "{{ aap_operator_hub_file_storage_class }}"
|
||||
file_storage_size: "{{ aap_operator_hub_file_storage_size }}"
|
||||
eda:
|
||||
disabled: "{{ aap_operator_eda_disabled | bool }}"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 3a: Clear controller route_host if not explicitly set
|
||||
# strategic-merge-patch (state: present) does not remove existing fields,
|
||||
# so we must explicitly remove /spec/controller/route_host if the user
|
||||
# hasn't set aap_operator_controller_route_host (e.g. after switching to
|
||||
# gateway-based routing).
|
||||
# ------------------------------------------------------------------
|
||||
# The platform operator propagates route_host to the child AutomationController CR.
|
||||
# Both must be cleared or the controller operator will continue to set the old hostname.
|
||||
- name: Remove controller route_host from platform CR (use auto-generated route)
|
||||
kubernetes.core.k8s_json_patch:
|
||||
api_version: aap.ansible.com/v1alpha1
|
||||
kind: AnsibleAutomationPlatform
|
||||
namespace: "{{ aap_operator_namespace }}"
|
||||
name: "{{ aap_operator_platform_name }}"
|
||||
patch:
|
||||
- op: remove
|
||||
path: /spec/controller/route_host
|
||||
when: aap_operator_controller_route_host is not defined
|
||||
failed_when: false # no-op if field doesn't exist
|
||||
|
||||
- name: Remove controller route_host from child AutomationController CR
|
||||
kubernetes.core.k8s_json_patch:
|
||||
api_version: automationcontroller.ansible.com/v1beta1
|
||||
kind: AutomationController
|
||||
namespace: "{{ aap_operator_namespace }}"
|
||||
name: "{{ aap_operator_platform_name }}-controller"
|
||||
patch:
|
||||
- op: remove
|
||||
path: /spec/route_host
|
||||
when: aap_operator_controller_route_host is not defined
|
||||
failed_when: false # no-op if field doesn't exist or CR not yet created
|
||||
|
||||
- name: Delete aap-controller Route so operator recreates with correct hostname
|
||||
kubernetes.core.k8s:
|
||||
api_version: route.openshift.io/v1
|
||||
kind: Route
|
||||
namespace: "{{ aap_operator_namespace }}"
|
||||
name: "{{ aap_operator_platform_name }}-controller"
|
||||
state: absent
|
||||
when: aap_operator_controller_route_host is not defined
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 4: Wait for platform to be ready
|
||||
# ------------------------------------------------------------------
|
||||
- name: Wait for AnsibleAutomationPlatform to be ready
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: aap.ansible.com/v1alpha1
|
||||
kind: AnsibleAutomationPlatform
|
||||
namespace: "{{ aap_operator_namespace }}"
|
||||
name: "{{ aap_operator_platform_name }}"
|
||||
register: __aap_operator_platform_status
|
||||
ignore_errors: true
|
||||
until: >-
|
||||
__aap_operator_platform_status.resources is defined and
|
||||
__aap_operator_platform_status.resources | length > 0 and
|
||||
(__aap_operator_platform_status.resources[0].status.conditions | default([])
|
||||
| selectattr('type', '==', 'Running')
|
||||
| selectattr('status', '==', 'True') | list | length > 0)
|
||||
retries: "{{ __aap_operator_wait_retries }}"
|
||||
delay: 10
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 5: Display summary
|
||||
# ------------------------------------------------------------------
|
||||
- name: Display AAP deployment summary
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Ansible Automation Platform deployment complete!"
|
||||
- " Namespace : {{ aap_operator_namespace }}"
|
||||
- " Platform CR: {{ aap_operator_platform_name }}"
|
||||
- " Controller : {{ 'disabled' if aap_operator_controller_disabled else 'enabled' }}"
|
||||
- " Hub : {{ 'disabled' if aap_operator_hub_disabled else 'enabled' }}"
|
||||
- " EDA : {{ 'disabled' if aap_operator_eda_disabled else 'enabled' }}"
|
||||
- ""
|
||||
- "Admin password secret: {{ aap_operator_platform_name }}-admin-password"
|
||||
- "Retrieve with: oc get secret {{ aap_operator_platform_name }}-admin-password -n {{ aap_operator_namespace }} -o jsonpath='{.data.password}' | base64 -d"
|
||||
3
roles/aap_operator/vars/main.yml
Normal file
3
roles/aap_operator/vars/main.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
# Computed internal variables - do not override
|
||||
__aap_operator_wait_retries: "{{ (aap_operator_wait_timeout / 10) | int }}"
|
||||
38
roles/automation_orchestrator_oidc/defaults/main.yml
Normal file
38
roles/automation_orchestrator_oidc/defaults/main.yml
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
# --- Automation Orchestrator instance ---
|
||||
# Defaults track the automation_orchestrator_operator role's variables when set.
|
||||
automation_orchestrator_oidc_namespace: "{{ automation_orchestrator_operator_app_namespace | default('automation-orchestrator') }}"
|
||||
automation_orchestrator_oidc_instance_name: "{{ automation_orchestrator_operator_instance_name | default('automation-orchestrator') }}"
|
||||
# Public base URL of the AO UI/API; "" derives it from the instance's Route.
|
||||
automation_orchestrator_oidc_url: ""
|
||||
automation_orchestrator_oidc_admin_user: admin
|
||||
automation_orchestrator_oidc_validate_certs: true
|
||||
|
||||
# --- Keycloak ---
|
||||
automation_orchestrator_oidc_keycloak_url: ""
|
||||
automation_orchestrator_oidc_keycloak_realm: ""
|
||||
automation_orchestrator_oidc_keycloak_admin_user: ""
|
||||
automation_orchestrator_oidc_keycloak_admin_password: ""
|
||||
automation_orchestrator_oidc_keycloak_validate_certs: true
|
||||
automation_orchestrator_oidc_client_id: automation-orchestrator
|
||||
# Create the Keycloak groups named in group_mappings (membership is managed in Keycloak).
|
||||
automation_orchestrator_oidc_create_keycloak_groups: true
|
||||
|
||||
# --- AO identity provider ---
|
||||
automation_orchestrator_oidc_provider_name: Keycloak
|
||||
# Keycloak group (from the "groups" claim) -> AO built-in group (admins, auditors, users).
|
||||
# Users in no mapped group are refused login.
|
||||
automation_orchestrator_oidc_group_mappings: []
|
||||
# - idp_group: ao-admins
|
||||
# group: admins
|
||||
automation_orchestrator_oidc_rp_initiated_logout: true
|
||||
# PATCH the provider even when its config is unchanged (e.g. to push a rotated client secret).
|
||||
automation_orchestrator_oidc_force_update: false
|
||||
|
||||
# The backend rejects OIDC issuers that resolve to private IPs unless this is set, and
|
||||
# the AutomationOrchestrator CR has no field for it — so the role sets the env var on the
|
||||
# backend Deployment directly. Re-run the role if the operator ever reverts it.
|
||||
automation_orchestrator_oidc_allow_private_networks: true
|
||||
|
||||
# --- Debugging ---
|
||||
automation_orchestrator_oidc_no_log: true
|
||||
99
roles/automation_orchestrator_oidc/meta/argument_specs.yml
Normal file
99
roles/automation_orchestrator_oidc/meta/argument_specs.yml
Normal file
@@ -0,0 +1,99 @@
|
||||
---
|
||||
argument_specs:
|
||||
main:
|
||||
short_description: Configure Keycloak OIDC login for Automation Orchestrator
|
||||
description:
|
||||
- Creates a confidential OIDC client (with a "groups" claim mapper) and the mapped groups
|
||||
in Keycloak, then registers Keycloak as an identity provider in Automation Orchestrator
|
||||
via its REST API, mapping Keycloak groups to AO built-in groups.
|
||||
- The client secret is read back from Keycloak on each run, so it is not stored elsewhere.
|
||||
- >-
|
||||
Sets APP_OIDC_ALLOW_PRIVATE_NETWORKS on the backend Deployment, since the
|
||||
AutomationOrchestrator CR has no field for it and the backend otherwise rejects
|
||||
issuers resolving to private IPs.
|
||||
options:
|
||||
automation_orchestrator_oidc_namespace:
|
||||
description: Namespace of the AutomationOrchestrator instance.
|
||||
type: str
|
||||
default: automation-orchestrator
|
||||
automation_orchestrator_oidc_instance_name:
|
||||
description: Name of the AutomationOrchestrator CR (prefix of its Route, Deployments and Secrets).
|
||||
type: str
|
||||
default: automation-orchestrator
|
||||
automation_orchestrator_oidc_url:
|
||||
description: Public base URL of Automation Orchestrator. Empty derives it from the instance Route.
|
||||
type: str
|
||||
default: ""
|
||||
automation_orchestrator_oidc_admin_user:
|
||||
description: Local AO admin used to call the API (password read from the initial-admin-password Secret).
|
||||
type: str
|
||||
default: admin
|
||||
automation_orchestrator_oidc_validate_certs:
|
||||
description: Verify TLS when calling the AO API.
|
||||
type: bool
|
||||
default: true
|
||||
automation_orchestrator_oidc_keycloak_url:
|
||||
description: Keycloak base URL (e.g. https://auth.example.com).
|
||||
type: str
|
||||
required: true
|
||||
automation_orchestrator_oidc_keycloak_realm:
|
||||
description: Keycloak realm holding the client and groups.
|
||||
type: str
|
||||
required: true
|
||||
automation_orchestrator_oidc_keycloak_admin_user:
|
||||
description: Keycloak master-realm admin username.
|
||||
type: str
|
||||
required: true
|
||||
automation_orchestrator_oidc_keycloak_admin_password:
|
||||
description: Keycloak master-realm admin password.
|
||||
type: str
|
||||
required: true
|
||||
no_log: true
|
||||
automation_orchestrator_oidc_keycloak_validate_certs:
|
||||
description: Verify TLS when calling the Keycloak admin API.
|
||||
type: bool
|
||||
default: true
|
||||
automation_orchestrator_oidc_client_id:
|
||||
description: Keycloak client ID for Automation Orchestrator.
|
||||
type: str
|
||||
default: automation-orchestrator
|
||||
automation_orchestrator_oidc_create_keycloak_groups:
|
||||
description: Create the Keycloak groups named in group_mappings. Group membership is managed in Keycloak.
|
||||
type: bool
|
||||
default: true
|
||||
automation_orchestrator_oidc_provider_name:
|
||||
description: Name of the identity provider in Automation Orchestrator (shown on the login page).
|
||||
type: str
|
||||
default: Keycloak
|
||||
automation_orchestrator_oidc_group_mappings:
|
||||
description: >-
|
||||
Keycloak group to AO built-in group mappings. Users in no mapped group are refused login.
|
||||
type: list
|
||||
elements: dict
|
||||
required: true
|
||||
options:
|
||||
idp_group:
|
||||
description: Keycloak group name, as emitted in the "groups" claim.
|
||||
type: str
|
||||
required: true
|
||||
group:
|
||||
description: AO built-in group.
|
||||
type: str
|
||||
required: true
|
||||
choices: [admins, auditors, users]
|
||||
automation_orchestrator_oidc_rp_initiated_logout:
|
||||
description: Log users out of Keycloak when they log out of Automation Orchestrator.
|
||||
type: bool
|
||||
default: true
|
||||
automation_orchestrator_oidc_force_update:
|
||||
description: PATCH the identity provider even when unchanged (e.g. to push a rotated client secret).
|
||||
type: bool
|
||||
default: false
|
||||
automation_orchestrator_oidc_allow_private_networks:
|
||||
description: Set APP_OIDC_ALLOW_PRIVATE_NETWORKS=true on the backend Deployment.
|
||||
type: bool
|
||||
default: true
|
||||
automation_orchestrator_oidc_no_log:
|
||||
description: Set to false to expose secret values in task output. Never set false in shared environments.
|
||||
type: bool
|
||||
default: true
|
||||
17
roles/automation_orchestrator_oidc/meta/main.yml
Normal file
17
roles/automation_orchestrator_oidc/meta/main.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: ptoal
|
||||
description: Configure Keycloak OIDC login for Automation Orchestrator
|
||||
license: MIT
|
||||
min_ansible_version: "2.16"
|
||||
platforms:
|
||||
- name: GenericLinux
|
||||
versions:
|
||||
- all
|
||||
galaxy_tags:
|
||||
- openshift
|
||||
- aap
|
||||
- keycloak
|
||||
- oidc
|
||||
|
||||
dependencies: []
|
||||
286
roles/automation_orchestrator_oidc/tasks/main.yml
Normal file
286
roles/automation_orchestrator_oidc/tasks/main.yml
Normal file
@@ -0,0 +1,286 @@
|
||||
---
|
||||
# Configure Keycloak OIDC login for Automation Orchestrator.
|
||||
#
|
||||
# 1. Keycloak: mapped groups + confidential client with a "groups" claim mapper;
|
||||
# the client secret is read back rather than stored elsewhere.
|
||||
# 2. Backend Deployment: APP_OIDC_ALLOW_PRIVATE_NETWORKS (no CR field exists for it).
|
||||
# 3. AO REST API: create/update the identity provider with group mappings.
|
||||
#
|
||||
# AO identity providers are runtime configuration in the AO database — the
|
||||
# AutomationOrchestrator CR has no auth fields.
|
||||
|
||||
- name: Validate group mappings
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- automation_orchestrator_oidc_group_mappings | length > 0
|
||||
- >-
|
||||
automation_orchestrator_oidc_group_mappings | map(attribute='group')
|
||||
| difference(__automation_orchestrator_oidc_valid_groups) | length == 0
|
||||
fail_msg: >-
|
||||
automation_orchestrator_oidc_group_mappings must map at least one Keycloak group to an
|
||||
AO built-in group ({{ __automation_orchestrator_oidc_valid_groups | join(', ') }});
|
||||
otherwise no Keycloak user can log in.
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 1: Resolve the Automation Orchestrator public URL
|
||||
# ------------------------------------------------------------------
|
||||
- name: Read Automation Orchestrator Route
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: route.openshift.io/v1
|
||||
kind: Route
|
||||
namespace: "{{ automation_orchestrator_oidc_namespace }}"
|
||||
name: "{{ automation_orchestrator_oidc_instance_name }}"
|
||||
register: __automation_orchestrator_oidc_route
|
||||
when: automation_orchestrator_oidc_url | length == 0
|
||||
failed_when: __automation_orchestrator_oidc_route.resources | length == 0
|
||||
|
||||
- name: Set Automation Orchestrator URL
|
||||
ansible.builtin.set_fact:
|
||||
__automation_orchestrator_oidc_url: >-
|
||||
{{ (automation_orchestrator_oidc_url
|
||||
if automation_orchestrator_oidc_url | length > 0
|
||||
else 'https://' + __automation_orchestrator_oidc_route.resources[0].spec.host)
|
||||
| regex_replace('/$', '') }}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 2: Keycloak client and groups
|
||||
# ------------------------------------------------------------------
|
||||
- name: Configure Keycloak
|
||||
module_defaults:
|
||||
community.general.keycloak_group: &keycloak_auth
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: "{{ automation_orchestrator_oidc_keycloak_url }}"
|
||||
auth_realm: master
|
||||
auth_username: "{{ automation_orchestrator_oidc_keycloak_admin_user }}"
|
||||
auth_password: "{{ automation_orchestrator_oidc_keycloak_admin_password }}"
|
||||
validate_certs: "{{ automation_orchestrator_oidc_keycloak_validate_certs }}"
|
||||
community.general.keycloak_clientsecret_info: *keycloak_auth
|
||||
middleware_automation.keycloak.keycloak_client: *keycloak_auth
|
||||
block:
|
||||
- name: Create mapped Keycloak groups
|
||||
community.general.keycloak_group:
|
||||
realm: "{{ automation_orchestrator_oidc_keycloak_realm }}"
|
||||
name: "{{ item }}"
|
||||
state: present
|
||||
loop: "{{ automation_orchestrator_oidc_group_mappings | map(attribute='idp_group') | unique }}"
|
||||
when: automation_orchestrator_oidc_create_keycloak_groups | bool
|
||||
|
||||
# No secret is passed: Keycloak generates one on create and keeps it on update.
|
||||
- name: Create Automation Orchestrator OIDC client in Keycloak
|
||||
middleware_automation.keycloak.keycloak_client:
|
||||
realm: "{{ automation_orchestrator_oidc_keycloak_realm }}"
|
||||
client_id: "{{ automation_orchestrator_oidc_client_id }}"
|
||||
name: Automation Orchestrator
|
||||
description: OIDC client for Automation Orchestrator at {{ __automation_orchestrator_oidc_url }}
|
||||
enabled: true
|
||||
protocol: openid-connect
|
||||
public_client: false
|
||||
standard_flow_enabled: true
|
||||
implicit_flow_enabled: false
|
||||
direct_access_grants_enabled: false
|
||||
service_accounts_enabled: false
|
||||
root_url: "{{ __automation_orchestrator_oidc_url }}"
|
||||
redirect_uris:
|
||||
- "{{ __automation_orchestrator_oidc_url }}/api/v1/auth/oidc/callback"
|
||||
web_origins:
|
||||
- "+"
|
||||
attributes:
|
||||
post.logout.redirect.uris: "{{ __automation_orchestrator_oidc_url }}##{{ __automation_orchestrator_oidc_url }}/*"
|
||||
protocol_mappers:
|
||||
- name: groups
|
||||
protocol: openid-connect
|
||||
protocolMapper: oidc-group-membership-mapper
|
||||
config:
|
||||
full.path: "false"
|
||||
id.token.claim: "true"
|
||||
access.token.claim: "true"
|
||||
userinfo.token.claim: "true"
|
||||
claim.name: groups
|
||||
state: present
|
||||
no_log: "{{ automation_orchestrator_oidc_no_log }}"
|
||||
|
||||
- name: Read Automation Orchestrator client secret from Keycloak
|
||||
community.general.keycloak_clientsecret_info:
|
||||
realm: "{{ automation_orchestrator_oidc_keycloak_realm }}"
|
||||
client_id: "{{ automation_orchestrator_oidc_client_id }}"
|
||||
register: __automation_orchestrator_oidc_clientsecret
|
||||
no_log: "{{ automation_orchestrator_oidc_no_log }}"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 3: Allow private-network issuers on the backend
|
||||
# ------------------------------------------------------------------
|
||||
- name: Set APP_OIDC_ALLOW_PRIVATE_NETWORKS on backend Deployment
|
||||
kubernetes.core.k8s:
|
||||
state: patched
|
||||
merge_type: strategic-merge
|
||||
api_version: apps/v1
|
||||
kind: Deployment
|
||||
namespace: "{{ automation_orchestrator_oidc_namespace }}"
|
||||
name: "{{ automation_orchestrator_oidc_instance_name }}-backend"
|
||||
definition:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: backend
|
||||
env:
|
||||
- name: APP_OIDC_ALLOW_PRIVATE_NETWORKS
|
||||
value: "{{ automation_orchestrator_oidc_allow_private_networks | string | lower }}"
|
||||
register: __automation_orchestrator_oidc_backend_patch
|
||||
|
||||
- name: Wait for backend rollout
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: apps/v1
|
||||
kind: Deployment
|
||||
namespace: "{{ automation_orchestrator_oidc_namespace }}"
|
||||
name: "{{ automation_orchestrator_oidc_instance_name }}-backend"
|
||||
register: __automation_orchestrator_oidc_backend
|
||||
until: >-
|
||||
__automation_orchestrator_oidc_backend.resources | length > 0 and
|
||||
(__automation_orchestrator_oidc_backend.resources[0].status.observedGeneration | default(0)) >=
|
||||
__automation_orchestrator_oidc_backend.resources[0].metadata.generation and
|
||||
(__automation_orchestrator_oidc_backend.resources[0].status.updatedReplicas | default(0)) ==
|
||||
__automation_orchestrator_oidc_backend.resources[0].spec.replicas and
|
||||
(__automation_orchestrator_oidc_backend.resources[0].status.availableReplicas | default(0)) ==
|
||||
__automation_orchestrator_oidc_backend.resources[0].spec.replicas
|
||||
retries: 30
|
||||
delay: 10
|
||||
when: __automation_orchestrator_oidc_backend_patch is changed
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 4: Register Keycloak as an AO identity provider
|
||||
# ------------------------------------------------------------------
|
||||
- name: Read AO initial admin password
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: v1
|
||||
kind: Secret
|
||||
namespace: "{{ automation_orchestrator_oidc_namespace }}"
|
||||
name: "{{ automation_orchestrator_oidc_instance_name }}-initial-admin-password"
|
||||
register: __automation_orchestrator_oidc_admin_secret
|
||||
no_log: "{{ automation_orchestrator_oidc_no_log }}"
|
||||
|
||||
- name: Log in to the AO API
|
||||
ansible.builtin.uri:
|
||||
url: "{{ __automation_orchestrator_oidc_url }}/api/v1/auth/login"
|
||||
method: POST
|
||||
body_format: json
|
||||
body:
|
||||
username: "{{ automation_orchestrator_oidc_admin_user }}"
|
||||
password: "{{ __automation_orchestrator_oidc_admin_secret.resources[0].data.password | b64decode }}"
|
||||
validate_certs: "{{ automation_orchestrator_oidc_validate_certs }}"
|
||||
register: __automation_orchestrator_oidc_login
|
||||
no_log: "{{ automation_orchestrator_oidc_no_log }}"
|
||||
|
||||
- name: Call the AO API
|
||||
module_defaults:
|
||||
ansible.builtin.uri:
|
||||
headers:
|
||||
Authorization: "Bearer {{ __automation_orchestrator_oidc_login.json.access_token }}"
|
||||
validate_certs: "{{ automation_orchestrator_oidc_validate_certs }}"
|
||||
block:
|
||||
- name: List AO groups
|
||||
ansible.builtin.uri:
|
||||
url: "{{ __automation_orchestrator_oidc_url }}/api/v1/groups?limit=100"
|
||||
register: __automation_orchestrator_oidc_groups
|
||||
|
||||
- name: List AO identity providers
|
||||
ansible.builtin.uri:
|
||||
url: "{{ __automation_orchestrator_oidc_url }}/api/v1/identity_providers?limit=100"
|
||||
register: __automation_orchestrator_oidc_idps
|
||||
|
||||
- name: Build desired identity provider configuration
|
||||
ansible.builtin.set_fact:
|
||||
__automation_orchestrator_oidc_existing: >-
|
||||
{{ __automation_orchestrator_oidc_idps.json.resources
|
||||
| selectattr('name', '==', automation_orchestrator_oidc_provider_name) | first | default({}) }}
|
||||
__automation_orchestrator_oidc_config:
|
||||
provider_type: oidc
|
||||
idp_type: custom
|
||||
auto_discovery: true
|
||||
issuer_url: "{{ __automation_orchestrator_oidc_issuer }}"
|
||||
client_id: "{{ automation_orchestrator_oidc_client_id }}"
|
||||
redirect_uri: "{{ __automation_orchestrator_oidc_url }}/api/v1/auth/oidc/callback"
|
||||
scopes: openid profile email
|
||||
group_jmespath_expression: groups
|
||||
allow_all_authenticated: false
|
||||
enable_rp_initiated_logout: "{{ automation_orchestrator_oidc_rp_initiated_logout | bool }}"
|
||||
__automation_orchestrator_oidc_group_ids: >-
|
||||
{{ __automation_orchestrator_oidc_groups.json.resources | items2dict(key_name='name', value_name='id') }}
|
||||
__automation_orchestrator_oidc_entries: []
|
||||
|
||||
- name: Build group mapping entries
|
||||
ansible.builtin.set_fact:
|
||||
__automation_orchestrator_oidc_entries: >-
|
||||
{{ __automation_orchestrator_oidc_entries + [{
|
||||
'idp_group_value': item.idp_group,
|
||||
'mapped_group_id': __automation_orchestrator_oidc_group_ids[item.group]}] }}
|
||||
loop: "{{ automation_orchestrator_oidc_group_mappings }}"
|
||||
loop_control:
|
||||
label: "{{ item.idp_group }} -> {{ item.group }}"
|
||||
|
||||
# Compare everything except client_secret (never returned by the API).
|
||||
- name: Determine whether the identity provider needs updating
|
||||
ansible.builtin.set_fact:
|
||||
__automation_orchestrator_oidc_needs_update: >-
|
||||
{{ automation_orchestrator_oidc_force_update | bool
|
||||
or not (__automation_orchestrator_oidc_existing.enabled | default(false))
|
||||
or (__automation_orchestrator_oidc_existing.configuration | default({}))
|
||||
!= ((__automation_orchestrator_oidc_existing.configuration | default({}))
|
||||
| combine(__automation_orchestrator_oidc_config))
|
||||
or ((__automation_orchestrator_oidc_existing.configuration.group_mapping_entries | default([])
|
||||
| map(attribute='idp_group_value') | zip(__automation_orchestrator_oidc_existing.configuration.group_mapping_entries
|
||||
| default([]) | map(attribute='mapped_group_id')) | map('join', '=') | sort)
|
||||
!= (__automation_orchestrator_oidc_entries | map(attribute='idp_group_value')
|
||||
| zip(__automation_orchestrator_oidc_entries | map(attribute='mapped_group_id'))
|
||||
| map('join', '=') | sort)) }}
|
||||
when: __automation_orchestrator_oidc_existing | length > 0
|
||||
|
||||
- name: Create Keycloak identity provider in AO
|
||||
ansible.builtin.uri:
|
||||
url: "{{ __automation_orchestrator_oidc_url }}/api/v1/identity_providers"
|
||||
method: POST
|
||||
body_format: json
|
||||
body:
|
||||
name: "{{ automation_orchestrator_oidc_provider_name }}"
|
||||
description: Keycloak realm {{ automation_orchestrator_oidc_keycloak_realm }}
|
||||
configuration: >-
|
||||
{{ __automation_orchestrator_oidc_config | combine({
|
||||
'client_secret': __automation_orchestrator_oidc_clientsecret.clientsecret_info.value,
|
||||
'group_mapping_entries': __automation_orchestrator_oidc_entries}) }}
|
||||
status_code: 201
|
||||
changed_when: true
|
||||
when: __automation_orchestrator_oidc_existing | length == 0
|
||||
no_log: "{{ automation_orchestrator_oidc_no_log }}"
|
||||
|
||||
- name: Update Keycloak identity provider in AO
|
||||
ansible.builtin.uri:
|
||||
url: "{{ __automation_orchestrator_oidc_url }}/api/v1/identity_providers/{{ __automation_orchestrator_oidc_existing.id }}"
|
||||
method: PATCH
|
||||
body_format: json
|
||||
body:
|
||||
enabled: true
|
||||
configuration: >-
|
||||
{{ __automation_orchestrator_oidc_config | combine({
|
||||
'client_secret': __automation_orchestrator_oidc_clientsecret.clientsecret_info.value,
|
||||
'group_mapping_entries': __automation_orchestrator_oidc_entries}) }}
|
||||
changed_when: true
|
||||
when:
|
||||
- __automation_orchestrator_oidc_existing | length > 0
|
||||
- __automation_orchestrator_oidc_needs_update | bool
|
||||
no_log: "{{ automation_orchestrator_oidc_no_log }}"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 5: Summary
|
||||
# ------------------------------------------------------------------
|
||||
- name: Display Automation Orchestrator OIDC summary
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Keycloak OIDC configured for Automation Orchestrator:"
|
||||
- " Login URL : {{ __automation_orchestrator_oidc_url }}"
|
||||
- " Issuer : {{ __automation_orchestrator_oidc_issuer }}"
|
||||
- " Client : {{ automation_orchestrator_oidc_client_id }}"
|
||||
- " Mappings : {{ automation_orchestrator_oidc_group_mappings | map(attribute='idp_group')
|
||||
| zip(automation_orchestrator_oidc_group_mappings | map(attribute='group'))
|
||||
| map('join', ' -> ') | join(', ') }}"
|
||||
- ""
|
||||
- "Add users to the mapped Keycloak groups to grant access."
|
||||
8
roles/automation_orchestrator_oidc/vars/main.yml
Normal file
8
roles/automation_orchestrator_oidc/vars/main.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
# Computed internal variables - do not override
|
||||
__automation_orchestrator_oidc_valid_groups:
|
||||
- admins
|
||||
- auditors
|
||||
- users
|
||||
__automation_orchestrator_oidc_issuer: >-
|
||||
{{ automation_orchestrator_oidc_keycloak_url | regex_replace('/$', '') }}/realms/{{ automation_orchestrator_oidc_keycloak_realm }}
|
||||
81
roles/automation_orchestrator_operator/defaults/main.yml
Normal file
81
roles/automation_orchestrator_operator/defaults/main.yml
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
# --- OLM subscription ---
|
||||
# Automation Orchestrator is published in redhat-operators. To install a pre-release
|
||||
# build instead (as aapctl's scripts/create-catalogsources.sh does), set:
|
||||
# automation_orchestrator_operator_catalog_source_name: cs-automation-orchestrator
|
||||
# automation_orchestrator_operator_catalog_image: quay.io/aap/ansible-automation-platform/automation-orchestrator-operator-index@sha256:...
|
||||
# plus automation_orchestrator_operator_image_digest_mirrors below.
|
||||
#
|
||||
# To adopt an existing install (e.g. from OperatorHub), point operator_namespace at
|
||||
# it: an existing OperatorGroup in that namespace is reused, not duplicated, and an
|
||||
# already-installed CSV skips InstallPlan approval.
|
||||
automation_orchestrator_operator_operator_namespace: automation-orchestrator
|
||||
automation_orchestrator_operator_channel: stable
|
||||
automation_orchestrator_operator_name: automation-orchestrator-operator
|
||||
automation_orchestrator_operator_catalog_source_name: redhat-operators
|
||||
# Index image for a custom CatalogSource; "" uses the existing catalog_source_name as-is.
|
||||
automation_orchestrator_operator_catalog_image: ""
|
||||
# Manual: the role approves the initial InstallPlan only (upgrades stay pending).
|
||||
automation_orchestrator_operator_install_plan_approval: Manual
|
||||
automation_orchestrator_operator_wait_timeout: 600
|
||||
|
||||
# --- Pre-release image mirrors (ImageDigestMirrorSet) ---
|
||||
# Only needed for pre-release builds whose images are not on registry.redhat.io.
|
||||
# Cluster-wide; applying it triggers a MachineConfigPool rollout. Pre-release example:
|
||||
# - source: registry.redhat.io/ansible-automation-platform-27-next
|
||||
# mirrors: [quay.io/aap/ansible-automation-platform-27-next]
|
||||
# - source: registry.redhat.io/ansible-automation-platform-27
|
||||
# mirrors: [quay.io/aap/ansible-automation-platform-27-next]
|
||||
automation_orchestrator_operator_idms_name: idms-aap-27-next
|
||||
automation_orchestrator_operator_image_digest_mirrors: []
|
||||
automation_orchestrator_operator_mcp_wait_timeout: 1800
|
||||
|
||||
# --- AutomationOrchestrator CR ---
|
||||
# Same as the operator namespace by default (aapctl runs OLM + CR in one namespace);
|
||||
# override only if you need to split the cluster-scoped operator from the app instance.
|
||||
automation_orchestrator_operator_app_namespace: "{{ automation_orchestrator_operator_operator_namespace }}"
|
||||
automation_orchestrator_operator_instance_name: automation-orchestrator
|
||||
|
||||
# --- PostgreSQL (bring-your-own) ---
|
||||
# Defaults match the Secrets/Service created by the cloudnativepg_operator role
|
||||
# (run it first, or point these at a customer-managed PostgreSQL instance instead).
|
||||
automation_orchestrator_operator_postgres_host: orchestrator-postgres-rw
|
||||
automation_orchestrator_operator_postgres_port: 5432
|
||||
automation_orchestrator_operator_postgres_ssl_mode: verify-ca
|
||||
# Name of a pre-existing Secret (key ca.crt). CloudNativePG auto-generates
|
||||
# "<cluster-name>-ca" for its Cluster CR. Only used with sslMode verify-ca/verify-full.
|
||||
automation_orchestrator_operator_postgres_ca_cert_secret: orchestrator-postgres-ca
|
||||
|
||||
# Names of pre-existing Secrets (keys: database, username, password) — created by
|
||||
# cloudnativepg_operator, or your own Secrets when using a customer-managed PostgreSQL.
|
||||
automation_orchestrator_operator_backend_db_secret_name: orchestrator-postgres-secret
|
||||
automation_orchestrator_operator_temporal_db_secret_name: temporal-postgres-secret
|
||||
|
||||
# --- Ingress ---
|
||||
# Route (OpenShift) or Ingress (AKS); the operator manages the resource itself.
|
||||
automation_orchestrator_operator_ingress_type: Route
|
||||
automation_orchestrator_operator_ingress_host: ""
|
||||
automation_orchestrator_operator_ingress_annotations: {}
|
||||
# Only used when ingress_type is Ingress — name of a pre-existing kubernetes.io/tls Secret.
|
||||
automation_orchestrator_operator_ingress_tls_secret_name: ""
|
||||
|
||||
# --- Component workload images (only needed for private registries) ---
|
||||
automation_orchestrator_operator_image_pull_secrets: []
|
||||
|
||||
# --- Per-component overrides (replicas, resources, affinity, tolerations, nodeSelector) ---
|
||||
# Passed through as-is into the matching spec.<component> block; leave {} for operator defaults.
|
||||
automation_orchestrator_operator_backend: {}
|
||||
automation_orchestrator_operator_ui: {}
|
||||
automation_orchestrator_operator_worker: {}
|
||||
automation_orchestrator_operator_background_worker: {}
|
||||
automation_orchestrator_operator_redis: {}
|
||||
automation_orchestrator_operator_temporal_server: {}
|
||||
|
||||
# Escape hatch: recursively merged into the generated spec last, for fields
|
||||
# (fileStorage, otel, tls, secrets, workflowHttpRequestAllowedHosts, ...) not
|
||||
# otherwise exposed as dedicated variables above.
|
||||
automation_orchestrator_operator_extra_spec: {}
|
||||
|
||||
# --- Debugging ---
|
||||
# Set to false to disable no_log and expose secret values in task output.
|
||||
automation_orchestrator_operator_no_log: true
|
||||
158
roles/automation_orchestrator_operator/meta/argument_specs.yml
Normal file
158
roles/automation_orchestrator_operator/meta/argument_specs.yml
Normal file
@@ -0,0 +1,158 @@
|
||||
---
|
||||
argument_specs:
|
||||
main:
|
||||
short_description: Install Automation Orchestrator via its OLM operator (AutomationOrchestrator CR)
|
||||
description:
|
||||
- Installs the Automation Orchestrator operator via OLM (redhat-operators by default, or a
|
||||
custom CatalogSource for pre-release builds) and creates a single
|
||||
AutomationOrchestrator CR. Adopts an existing install (OperatorGroup, installed CSV)
|
||||
when operator_namespace points at one.
|
||||
- >-
|
||||
Automation Orchestrator is bring-your-own-PostgreSQL: this role only references
|
||||
existing K8s Secrets (keys: database, username, password) — it does not create the
|
||||
database/user or their credential Secret. Defaults match the cloudnativepg_operator
|
||||
role's output; run that role first, or point postgres_* variables and the secret-name
|
||||
variables at a customer-managed PostgreSQL instance.
|
||||
- Modeled on https://github.com/ansible/aapctl and https://github.com/automation-nexus/automation-orchestrator-operator.
|
||||
options:
|
||||
automation_orchestrator_operator_operator_namespace:
|
||||
description: Namespace for the Automation Orchestrator operator and (by default) its CR.
|
||||
type: str
|
||||
default: automation-orchestrator
|
||||
automation_orchestrator_operator_channel:
|
||||
description: OLM subscription channel.
|
||||
type: str
|
||||
default: stable
|
||||
automation_orchestrator_operator_name:
|
||||
description: Operator package name in the catalog.
|
||||
type: str
|
||||
default: automation-orchestrator-operator
|
||||
automation_orchestrator_operator_catalog_source_name:
|
||||
description: CatalogSource to subscribe from (created by this role only when catalog_image is set).
|
||||
type: str
|
||||
default: redhat-operators
|
||||
automation_orchestrator_operator_catalog_image:
|
||||
description: >-
|
||||
Index image for a custom CatalogSource (e.g. a pre-release build). Empty string
|
||||
uses the existing catalog_source_name without creating a CatalogSource.
|
||||
type: str
|
||||
default: ""
|
||||
automation_orchestrator_operator_install_plan_approval:
|
||||
description: >-
|
||||
Subscription installPlanApproval (Manual or Automatic). With Manual, the role approves
|
||||
only the initial InstallPlan; once a CSV is installed, upgrades are left pending.
|
||||
type: str
|
||||
choices: [Manual, Automatic]
|
||||
default: Manual
|
||||
automation_orchestrator_operator_wait_timeout:
|
||||
description: Seconds to wait for the operator and instance to become ready.
|
||||
type: int
|
||||
default: 600
|
||||
automation_orchestrator_operator_idms_name:
|
||||
description: Name of the cluster-wide ImageDigestMirrorSet for pre-release images.
|
||||
type: str
|
||||
default: idms-aap-27-next
|
||||
automation_orchestrator_operator_image_digest_mirrors:
|
||||
description: >-
|
||||
spec.imageDigestMirrors entries ({source, mirrors}) for the IDMS. Applying a change
|
||||
triggers a MachineConfigPool rollout. Only needed for pre-release builds; [] skips it.
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
automation_orchestrator_operator_mcp_wait_timeout:
|
||||
description: Seconds to wait for the MachineConfigPool rollout after the IDMS changes.
|
||||
type: int
|
||||
default: 1800
|
||||
automation_orchestrator_operator_app_namespace:
|
||||
description: >-
|
||||
Namespace for the AutomationOrchestrator instance. Exactly one CR is allowed per namespace.
|
||||
Defaults to the same namespace as the operator.
|
||||
type: str
|
||||
default: "{operator_namespace}"
|
||||
automation_orchestrator_operator_instance_name:
|
||||
description: Name of the AutomationOrchestrator CR.
|
||||
type: str
|
||||
default: automation-orchestrator
|
||||
automation_orchestrator_operator_postgres_host:
|
||||
description: PostgreSQL host shared by the backend and Temporal databases.
|
||||
type: str
|
||||
default: orchestrator-postgres-rw
|
||||
automation_orchestrator_operator_postgres_port:
|
||||
description: PostgreSQL port.
|
||||
type: int
|
||||
default: 5432
|
||||
automation_orchestrator_operator_postgres_ssl_mode:
|
||||
description: PostgreSQL SSL mode (disable, allow, prefer, require, verify-ca, verify-full).
|
||||
type: str
|
||||
default: verify-ca
|
||||
automation_orchestrator_operator_postgres_ca_cert_secret:
|
||||
description: >-
|
||||
Name of a pre-existing Secret (key ca.crt) with the PostgreSQL server CA certificate.
|
||||
Required when sslMode is verify-ca or verify-full.
|
||||
type: str
|
||||
default: orchestrator-postgres-ca
|
||||
automation_orchestrator_operator_backend_db_secret_name:
|
||||
description: Name of the pre-existing Secret with the backend database credentials.
|
||||
type: str
|
||||
default: orchestrator-postgres-secret
|
||||
automation_orchestrator_operator_temporal_db_secret_name:
|
||||
description: Name of the pre-existing Secret with the Temporal database credentials.
|
||||
type: str
|
||||
default: temporal-postgres-secret
|
||||
automation_orchestrator_operator_ingress_type:
|
||||
description: External access type (Route on OpenShift, Ingress on AKS, or None).
|
||||
type: str
|
||||
default: Route
|
||||
automation_orchestrator_operator_ingress_host:
|
||||
description: Custom hostname for external access. Leave unset to use the operator's auto-generated hostname.
|
||||
type: str
|
||||
default: ""
|
||||
automation_orchestrator_operator_ingress_annotations:
|
||||
description: Annotations applied to the generated Route/Ingress resource.
|
||||
type: dict
|
||||
default: {}
|
||||
automation_orchestrator_operator_ingress_tls_secret_name:
|
||||
description: Name of a pre-existing kubernetes.io/tls Secret. Only used when ingress_type is Ingress.
|
||||
type: str
|
||||
default: ""
|
||||
automation_orchestrator_operator_image_pull_secrets:
|
||||
description: List of {name} references to Secrets used to pull component workload images.
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
automation_orchestrator_operator_backend:
|
||||
description: Passthrough overrides (replicas, resources, affinity, tolerations, nodeSelector) for spec.backend.
|
||||
type: dict
|
||||
default: {}
|
||||
automation_orchestrator_operator_ui:
|
||||
description: Passthrough overrides for spec.ui.
|
||||
type: dict
|
||||
default: {}
|
||||
automation_orchestrator_operator_worker:
|
||||
description: Passthrough overrides for spec.worker.
|
||||
type: dict
|
||||
default: {}
|
||||
automation_orchestrator_operator_background_worker:
|
||||
description: >-
|
||||
Passthrough overrides for spec.backgroundWorker. Note: replicas has no effect —
|
||||
the operator always manages this component via an HPA.
|
||||
type: dict
|
||||
default: {}
|
||||
automation_orchestrator_operator_redis:
|
||||
description: Passthrough overrides for spec.redis.
|
||||
type: dict
|
||||
default: {}
|
||||
automation_orchestrator_operator_temporal_server:
|
||||
description: Passthrough overrides for spec.temporal.server.
|
||||
type: dict
|
||||
default: {}
|
||||
automation_orchestrator_operator_extra_spec:
|
||||
description: >-
|
||||
Arbitrary dict recursively merged into the generated CR spec last — escape hatch for
|
||||
fields not otherwise exposed (fileStorage, otel, tls, secrets, workflowHttpRequestAllowedHosts, ...).
|
||||
type: dict
|
||||
default: {}
|
||||
automation_orchestrator_operator_no_log:
|
||||
description: Set to false to disable no_log and expose secret values in task output. Never set false in shared environments.
|
||||
type: bool
|
||||
default: true
|
||||
18
roles/automation_orchestrator_operator/meta/main.yml
Normal file
18
roles/automation_orchestrator_operator/meta/main.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: ptoal
|
||||
description: Install Automation Orchestrator via its OpenShift/AKS OLM operator
|
||||
license: MIT
|
||||
min_ansible_version: "2.16"
|
||||
platforms:
|
||||
- name: GenericLinux
|
||||
versions:
|
||||
- all
|
||||
galaxy_tags:
|
||||
- openshift
|
||||
- aap
|
||||
- operator
|
||||
- olm
|
||||
- ansible
|
||||
|
||||
dependencies: []
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
# Apply the ImageDigestMirrorSet that redirects pre-release AAP 2.7 images from
|
||||
# registry.redhat.io to quay.io, then wait for the resulting MachineConfigPool
|
||||
# rollout (matches https://github.com/ansible/aapctl scripts/create-catalogsources.sh).
|
||||
#
|
||||
# The IDMS is cluster-wide: it affects every digest pull from the source repos,
|
||||
# not just Automation Orchestrator. The API may be briefly unavailable during the
|
||||
# rollout on SNO, so the waits tolerate failed reads and keep retrying.
|
||||
|
||||
- name: Read MachineConfigPools before applying IDMS
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: machineconfiguration.openshift.io/v1
|
||||
kind: MachineConfigPool
|
||||
register: __automation_orchestrator_operator_mcp_before
|
||||
|
||||
- name: Create ImageDigestMirrorSet for pre-release images
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: config.openshift.io/v1
|
||||
kind: ImageDigestMirrorSet
|
||||
metadata:
|
||||
name: "{{ automation_orchestrator_operator_idms_name }}"
|
||||
spec:
|
||||
imageDigestMirrors: "{{ automation_orchestrator_operator_image_digest_mirrors }}"
|
||||
register: __automation_orchestrator_operator_idms
|
||||
|
||||
# Without this, the Updated wait below could pass on the pools' pre-IDMS state
|
||||
# before the MCO has rendered the new MachineConfig.
|
||||
- name: Wait for MCO to render new MachineConfigs
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: machineconfiguration.openshift.io/v1
|
||||
kind: MachineConfigPool
|
||||
register: __automation_orchestrator_operator_mcp
|
||||
until: >-
|
||||
(__automation_orchestrator_operator_mcp.resources | default([]) | length) > 0 and
|
||||
(__automation_orchestrator_operator_mcp.resources
|
||||
| map(attribute='spec.configuration.name')
|
||||
| intersect(__automation_orchestrator_operator_mcp_before.resources | map(attribute='spec.configuration.name'))
|
||||
| length) == 0
|
||||
retries: "{{ __automation_orchestrator_operator_mcp_wait_retries }}"
|
||||
delay: 10
|
||||
when: __automation_orchestrator_operator_idms is changed
|
||||
|
||||
# Always runs, so a re-run also catches a rollout interrupted on a previous run.
|
||||
- name: Wait for MachineConfigPool rollout to complete
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: machineconfiguration.openshift.io/v1
|
||||
kind: MachineConfigPool
|
||||
register: __automation_orchestrator_operator_mcp
|
||||
until: >-
|
||||
(__automation_orchestrator_operator_mcp.resources | default([]) | length) > 0 and
|
||||
(__automation_orchestrator_operator_mcp.resources | map(attribute='spec.configuration.name') | list) ==
|
||||
(__automation_orchestrator_operator_mcp.resources | map(attribute='status.configuration.name', default='') | list) and
|
||||
(__automation_orchestrator_operator_mcp.resources
|
||||
| map(attribute='status.conditions', default=[])
|
||||
| map('selectattr', 'type', '==', 'Updated')
|
||||
| map('selectattr', 'status', '==', 'True')
|
||||
| map('list')
|
||||
| reject('equalto', [])
|
||||
| list | length) == (__automation_orchestrator_operator_mcp.resources | length)
|
||||
retries: "{{ __automation_orchestrator_operator_mcp_wait_retries }}"
|
||||
delay: 10
|
||||
271
roles/automation_orchestrator_operator/tasks/main.yml
Normal file
271
roles/automation_orchestrator_operator/tasks/main.yml
Normal file
@@ -0,0 +1,271 @@
|
||||
---
|
||||
# Install Automation Orchestrator via its OpenShift OLM operator.
|
||||
#
|
||||
# Automation Orchestrator is bring-your-own-PostgreSQL: this role only creates the
|
||||
# OperatorGroup/Subscription and the AutomationOrchestrator CR — it references
|
||||
# existing K8s Secrets (keys: database/username/password, or host/port/database/
|
||||
# username/password for CNPG's basic-auth secrets) rather than creating them.
|
||||
# Pair with the cloudnativepg_operator role (defaults match its secret names) or
|
||||
# point postgres_* variables at a customer-managed PostgreSQL instance.
|
||||
#
|
||||
# Modeled on https://github.com/ansible/aapctl (internal/install/automationorchestrator)
|
||||
# and https://github.com/automation-nexus/automation-orchestrator-operator.
|
||||
# With Manual install-plan approval (the default) this role approves the initial
|
||||
# InstallPlan itself; later upgrade InstallPlans are left for manual approval.
|
||||
#
|
||||
# All tasks are idempotent (kubernetes.core.k8s state: present).
|
||||
|
||||
- name: Verify PostgreSQL host is set
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- automation_orchestrator_operator_postgres_host | length > 0
|
||||
fail_msg: >-
|
||||
automation_orchestrator_operator_postgres_host must be set. Automation Orchestrator is
|
||||
bring-your-own-PostgreSQL — either run the cloudnativepg_operator role first (its defaults
|
||||
match this role's) or point this at a customer-managed PostgreSQL instance.
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 0: Mirror pre-release AAP 2.7 images (IDMS) — triggers an MCP rollout
|
||||
# ------------------------------------------------------------------
|
||||
- name: Configure pre-release image mirrors
|
||||
ansible.builtin.include_tasks: image_mirrors.yml
|
||||
when: automation_orchestrator_operator_image_digest_mirrors | length > 0
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 1: Install Automation Orchestrator operator via OLM
|
||||
# ------------------------------------------------------------------
|
||||
- name: Create Automation Orchestrator namespace
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: "{{ automation_orchestrator_operator_operator_namespace }}"
|
||||
|
||||
- name: Create Automation Orchestrator app namespace
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: "{{ automation_orchestrator_operator_app_namespace }}"
|
||||
when: automation_orchestrator_operator_app_namespace != automation_orchestrator_operator_operator_namespace
|
||||
|
||||
- name: Create CatalogSource for Automation Orchestrator
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: operators.coreos.com/v1alpha1
|
||||
kind: CatalogSource
|
||||
metadata:
|
||||
name: "{{ automation_orchestrator_operator_catalog_source_name }}"
|
||||
namespace: openshift-marketplace
|
||||
spec:
|
||||
sourceType: grpc
|
||||
image: "{{ automation_orchestrator_operator_catalog_image }}"
|
||||
displayName: Automation Orchestrator
|
||||
when: automation_orchestrator_operator_catalog_image | length > 0
|
||||
|
||||
# OLM allows only one OperatorGroup per namespace; reuse any existing one
|
||||
# (e.g. the auto-named group an OperatorHub install creates).
|
||||
- name: Look up existing OperatorGroups
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: operators.coreos.com/v1
|
||||
kind: OperatorGroup
|
||||
namespace: "{{ automation_orchestrator_operator_operator_namespace }}"
|
||||
register: __automation_orchestrator_operator_ogs
|
||||
|
||||
- name: Create OperatorGroup for Automation Orchestrator
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: operators.coreos.com/v1
|
||||
kind: OperatorGroup
|
||||
metadata:
|
||||
name: "{{ automation_orchestrator_operator_name }}"
|
||||
namespace: "{{ automation_orchestrator_operator_operator_namespace }}"
|
||||
spec: {}
|
||||
when: __automation_orchestrator_operator_ogs.resources | length == 0
|
||||
|
||||
- name: Subscribe to Automation Orchestrator operator
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: operators.coreos.com/v1alpha1
|
||||
kind: Subscription
|
||||
metadata:
|
||||
name: "{{ automation_orchestrator_operator_name }}"
|
||||
namespace: "{{ automation_orchestrator_operator_operator_namespace }}"
|
||||
spec:
|
||||
channel: "{{ automation_orchestrator_operator_channel }}"
|
||||
installPlanApproval: "{{ automation_orchestrator_operator_install_plan_approval }}"
|
||||
name: "{{ automation_orchestrator_operator_name }}"
|
||||
source: "{{ automation_orchestrator_operator_catalog_source_name }}"
|
||||
sourceNamespace: openshift-marketplace
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 2: Approve the initial InstallPlan (if needed) and wait for the CSV
|
||||
# ------------------------------------------------------------------
|
||||
# Once a CSV is installed, pending InstallPlans are upgrades — leave those for
|
||||
# manual approval rather than approving them on every run.
|
||||
- name: Wait for OLM to resolve the Subscription
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: operators.coreos.com/v1alpha1
|
||||
kind: Subscription
|
||||
namespace: "{{ automation_orchestrator_operator_operator_namespace }}"
|
||||
name: "{{ automation_orchestrator_operator_name }}"
|
||||
register: __automation_orchestrator_operator_sub
|
||||
until: >-
|
||||
__automation_orchestrator_operator_sub.resources | length > 0 and
|
||||
((__automation_orchestrator_operator_sub.resources[0].status.installedCSV | default('')) | length > 0 or
|
||||
(__automation_orchestrator_operator_sub.resources[0].status.installPlanRef.name | default('')) | length > 0)
|
||||
retries: "{{ __automation_orchestrator_operator_wait_retries }}"
|
||||
delay: 10
|
||||
|
||||
- name: Resolve installed CSV name
|
||||
ansible.builtin.set_fact:
|
||||
__automation_orchestrator_operator_csv_name: >-
|
||||
{{ __automation_orchestrator_operator_sub.resources[0].status.installedCSV | default('') }}
|
||||
|
||||
- name: Approve the initial InstallPlan
|
||||
when: __automation_orchestrator_operator_csv_name | length == 0
|
||||
block:
|
||||
- name: Read the InstallPlan
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: operators.coreos.com/v1alpha1
|
||||
kind: InstallPlan
|
||||
namespace: "{{ automation_orchestrator_operator_operator_namespace }}"
|
||||
name: "{{ __automation_orchestrator_operator_sub.resources[0].status.installPlanRef.name }}"
|
||||
register: __automation_orchestrator_operator_installplan
|
||||
|
||||
- name: Approve the InstallPlan
|
||||
kubernetes.core.k8s_json_patch:
|
||||
api_version: operators.coreos.com/v1alpha1
|
||||
kind: InstallPlan
|
||||
namespace: "{{ automation_orchestrator_operator_operator_namespace }}"
|
||||
name: "{{ __automation_orchestrator_operator_sub.resources[0].status.installPlanRef.name }}"
|
||||
patch:
|
||||
- op: add
|
||||
path: /spec/approved
|
||||
value: true
|
||||
when: not (__automation_orchestrator_operator_installplan.resources[0].spec.approved | default(false))
|
||||
|
||||
- name: Resolve CSV name from the InstallPlan
|
||||
ansible.builtin.set_fact:
|
||||
__automation_orchestrator_operator_csv_name: >-
|
||||
{{ __automation_orchestrator_operator_installplan.resources[0].spec.clusterServiceVersionNames[0] }}
|
||||
|
||||
- name: Wait for Automation Orchestrator CSV to succeed
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: operators.coreos.com/v1alpha1
|
||||
kind: ClusterServiceVersion
|
||||
namespace: "{{ automation_orchestrator_operator_operator_namespace }}"
|
||||
name: "{{ __automation_orchestrator_operator_csv_name }}"
|
||||
register: __automation_orchestrator_operator_csv
|
||||
until: >-
|
||||
__automation_orchestrator_operator_csv.resources | length > 0 and
|
||||
__automation_orchestrator_operator_csv.resources[0].status.phase | default('') == 'Succeeded'
|
||||
retries: "{{ __automation_orchestrator_operator_wait_retries }}"
|
||||
delay: 10
|
||||
|
||||
- name: Wait for AutomationOrchestrator CRD to be available
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
name: automationorchestrators.aap.ansible.com
|
||||
register: __automation_orchestrator_operator_crd
|
||||
until: __automation_orchestrator_operator_crd.resources | length > 0
|
||||
retries: "{{ __automation_orchestrator_operator_wait_retries }}"
|
||||
delay: 10
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 3: Deploy the AutomationOrchestrator instance
|
||||
# ------------------------------------------------------------------
|
||||
- name: Build AutomationOrchestrator spec
|
||||
ansible.builtin.set_fact:
|
||||
__automation_orchestrator_operator_spec: >-
|
||||
{{
|
||||
{
|
||||
'imagePullSecrets': automation_orchestrator_operator_image_pull_secrets,
|
||||
'postgres': {
|
||||
'host': automation_orchestrator_operator_postgres_host,
|
||||
'port': automation_orchestrator_operator_postgres_port,
|
||||
'sslMode': automation_orchestrator_operator_postgres_ssl_mode,
|
||||
'backendDatabase': {'secretRef': {'name': automation_orchestrator_operator_backend_db_secret_name}},
|
||||
'temporalDatabase': {'secretRef': {'name': automation_orchestrator_operator_temporal_db_secret_name}},
|
||||
},
|
||||
'ingress': {
|
||||
'type': automation_orchestrator_operator_ingress_type,
|
||||
'annotations': automation_orchestrator_operator_ingress_annotations,
|
||||
},
|
||||
'backend': automation_orchestrator_operator_backend,
|
||||
'ui': automation_orchestrator_operator_ui,
|
||||
'worker': automation_orchestrator_operator_worker,
|
||||
'backgroundWorker': automation_orchestrator_operator_background_worker,
|
||||
'redis': automation_orchestrator_operator_redis,
|
||||
'temporal': {'server': automation_orchestrator_operator_temporal_server},
|
||||
}
|
||||
| combine(
|
||||
{'postgres': {'caCertSecretRef': {'name': automation_orchestrator_operator_postgres_ca_cert_secret}}} if automation_orchestrator_operator_postgres_ca_cert_secret else {},
|
||||
recursive=True
|
||||
)
|
||||
| combine(
|
||||
{'ingress': {'host': automation_orchestrator_operator_ingress_host}} if automation_orchestrator_operator_ingress_host else {},
|
||||
recursive=True
|
||||
)
|
||||
| combine(
|
||||
{'ingress': {'tlsSecretRef': {'name': automation_orchestrator_operator_ingress_tls_secret_name}}}
|
||||
if (automation_orchestrator_operator_ingress_type == 'Ingress' and automation_orchestrator_operator_ingress_tls_secret_name)
|
||||
else {},
|
||||
recursive=True
|
||||
)
|
||||
| combine(automation_orchestrator_operator_extra_spec, recursive=True)
|
||||
}}
|
||||
|
||||
- name: Create AutomationOrchestrator instance
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: aap.ansible.com/v1alpha1
|
||||
kind: AutomationOrchestrator
|
||||
metadata:
|
||||
name: "{{ automation_orchestrator_operator_instance_name }}"
|
||||
namespace: "{{ automation_orchestrator_operator_app_namespace }}"
|
||||
spec: "{{ __automation_orchestrator_operator_spec }}"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 4: Wait for the instance to be ready
|
||||
# ------------------------------------------------------------------
|
||||
- name: Wait for AutomationOrchestrator to be ready
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: aap.ansible.com/v1alpha1
|
||||
kind: AutomationOrchestrator
|
||||
namespace: "{{ automation_orchestrator_operator_app_namespace }}"
|
||||
name: "{{ automation_orchestrator_operator_instance_name }}"
|
||||
register: __automation_orchestrator_operator_status
|
||||
ignore_errors: true
|
||||
until: >-
|
||||
__automation_orchestrator_operator_status.resources is defined and
|
||||
__automation_orchestrator_operator_status.resources | length > 0 and
|
||||
(__automation_orchestrator_operator_status.resources[0].status.conditions | default([])
|
||||
| selectattr('type', '==', 'Ready')
|
||||
| selectattr('status', '==', 'True') | list | length > 0)
|
||||
retries: "{{ __automation_orchestrator_operator_wait_retries }}"
|
||||
delay: 10
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 5: Display summary
|
||||
# ------------------------------------------------------------------
|
||||
- name: Display Automation Orchestrator deployment summary
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Automation Orchestrator deployment complete!"
|
||||
- " Namespace : {{ automation_orchestrator_operator_app_namespace }}"
|
||||
- " Instance : {{ automation_orchestrator_operator_instance_name }}"
|
||||
- " Ingress type: {{ automation_orchestrator_operator_ingress_type }}"
|
||||
- ""
|
||||
- "Find the UI hostname with: oc get routes -n {{ automation_orchestrator_operator_app_namespace }}"
|
||||
- "The initial admin password is auto-generated unless spec.secrets.initialAdminPasswordSecretRef is set:"
|
||||
- " oc get secret {{ automation_orchestrator_operator_instance_name }}-initial-admin-password -n {{ automation_orchestrator_operator_app_namespace }} -o jsonpath='{.data.password}' | base64 -d"
|
||||
4
roles/automation_orchestrator_operator/vars/main.yml
Normal file
4
roles/automation_orchestrator_operator/vars/main.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
# Computed internal variables - do not override
|
||||
__automation_orchestrator_operator_wait_retries: "{{ (automation_orchestrator_operator_wait_timeout / 10) | int }}"
|
||||
__automation_orchestrator_operator_mcp_wait_retries: "{{ (automation_orchestrator_operator_mcp_wait_timeout / 10) | int }}"
|
||||
53
roles/cloudnativepg_operator/defaults/main.yml
Normal file
53
roles/cloudnativepg_operator/defaults/main.yml
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
# --- OLM subscription ---
|
||||
# CloudNativePG is in the certified-operators catalog by default (no custom
|
||||
# CatalogSource needed, unlike automation_orchestrator_operator).
|
||||
cloudnativepg_operator_operator_namespace: cloudnative-pg
|
||||
cloudnativepg_operator_channel: stable-v1
|
||||
cloudnativepg_operator_name: cloudnative-pg
|
||||
cloudnativepg_operator_catalog_source: certified-operators
|
||||
cloudnativepg_operator_wait_timeout: 600
|
||||
|
||||
# --- Cluster CR ---
|
||||
# Namespace for the Cluster, credential Secrets, and Database CRs. Defaults to
|
||||
# the automation_orchestrator_operator app namespace — keep the two in sync if overridden.
|
||||
cloudnativepg_operator_cluster_namespace: automation-orchestrator
|
||||
cloudnativepg_operator_cluster_name: orchestrator-postgres
|
||||
cloudnativepg_operator_owner_user: orchestrator
|
||||
cloudnativepg_operator_instances: 1
|
||||
# A primary PDB on a single instance is permanently at its limit (PodDisruptionBudgetAtLimit)
|
||||
# and blocks node drains; CNPG recommends disabling it for single-instance dev clusters.
|
||||
cloudnativepg_operator_enable_pdb: "{{ cloudnativepg_operator_instances | int > 1 }}"
|
||||
cloudnativepg_operator_image_name: ghcr.io/cloudnative-pg/postgresql:15
|
||||
cloudnativepg_operator_storage_size: 5Gi
|
||||
cloudnativepg_operator_storage_class: ""
|
||||
cloudnativepg_operator_max_connections: 200
|
||||
|
||||
# --- Databases (Database CRs, all owned by cloudnativepg_operator_owner_user) ---
|
||||
cloudnativepg_operator_databases:
|
||||
- name: orchestrator
|
||||
dbname: orchestrator
|
||||
- name: temporal
|
||||
dbname: temporal
|
||||
- name: temporal-visibility
|
||||
dbname: temporal_visibility
|
||||
|
||||
# --- Credential secrets (type kubernetes.io/basic-auth) ---
|
||||
# All share the same generated username/password; only the referenced database differs.
|
||||
# secrets[0] is used as the CNPG bootstrap.initdb secret (creates the owner role + first DB).
|
||||
cloudnativepg_operator_secrets:
|
||||
- name: orchestrator-postgres-secret
|
||||
database: orchestrator
|
||||
- name: temporal-postgres-secret
|
||||
database: temporal
|
||||
- name: temporal-visibility-postgres-secret
|
||||
database: temporal_visibility
|
||||
|
||||
# --- Vault ---
|
||||
# KV2 path/key where the generated postgres password is stored and retrieved.
|
||||
cloudnativepg_operator_vault_mount: kv
|
||||
cloudnativepg_operator_vault_path: cloudnativepg/orchestrator-postgres
|
||||
|
||||
# --- Debugging ---
|
||||
# Set to false to disable no_log and expose secret values in task output.
|
||||
cloudnativepg_operator_no_log: true
|
||||
94
roles/cloudnativepg_operator/meta/argument_specs.yml
Normal file
94
roles/cloudnativepg_operator/meta/argument_specs.yml
Normal file
@@ -0,0 +1,94 @@
|
||||
---
|
||||
argument_specs:
|
||||
main:
|
||||
short_description: Install CloudNativePG via OLM and provision a PostgreSQL Cluster + Databases
|
||||
description:
|
||||
- Installs the CloudNativePG operator via OLM (certified-operators catalog, Manual install-plan
|
||||
approval which this role approves itself) and creates a single-instance PostgreSQL Cluster CR
|
||||
with Database CRs for the orchestrator, temporal, and temporal_visibility databases — all owned
|
||||
by one shared PostgreSQL role.
|
||||
- >-
|
||||
WARNING: this is a demo/dev-oriented, non-HA PostgreSQL deployment. For production, point
|
||||
automation_orchestrator_operator at a customer-managed PostgreSQL instance instead.
|
||||
- The generated owner-role password is stored in Vault at cloudnativepg_operator_vault_path
|
||||
and reused on subsequent runs for idempotency.
|
||||
options:
|
||||
cloudnativepg_operator_operator_namespace:
|
||||
description: Namespace for the CloudNativePG operator.
|
||||
type: str
|
||||
default: cloudnative-pg
|
||||
cloudnativepg_operator_channel:
|
||||
description: OLM subscription channel.
|
||||
type: str
|
||||
default: stable-v1
|
||||
cloudnativepg_operator_name:
|
||||
description: Operator package name in the catalog.
|
||||
type: str
|
||||
default: cloudnative-pg
|
||||
cloudnativepg_operator_catalog_source:
|
||||
description: Name of the CatalogSource to subscribe from.
|
||||
type: str
|
||||
default: certified-operators
|
||||
cloudnativepg_operator_wait_timeout:
|
||||
description: Seconds to wait for the operator, CSV, and Database CRs to become ready.
|
||||
type: int
|
||||
default: 600
|
||||
cloudnativepg_operator_cluster_namespace:
|
||||
description: Namespace for the Cluster CR, credential Secrets, and Database CRs.
|
||||
type: str
|
||||
default: automation-orchestrator
|
||||
cloudnativepg_operator_cluster_name:
|
||||
description: Name of the CloudNativePG Cluster CR.
|
||||
type: str
|
||||
default: orchestrator-postgres
|
||||
cloudnativepg_operator_owner_user:
|
||||
description: PostgreSQL role that owns all provisioned databases.
|
||||
type: str
|
||||
default: orchestrator
|
||||
cloudnativepg_operator_instances:
|
||||
description: Number of Postgres instances in the Cluster (1 = no HA).
|
||||
type: int
|
||||
default: 1
|
||||
cloudnativepg_operator_enable_pdb:
|
||||
description: >-
|
||||
Create the CNPG primary PodDisruptionBudget. Defaults to true only when instances > 1;
|
||||
a single-instance PDB is always at its limit and blocks node drains.
|
||||
type: bool
|
||||
cloudnativepg_operator_image_name:
|
||||
description: PostgreSQL container image for the Cluster.
|
||||
type: str
|
||||
default: ghcr.io/cloudnative-pg/postgresql:15
|
||||
cloudnativepg_operator_storage_size:
|
||||
description: Size of the Cluster's PersistentVolumeClaim.
|
||||
type: str
|
||||
default: 5Gi
|
||||
cloudnativepg_operator_storage_class:
|
||||
description: StorageClass for the Cluster's PVC. Empty string uses the cluster default.
|
||||
type: str
|
||||
default: ""
|
||||
cloudnativepg_operator_max_connections:
|
||||
description: postgresql.parameters.max_connections. Set to 0 to omit and use the Postgres default.
|
||||
type: int
|
||||
default: 200
|
||||
cloudnativepg_operator_databases:
|
||||
description: List of {name, dbname} Database CRs to create, all owned by cloudnativepg_operator_owner_user.
|
||||
type: list
|
||||
elements: dict
|
||||
cloudnativepg_operator_secrets:
|
||||
description: >-
|
||||
List of {name, database} credential Secrets to create (type kubernetes.io/basic-auth), all
|
||||
sharing the same generated username/password. secrets[0] is used as the CNPG bootstrap.initdb secret.
|
||||
type: list
|
||||
elements: dict
|
||||
cloudnativepg_operator_vault_mount:
|
||||
description: Vault KV2 mount point where the generated owner-role password is stored.
|
||||
type: str
|
||||
default: kv
|
||||
cloudnativepg_operator_vault_path:
|
||||
description: Vault KV2 path where the generated owner-role password is stored.
|
||||
type: str
|
||||
default: cloudnativepg/orchestrator-postgres
|
||||
cloudnativepg_operator_no_log:
|
||||
description: Set to false to disable no_log and expose secret values in task output. Never set false in shared environments.
|
||||
type: bool
|
||||
default: true
|
||||
18
roles/cloudnativepg_operator/meta/main.yml
Normal file
18
roles/cloudnativepg_operator/meta/main.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: ptoal
|
||||
description: Install CloudNativePG via OLM and provision a PostgreSQL Cluster for Automation Orchestrator
|
||||
license: MIT
|
||||
min_ansible_version: "2.16"
|
||||
platforms:
|
||||
- name: GenericLinux
|
||||
versions:
|
||||
- all
|
||||
galaxy_tags:
|
||||
- openshift
|
||||
- postgresql
|
||||
- cnpg
|
||||
- operator
|
||||
- olm
|
||||
|
||||
dependencies: []
|
||||
230
roles/cloudnativepg_operator/tasks/main.yml
Normal file
230
roles/cloudnativepg_operator/tasks/main.yml
Normal file
@@ -0,0 +1,230 @@
|
||||
---
|
||||
# Install CloudNativePG via OLM and provision the PostgreSQL Cluster + Database CRs
|
||||
# and credential Secrets that back Automation Orchestrator (backend + Temporal +
|
||||
# Temporal Visibility databases, one shared owner role).
|
||||
#
|
||||
# WARNING: this is a single-instance (no HA), operator-provisioned PostgreSQL
|
||||
# cluster intended for demo/dev use. For production, point automation_orchestrator_operator
|
||||
# at a customer-managed PostgreSQL instance instead of running this role.
|
||||
#
|
||||
# Modeled on https://github.com/ansible/aapctl (internal/install/cloudnativepg).
|
||||
# CloudNativePG's OLM subscription uses Manual install-plan approval, so this role
|
||||
# approves the InstallPlan itself rather than relying on Automatic.
|
||||
#
|
||||
# All tasks are idempotent (kubernetes.core.k8s state: present).
|
||||
|
||||
- name: Resolve postgres credentials from Vault
|
||||
ansible.builtin.include_tasks: vault_secrets.yml
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 1: Install CloudNativePG operator via OLM
|
||||
# ------------------------------------------------------------------
|
||||
- name: Create CloudNativePG operator namespace
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: "{{ cloudnativepg_operator_operator_namespace }}"
|
||||
|
||||
- name: Create CloudNativePG OperatorGroup
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: operators.coreos.com/v1
|
||||
kind: OperatorGroup
|
||||
metadata:
|
||||
name: "{{ cloudnativepg_operator_name }}"
|
||||
namespace: "{{ cloudnativepg_operator_operator_namespace }}"
|
||||
spec: {}
|
||||
|
||||
- name: Subscribe to CloudNativePG operator
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: operators.coreos.com/v1alpha1
|
||||
kind: Subscription
|
||||
metadata:
|
||||
name: "{{ cloudnativepg_operator_name }}"
|
||||
namespace: "{{ cloudnativepg_operator_operator_namespace }}"
|
||||
spec:
|
||||
channel: "{{ cloudnativepg_operator_channel }}"
|
||||
installPlanApproval: Manual
|
||||
name: "{{ cloudnativepg_operator_name }}"
|
||||
source: "{{ cloudnativepg_operator_catalog_source }}"
|
||||
sourceNamespace: openshift-marketplace
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 2: Approve the Manual InstallPlan and wait for the CSV to succeed
|
||||
# ------------------------------------------------------------------
|
||||
- name: Wait for OLM to create an InstallPlan for the Subscription
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: operators.coreos.com/v1alpha1
|
||||
kind: Subscription
|
||||
namespace: "{{ cloudnativepg_operator_operator_namespace }}"
|
||||
name: "{{ cloudnativepg_operator_name }}"
|
||||
register: __cloudnativepg_operator_sub
|
||||
until: >-
|
||||
__cloudnativepg_operator_sub.resources | length > 0 and
|
||||
(__cloudnativepg_operator_sub.resources[0].status.installPlanRef.name | default('')) | length > 0
|
||||
retries: "{{ __cloudnativepg_operator_wait_retries }}"
|
||||
delay: 10
|
||||
|
||||
- name: Read the InstallPlan
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: operators.coreos.com/v1alpha1
|
||||
kind: InstallPlan
|
||||
namespace: "{{ cloudnativepg_operator_operator_namespace }}"
|
||||
name: "{{ __cloudnativepg_operator_sub.resources[0].status.installPlanRef.name }}"
|
||||
register: __cloudnativepg_operator_installplan
|
||||
|
||||
- name: Approve the InstallPlan
|
||||
kubernetes.core.k8s_json_patch:
|
||||
api_version: operators.coreos.com/v1alpha1
|
||||
kind: InstallPlan
|
||||
namespace: "{{ cloudnativepg_operator_operator_namespace }}"
|
||||
name: "{{ __cloudnativepg_operator_sub.resources[0].status.installPlanRef.name }}"
|
||||
patch:
|
||||
- op: add
|
||||
path: /spec/approved
|
||||
value: true
|
||||
|
||||
- name: Wait for CloudNativePG CSV to succeed
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: operators.coreos.com/v1alpha1
|
||||
kind: ClusterServiceVersion
|
||||
namespace: "{{ cloudnativepg_operator_operator_namespace }}"
|
||||
name: "{{ __cloudnativepg_operator_installplan.resources[0].spec.clusterServiceVersionNames[0] }}"
|
||||
register: __cloudnativepg_operator_csv
|
||||
until: >-
|
||||
__cloudnativepg_operator_csv.resources | length > 0 and
|
||||
__cloudnativepg_operator_csv.resources[0].status.phase | default('') == 'Succeeded'
|
||||
retries: "{{ __cloudnativepg_operator_wait_retries }}"
|
||||
delay: 10
|
||||
|
||||
- name: Wait for CloudNativePG controller manager webhook to be reachable
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: v1
|
||||
kind: Endpoints
|
||||
namespace: "{{ cloudnativepg_operator_operator_namespace }}"
|
||||
name: cnpg-controller-manager-service
|
||||
register: __cloudnativepg_operator_endpoints
|
||||
until: >-
|
||||
__cloudnativepg_operator_endpoints.resources | length > 0 and
|
||||
(__cloudnativepg_operator_endpoints.resources[0].subsets | default([])
|
||||
| map(attribute='addresses', default=[]) | flatten | length) > 0
|
||||
retries: "{{ __cloudnativepg_operator_wait_retries }}"
|
||||
delay: 10
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 3: Credential secrets (type kubernetes.io/basic-auth)
|
||||
# ------------------------------------------------------------------
|
||||
- name: Create cluster namespace
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: "{{ cloudnativepg_operator_cluster_namespace }}"
|
||||
|
||||
- name: Create postgres credential secrets
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: "{{ item.name }}"
|
||||
namespace: "{{ cloudnativepg_operator_cluster_namespace }}"
|
||||
type: kubernetes.io/basic-auth
|
||||
stringData:
|
||||
username: "{{ cloudnativepg_operator_owner_user }}"
|
||||
password: "{{ __cloudnativepg_operator_password }}"
|
||||
host: "{{ cloudnativepg_operator_cluster_name }}-rw.{{ cloudnativepg_operator_cluster_namespace }}.svc"
|
||||
port: "5432"
|
||||
database: "{{ item.database }}"
|
||||
loop: "{{ cloudnativepg_operator_secrets }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
no_log: "{{ cloudnativepg_operator_no_log }}"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 4: PostgreSQL Cluster
|
||||
# ------------------------------------------------------------------
|
||||
- name: Create PostgreSQL Cluster
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: postgresql.cnpg.io/v1
|
||||
kind: Cluster
|
||||
metadata:
|
||||
name: "{{ cloudnativepg_operator_cluster_name }}"
|
||||
namespace: "{{ cloudnativepg_operator_cluster_namespace }}"
|
||||
spec: >-
|
||||
{{
|
||||
{
|
||||
'instances': cloudnativepg_operator_instances,
|
||||
'enablePDB': cloudnativepg_operator_enable_pdb | bool,
|
||||
'imageName': cloudnativepg_operator_image_name,
|
||||
'bootstrap': {
|
||||
'initdb': {
|
||||
'owner': cloudnativepg_operator_owner_user,
|
||||
'secret': {'name': cloudnativepg_operator_secrets[0].name},
|
||||
},
|
||||
},
|
||||
'storage': {'size': cloudnativepg_operator_storage_size},
|
||||
}
|
||||
| combine({'storage': {'storageClass': cloudnativepg_operator_storage_class}} if cloudnativepg_operator_storage_class else {}, recursive=True)
|
||||
| combine({'postgresql': {'parameters': {'max_connections': cloudnativepg_operator_max_connections | string}}} if cloudnativepg_operator_max_connections else {}, recursive=True)
|
||||
}}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 5: Logical databases
|
||||
# ------------------------------------------------------------------
|
||||
- name: Create Database CRs
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: postgresql.cnpg.io/v1
|
||||
kind: Database
|
||||
metadata:
|
||||
name: "{{ item.name }}"
|
||||
namespace: "{{ cloudnativepg_operator_cluster_namespace }}"
|
||||
spec:
|
||||
name: "{{ item.dbname }}"
|
||||
owner: "{{ cloudnativepg_operator_owner_user }}"
|
||||
cluster:
|
||||
name: "{{ cloudnativepg_operator_cluster_name }}"
|
||||
loop: "{{ cloudnativepg_operator_databases }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
|
||||
- name: Wait for Database CRs to be applied
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: postgresql.cnpg.io/v1
|
||||
kind: Database
|
||||
namespace: "{{ cloudnativepg_operator_cluster_namespace }}"
|
||||
register: __cloudnativepg_operator_databases_status
|
||||
until: >-
|
||||
__cloudnativepg_operator_databases_status.resources | length >= (cloudnativepg_operator_databases | length) and
|
||||
(__cloudnativepg_operator_databases_status.resources
|
||||
| rejectattr('status.applied', 'undefined')
|
||||
| selectattr('status.applied')
|
||||
| list | length) == (__cloudnativepg_operator_databases_status.resources | length)
|
||||
retries: "{{ __cloudnativepg_operator_wait_retries }}"
|
||||
delay: 10
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 6: Summary
|
||||
# ------------------------------------------------------------------
|
||||
- name: Display CloudNativePG deployment summary
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "CloudNativePG PostgreSQL cluster ready!"
|
||||
- " Operator namespace : {{ cloudnativepg_operator_operator_namespace }}"
|
||||
- " Cluster : {{ cloudnativepg_operator_cluster_name }} ({{ cloudnativepg_operator_cluster_namespace }})"
|
||||
- " Owner role : {{ cloudnativepg_operator_owner_user }}"
|
||||
- " Databases : {{ cloudnativepg_operator_databases | map(attribute='dbname') | join(', ') }}"
|
||||
- " Credential secrets : {{ cloudnativepg_operator_secrets | map(attribute='name') | join(', ') }}"
|
||||
26
roles/cloudnativepg_operator/tasks/vault_secrets.yml
Normal file
26
roles/cloudnativepg_operator/tasks/vault_secrets.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
# Read the shared postgres owner password from Vault; generate and store it if missing.
|
||||
# Sets __cloudnativepg_operator_password, consumed by the K8s secret tasks in main.yml.
|
||||
|
||||
- name: Read cloudnativepg postgres password from Vault
|
||||
community.hashi_vault.vault_kv2_get:
|
||||
path: "{{ cloudnativepg_operator_vault_path }}"
|
||||
engine_mount_point: "{{ cloudnativepg_operator_vault_mount }}"
|
||||
register: __cloudnativepg_operator_vault_read
|
||||
failed_when: false
|
||||
no_log: "{{ cloudnativepg_operator_no_log }}"
|
||||
|
||||
- name: Resolve postgres password (generate if missing)
|
||||
ansible.builtin.set_fact:
|
||||
__cloudnativepg_operator_password: >-
|
||||
{{ (__cloudnativepg_operator_vault_read.secret | default({})).password
|
||||
| default(lookup('community.general.random_string', length=32, special=false)) }}
|
||||
no_log: "{{ cloudnativepg_operator_no_log }}"
|
||||
|
||||
- name: Write postgres password to Vault
|
||||
community.hashi_vault.vault_kv2_write:
|
||||
path: "{{ cloudnativepg_operator_vault_path }}"
|
||||
engine_mount_point: "{{ cloudnativepg_operator_vault_mount }}"
|
||||
data:
|
||||
password: "{{ __cloudnativepg_operator_password }}"
|
||||
no_log: "{{ cloudnativepg_operator_no_log }}"
|
||||
3
roles/cloudnativepg_operator/vars/main.yml
Normal file
3
roles/cloudnativepg_operator/vars/main.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
# Computed internal variables - do not override
|
||||
__cloudnativepg_operator_wait_retries: "{{ (cloudnativepg_operator_wait_timeout / 10) | int }}"
|
||||
58
roles/dnsmadeeasy_record/README.md
Normal file
58
roles/dnsmadeeasy_record/README.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# dnsmadeeasy_record
|
||||
|
||||
Manages DNS records in DNS Made Easy via the `community.general.dnsmadeeasy` module.
|
||||
|
||||
Accepts a list of record entries and creates or updates each one.
|
||||
|
||||
## Requirements
|
||||
|
||||
- `community.general` collection
|
||||
- DNS Made Easy account credentials
|
||||
|
||||
## Role Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `dnsmadeeasy_record_account_key` | *required* | DNS Made Easy account key |
|
||||
| `dnsmadeeasy_record_account_secret` | *required* | DNS Made Easy account secret (sensitive) |
|
||||
| `dnsmadeeasy_record_entries` | `[]` | List of DNS record entries (see below) |
|
||||
|
||||
### Entry format
|
||||
|
||||
Each entry in `dnsmadeeasy_record_entries` requires:
|
||||
|
||||
| Field | Required | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `domain` | yes | | DNS zone (e.g. `openshift.toal.ca`) |
|
||||
| `record_name` | yes | | Record name within the zone |
|
||||
| `record_type` | yes | | DNS record type (A, CNAME, etc.) |
|
||||
| `record_value` | yes | | Target value |
|
||||
| `record_ttl` | no | `1800` | TTL in seconds |
|
||||
|
||||
## Example Playbook
|
||||
|
||||
```yaml
|
||||
- name: Configure public DNS records
|
||||
hosts: sno.openshift.toal.ca
|
||||
gather_facts: false
|
||||
connection: local
|
||||
|
||||
roles:
|
||||
- role: dnsmadeeasy_record
|
||||
dnsmadeeasy_record_account_key: "{{ dme_account_key }}"
|
||||
dnsmadeeasy_record_account_secret: "{{ dme_account_secret }}"
|
||||
dnsmadeeasy_record_entries:
|
||||
- domain: openshift.toal.ca
|
||||
record_name: api.sno
|
||||
record_type: A
|
||||
record_value: 203.0.113.1
|
||||
record_ttl: 300
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Author
|
||||
|
||||
ptoal
|
||||
24
roles/dnsmadeeasy_record/defaults/main.yml
Normal file
24
roles/dnsmadeeasy_record/defaults/main.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
# DNS Made Easy API credentials
|
||||
# dnsmadeeasy_record_account_key: "" # required
|
||||
# dnsmadeeasy_record_account_secret: "" # required (sensitive)
|
||||
|
||||
# List of DNS records to create/update.
|
||||
#
|
||||
# Each entry requires:
|
||||
# domain: DNS zone (e.g. "openshift.toal.ca")
|
||||
# record_name: record name within the zone (e.g. "api.sno")
|
||||
# record_type: DNS record type (A, CNAME, etc.)
|
||||
# record_value: target value (IP address or hostname)
|
||||
#
|
||||
# Optional per entry:
|
||||
# record_ttl: TTL in seconds (default: 1800)
|
||||
#
|
||||
# Example:
|
||||
# dnsmadeeasy_record_entries:
|
||||
# - domain: openshift.toal.ca
|
||||
# record_name: api.sno
|
||||
# record_type: A
|
||||
# record_value: 203.0.113.1
|
||||
# record_ttl: 300
|
||||
dnsmadeeasy_record_entries: []
|
||||
24
roles/dnsmadeeasy_record/meta/argument_specs.yml
Normal file
24
roles/dnsmadeeasy_record/meta/argument_specs.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
argument_specs:
|
||||
main:
|
||||
short_description: Manage DNS records in DNS Made Easy
|
||||
description:
|
||||
- Creates or updates DNS records via the DNS Made Easy API
|
||||
using the community.general.dnsmadeeasy module.
|
||||
options:
|
||||
dnsmadeeasy_record_account_key:
|
||||
description: DNS Made Easy account key.
|
||||
type: str
|
||||
required: true
|
||||
dnsmadeeasy_record_account_secret:
|
||||
description: DNS Made Easy account secret.
|
||||
type: str
|
||||
required: true
|
||||
no_log: true
|
||||
dnsmadeeasy_record_entries:
|
||||
description: >-
|
||||
List of DNS record entries. Each entry requires C(domain), C(record_name),
|
||||
C(record_type), and C(record_value). Optional C(record_ttl) defaults to 1800.
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
15
roles/dnsmadeeasy_record/meta/main.yml
Normal file
15
roles/dnsmadeeasy_record/meta/main.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: ptoal
|
||||
description: Manage DNS records in DNS Made Easy
|
||||
license: MIT
|
||||
min_ansible_version: "2.16"
|
||||
platforms:
|
||||
- name: GenericLinux
|
||||
versions:
|
||||
- all
|
||||
galaxy_tags:
|
||||
- dns
|
||||
- dnsmadeeasy
|
||||
|
||||
dependencies: []
|
||||
14
roles/dnsmadeeasy_record/tasks/main.yml
Normal file
14
roles/dnsmadeeasy_record/tasks/main.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
- name: Manage DNS Made Easy records
|
||||
community.general.dnsmadeeasy:
|
||||
account_key: "{{ dnsmadeeasy_record_account_key }}"
|
||||
account_secret: "{{ dnsmadeeasy_record_account_secret }}"
|
||||
domain: "{{ item.domain }}"
|
||||
record_name: "{{ item.record_name }}"
|
||||
record_type: "{{ item.record_type }}"
|
||||
record_value: "{{ item.record_value }}"
|
||||
record_ttl: "{{ item.record_ttl | default(1800) }}"
|
||||
state: present
|
||||
loop: "{{ dnsmadeeasy_record_entries }}"
|
||||
loop_control:
|
||||
label: "{{ item.record_name }}.{{ item.domain }} ({{ item.record_type }})"
|
||||
13
roles/lvms_operator/defaults/main.yml
Normal file
13
roles/lvms_operator/defaults/main.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
# --- OLM subscription ---
|
||||
lvms_operator_namespace: openshift-storage
|
||||
lvms_operator_channel: "stable-4.21"
|
||||
lvms_operator_source: redhat-operators
|
||||
lvms_operator_name: lvms-operator
|
||||
lvms_operator_wait_timeout: 300
|
||||
|
||||
# --- LVMCluster ---
|
||||
lvms_operator_vg_name: vg-data
|
||||
lvms_operator_device_paths:
|
||||
- /dev/sdb
|
||||
lvms_operator_storage_class_name: lvms-vg-data
|
||||
42
roles/lvms_operator/meta/argument_specs.yml
Normal file
42
roles/lvms_operator/meta/argument_specs.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
argument_specs:
|
||||
main:
|
||||
short_description: Install LVMS operator for persistent storage on OpenShift
|
||||
description:
|
||||
- Installs the LVM Storage operator via OLM and creates an LVMCluster
|
||||
with a volume group backed by specified block devices.
|
||||
options:
|
||||
lvms_operator_namespace:
|
||||
description: Namespace for the LVMS operator.
|
||||
type: str
|
||||
default: openshift-storage
|
||||
lvms_operator_channel:
|
||||
description: OLM subscription channel.
|
||||
type: str
|
||||
default: "stable-4.21"
|
||||
lvms_operator_source:
|
||||
description: OLM catalog source name.
|
||||
type: str
|
||||
default: redhat-operators
|
||||
lvms_operator_name:
|
||||
description: Operator package name in the catalog.
|
||||
type: str
|
||||
default: lvms-operator
|
||||
lvms_operator_wait_timeout:
|
||||
description: Seconds to wait for operator and LVMCluster to become ready.
|
||||
type: int
|
||||
default: 300
|
||||
lvms_operator_vg_name:
|
||||
description: Name of the volume group to create in the LVMCluster.
|
||||
type: str
|
||||
default: vg-data
|
||||
lvms_operator_device_paths:
|
||||
description: List of block device paths to include in the volume group.
|
||||
type: list
|
||||
elements: str
|
||||
default:
|
||||
- /dev/sdb
|
||||
lvms_operator_storage_class_name:
|
||||
description: Name of the StorageClass created by LVMS for this volume group.
|
||||
type: str
|
||||
default: lvms-vg-data
|
||||
18
roles/lvms_operator/meta/main.yml
Normal file
18
roles/lvms_operator/meta/main.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: ptoal
|
||||
description: Install LVM Storage (LVMS) operator on OpenShift for persistent volumes
|
||||
license: MIT
|
||||
min_ansible_version: "2.16"
|
||||
platforms:
|
||||
- name: GenericLinux
|
||||
versions:
|
||||
- all
|
||||
galaxy_tags:
|
||||
- openshift
|
||||
- lvms
|
||||
- storage
|
||||
- operator
|
||||
- olm
|
||||
|
||||
dependencies: []
|
||||
135
roles/lvms_operator/tasks/main.yml
Normal file
135
roles/lvms_operator/tasks/main.yml
Normal file
@@ -0,0 +1,135 @@
|
||||
---
|
||||
# Install LVM Storage (LVMS) operator via OpenShift OLM.
|
||||
#
|
||||
# Creates an LVMCluster with a volume group backed by the specified
|
||||
# block devices, providing a StorageClass for persistent volume claims.
|
||||
# All tasks are idempotent (kubernetes.core.k8s state: present).
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 1: Install LVMS operator via OLM
|
||||
# ------------------------------------------------------------------
|
||||
- name: Create LVMS namespace
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: "{{ lvms_operator_namespace }}"
|
||||
|
||||
- name: Create OperatorGroup for LVMS
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: operators.coreos.com/v1
|
||||
kind: OperatorGroup
|
||||
metadata:
|
||||
name: "{{ lvms_operator_name }}"
|
||||
namespace: "{{ lvms_operator_namespace }}"
|
||||
spec:
|
||||
targetNamespaces:
|
||||
- "{{ lvms_operator_namespace }}"
|
||||
upgradeStrategy: Default
|
||||
|
||||
- name: Subscribe to LVMS operator
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: operators.coreos.com/v1alpha1
|
||||
kind: Subscription
|
||||
metadata:
|
||||
name: "{{ lvms_operator_name }}"
|
||||
namespace: "{{ lvms_operator_namespace }}"
|
||||
spec:
|
||||
channel: "{{ lvms_operator_channel }}"
|
||||
installPlanApproval: Automatic
|
||||
name: "{{ lvms_operator_name }}"
|
||||
source: "{{ lvms_operator_source }}"
|
||||
sourceNamespace: openshift-marketplace
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 2: Wait for operator to be ready
|
||||
# ------------------------------------------------------------------
|
||||
- name: Wait for LVMCluster CRD to be available
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
name: lvmclusters.lvm.topolvm.io
|
||||
register: __lvms_operator_crd
|
||||
until: __lvms_operator_crd.resources | length > 0
|
||||
retries: "{{ __lvms_operator_wait_retries }}"
|
||||
delay: 10
|
||||
|
||||
- name: Wait for LVMS operator deployment to be ready
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: apps/v1
|
||||
kind: Deployment
|
||||
namespace: "{{ lvms_operator_namespace }}"
|
||||
label_selectors:
|
||||
- "operators.coreos.com/{{ lvms_operator_name }}.{{ lvms_operator_namespace }}"
|
||||
register: __lvms_operator_deploy
|
||||
until: >-
|
||||
__lvms_operator_deploy.resources | length > 0 and
|
||||
(__lvms_operator_deploy.resources
|
||||
| rejectattr('status.readyReplicas', 'undefined')
|
||||
| selectattr('status.readyReplicas', '>=', 1)
|
||||
| list | length) == (__lvms_operator_deploy.resources | length)
|
||||
retries: "{{ __lvms_operator_wait_retries }}"
|
||||
delay: 10
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 3: Create LVMCluster
|
||||
# ------------------------------------------------------------------
|
||||
- name: Create LVMCluster
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: lvm.topolvm.io/v1alpha1
|
||||
kind: LVMCluster
|
||||
metadata:
|
||||
name: lvms-cluster
|
||||
namespace: "{{ lvms_operator_namespace }}"
|
||||
spec:
|
||||
storage:
|
||||
deviceClasses:
|
||||
- name: "{{ lvms_operator_vg_name }}"
|
||||
default: true
|
||||
deviceSelector:
|
||||
paths: "{{ lvms_operator_device_paths }}"
|
||||
thinPoolConfig:
|
||||
name: thin-pool
|
||||
sizePercent: 90
|
||||
overprovisionRatio: 10
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 4: Wait for LVMCluster to be ready
|
||||
# ------------------------------------------------------------------
|
||||
- name: Wait for LVMCluster to be ready
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: lvm.topolvm.io/v1alpha1
|
||||
kind: LVMCluster
|
||||
namespace: "{{ lvms_operator_namespace }}"
|
||||
name: lvms-cluster
|
||||
register: __lvms_operator_cluster_status
|
||||
until: >-
|
||||
__lvms_operator_cluster_status.resources | length > 0 and
|
||||
(__lvms_operator_cluster_status.resources[0].status.state | default('')) == 'Ready'
|
||||
retries: "{{ __lvms_operator_wait_retries }}"
|
||||
delay: 10
|
||||
|
||||
- name: Verify StorageClass exists
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: storage.k8s.io/v1
|
||||
kind: StorageClass
|
||||
name: "{{ lvms_operator_storage_class_name }}"
|
||||
register: __lvms_operator_sc
|
||||
failed_when: __lvms_operator_sc.resources | length == 0
|
||||
|
||||
- name: Display LVMS summary
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "LVM Storage deployment complete!"
|
||||
- " Namespace : {{ lvms_operator_namespace }}"
|
||||
- " Volume Group : {{ lvms_operator_vg_name }}"
|
||||
- " Device Paths : {{ lvms_operator_device_paths | join(', ') }}"
|
||||
- " StorageClass : {{ lvms_operator_storage_class_name }}"
|
||||
3
roles/lvms_operator/vars/main.yml
Normal file
3
roles/lvms_operator/vars/main.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
# Computed internal variables - do not override
|
||||
__lvms_operator_wait_retries: "{{ (lvms_operator_wait_timeout / 10) | int }}"
|
||||
23
roles/nfs_provisioner/defaults/main.yml
Normal file
23
roles/nfs_provisioner/defaults/main.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
# --- Namespace ---
|
||||
nfs_provisioner_namespace: nfs-provisioner
|
||||
|
||||
# --- External NFS server (set these to use a pre-existing NFS share) ---
|
||||
# When nfs_provisioner_external_server is set, the in-cluster NFS server is
|
||||
# not deployed; the provisioner points directly at the external share.
|
||||
nfs_provisioner_external_server: "" # e.g. 192.168.129.100
|
||||
nfs_provisioner_external_path: "" # e.g. /mnt/BIGPOOL/NoBackups/OCPNFS
|
||||
|
||||
# --- Backing storage for in-cluster NFS server (ignored when external_server is set) ---
|
||||
nfs_provisioner_storage_class: lvms-vg-data
|
||||
nfs_provisioner_storage_size: 50Gi
|
||||
nfs_provisioner_server_image: registry.k8s.io/volume-nfs:0.8
|
||||
nfs_provisioner_export_path: /exports
|
||||
|
||||
# --- NFS provisioner ---
|
||||
nfs_provisioner_name: nfs-client
|
||||
nfs_provisioner_storage_class_name: nfs-client
|
||||
nfs_provisioner_image: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2
|
||||
|
||||
# --- Wait ---
|
||||
nfs_provisioner_wait_timeout: 300
|
||||
67
roles/nfs_provisioner/meta/argument_specs.yml
Normal file
67
roles/nfs_provisioner/meta/argument_specs.yml
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
argument_specs:
|
||||
main:
|
||||
short_description: Deploy NFS provisioner (external or in-cluster) for RWX storage on OpenShift
|
||||
description:
|
||||
- Deploys the nfs-subdir-external-provisioner and a ReadWriteMany StorageClass.
|
||||
- When nfs_provisioner_external_server is set, points directly at a pre-existing
|
||||
NFS share (no in-cluster NFS server pod is deployed).
|
||||
- When nfs_provisioner_external_server is empty, deploys an in-cluster NFS server
|
||||
pod backed by an LVMS PVC.
|
||||
options:
|
||||
nfs_provisioner_namespace:
|
||||
description: Namespace for the NFS provisioner (and optional in-cluster NFS server).
|
||||
type: str
|
||||
default: nfs-provisioner
|
||||
nfs_provisioner_external_server:
|
||||
description: >-
|
||||
IP or hostname of a pre-existing external NFS server. When set, the
|
||||
in-cluster NFS server pod is not deployed. Leave empty to use in-cluster mode.
|
||||
type: str
|
||||
default: ""
|
||||
nfs_provisioner_external_path:
|
||||
description: >-
|
||||
Exported path on the external NFS server.
|
||||
Required when nfs_provisioner_external_server is set.
|
||||
type: str
|
||||
default: ""
|
||||
nfs_provisioner_storage_class:
|
||||
description: >-
|
||||
StorageClass (RWO) for the in-cluster NFS server backing PVC.
|
||||
Ignored when nfs_provisioner_external_server is set.
|
||||
type: str
|
||||
default: lvms-vg-data
|
||||
nfs_provisioner_storage_size:
|
||||
description: >-
|
||||
Size of the in-cluster NFS server backing PVC.
|
||||
Ignored when nfs_provisioner_external_server is set.
|
||||
type: str
|
||||
default: 50Gi
|
||||
nfs_provisioner_name:
|
||||
description: Provisioner name written into the StorageClass.
|
||||
type: str
|
||||
default: nfs-client
|
||||
nfs_provisioner_storage_class_name:
|
||||
description: Name of the RWX StorageClass created by this role.
|
||||
type: str
|
||||
default: nfs-client
|
||||
nfs_provisioner_image:
|
||||
description: Container image for the nfs-subdir-external-provisioner.
|
||||
type: str
|
||||
default: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2
|
||||
nfs_provisioner_server_image:
|
||||
description: >-
|
||||
Container image for the in-cluster NFS server.
|
||||
Ignored when nfs_provisioner_external_server is set.
|
||||
type: str
|
||||
default: registry.k8s.io/volume-nfs:0.8
|
||||
nfs_provisioner_export_path:
|
||||
description: >-
|
||||
Path exported by the in-cluster NFS server.
|
||||
Ignored when nfs_provisioner_external_server is set.
|
||||
type: str
|
||||
default: /exports
|
||||
nfs_provisioner_wait_timeout:
|
||||
description: Seconds to wait for deployments to become ready.
|
||||
type: int
|
||||
default: 300
|
||||
17
roles/nfs_provisioner/meta/main.yml
Normal file
17
roles/nfs_provisioner/meta/main.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: ptoal
|
||||
description: Deploy in-cluster NFS server and provisioner for ReadWriteMany storage on OpenShift
|
||||
license: MIT
|
||||
min_ansible_version: "2.16"
|
||||
platforms:
|
||||
- name: GenericLinux
|
||||
versions:
|
||||
- all
|
||||
galaxy_tags:
|
||||
- openshift
|
||||
- nfs
|
||||
- storage
|
||||
- provisioner
|
||||
|
||||
dependencies: []
|
||||
394
roles/nfs_provisioner/tasks/main.yml
Normal file
394
roles/nfs_provisioner/tasks/main.yml
Normal file
@@ -0,0 +1,394 @@
|
||||
---
|
||||
# Deploy nfs-subdir-external-provisioner on OpenShift, backed by either:
|
||||
# (a) an external NFS server (set nfs_provisioner_external_server / nfs_provisioner_external_path)
|
||||
# (b) an in-cluster NFS server pod backed by an LVMS RWO PVC (default)
|
||||
#
|
||||
# Architecture (in-cluster mode):
|
||||
# - NFS server StatefulSet: backs exports with an LVMS RWO PVC
|
||||
# - Service: exposes NFS server at a stable ClusterIP
|
||||
# - nfs-subdir-external-provisioner: creates PVs on-demand under the export path
|
||||
# - StorageClass: "nfs-client" with ReadWriteMany support
|
||||
#
|
||||
# Architecture (external mode, nfs_provisioner_external_server != ""):
|
||||
# - In-cluster NFS server is NOT deployed
|
||||
# - nfs-subdir-external-provisioner points directly at the external NFS share
|
||||
# - StorageClass: "nfs-client" with ReadWriteMany support
|
||||
#
|
||||
# The in-cluster NFS server requires privileged SCC on OpenShift (kernel NFS).
|
||||
# All tasks are idempotent (kubernetes.core.k8s state: present).
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 1: Namespace and RBAC
|
||||
# ------------------------------------------------------------------
|
||||
- name: Create NFS provisioner namespace
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: "{{ nfs_provisioner_namespace }}"
|
||||
|
||||
- name: Create NFS server ServiceAccount
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: nfs-server
|
||||
namespace: "{{ nfs_provisioner_namespace }}"
|
||||
when: nfs_provisioner_external_server | length == 0
|
||||
|
||||
- name: Create NFS provisioner ServiceAccount
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: nfs-provisioner
|
||||
namespace: "{{ nfs_provisioner_namespace }}"
|
||||
|
||||
- name: Create ClusterRole to use privileged SCC (NFS server)
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: nfs-server-scc
|
||||
rules:
|
||||
- apiGroups: [security.openshift.io]
|
||||
resources: [securitycontextconstraints]
|
||||
verbs: [use]
|
||||
resourceNames: [privileged]
|
||||
when: nfs_provisioner_external_server | length == 0
|
||||
|
||||
- name: Bind privileged SCC ClusterRole to NFS server ServiceAccount
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: nfs-server-scc
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: nfs-server
|
||||
namespace: "{{ nfs_provisioner_namespace }}"
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: nfs-server-scc
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
when: nfs_provisioner_external_server | length == 0
|
||||
|
||||
- name: Create ClusterRole to use hostmount-anyuid SCC (NFS provisioner)
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: nfs-provisioner-scc
|
||||
rules:
|
||||
- apiGroups: [security.openshift.io]
|
||||
resources: [securitycontextconstraints]
|
||||
verbs: [use]
|
||||
resourceNames: [hostmount-anyuid]
|
||||
|
||||
- name: Bind hostmount-anyuid SCC ClusterRole to NFS provisioner ServiceAccount
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: nfs-provisioner-scc
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: nfs-provisioner
|
||||
namespace: "{{ nfs_provisioner_namespace }}"
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: nfs-provisioner-scc
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
|
||||
- name: Create ClusterRole for NFS provisioner
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: nfs-provisioner-runner
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: [persistentvolumes]
|
||||
verbs: [get, list, watch, create, delete]
|
||||
- apiGroups: [""]
|
||||
resources: [persistentvolumeclaims]
|
||||
verbs: [get, list, watch, update]
|
||||
- apiGroups: [storage.k8s.io]
|
||||
resources: [storageclasses]
|
||||
verbs: [get, list, watch]
|
||||
- apiGroups: [""]
|
||||
resources: [events]
|
||||
verbs: [create, update, patch]
|
||||
|
||||
- name: Bind ClusterRole to NFS provisioner ServiceAccount
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: run-nfs-provisioner
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: nfs-provisioner
|
||||
namespace: "{{ nfs_provisioner_namespace }}"
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: nfs-provisioner-runner
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
|
||||
- name: Create Role for leader election
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: leader-locking-nfs-provisioner
|
||||
namespace: "{{ nfs_provisioner_namespace }}"
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: [endpoints]
|
||||
verbs: [get, list, watch, create, update, patch]
|
||||
|
||||
- name: Bind leader election Role
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: leader-locking-nfs-provisioner
|
||||
namespace: "{{ nfs_provisioner_namespace }}"
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: nfs-provisioner
|
||||
namespace: "{{ nfs_provisioner_namespace }}"
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: leader-locking-nfs-provisioner
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 2: NFS server backing storage and StatefulSet (in-cluster mode only)
|
||||
# ------------------------------------------------------------------
|
||||
- name: Create NFS server backing PVC
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: nfs-server-data
|
||||
namespace: "{{ nfs_provisioner_namespace }}"
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: "{{ nfs_provisioner_storage_size }}"
|
||||
storageClassName: "{{ nfs_provisioner_storage_class }}"
|
||||
when: nfs_provisioner_external_server | length == 0
|
||||
|
||||
- name: Deploy NFS server StatefulSet
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: nfs-server
|
||||
namespace: "{{ nfs_provisioner_namespace }}"
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nfs-server
|
||||
serviceName: nfs-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nfs-server
|
||||
spec:
|
||||
serviceAccountName: nfs-server
|
||||
containers:
|
||||
- name: nfs-server
|
||||
image: "{{ nfs_provisioner_server_image }}"
|
||||
ports:
|
||||
- name: nfs
|
||||
containerPort: 2049
|
||||
- name: mountd
|
||||
containerPort: 20048
|
||||
- name: rpcbind
|
||||
containerPort: 111
|
||||
securityContext:
|
||||
privileged: true
|
||||
volumeMounts:
|
||||
- name: nfs-data
|
||||
mountPath: "{{ nfs_provisioner_export_path }}"
|
||||
volumes:
|
||||
- name: nfs-data
|
||||
persistentVolumeClaim:
|
||||
claimName: nfs-server-data
|
||||
when: nfs_provisioner_external_server | length == 0
|
||||
|
||||
- name: Create NFS server Service
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nfs-server
|
||||
namespace: "{{ nfs_provisioner_namespace }}"
|
||||
spec:
|
||||
selector:
|
||||
app: nfs-server
|
||||
ports:
|
||||
- name: nfs
|
||||
port: 2049
|
||||
- name: mountd
|
||||
port: 20048
|
||||
- name: rpcbind
|
||||
port: 111
|
||||
when: nfs_provisioner_external_server | length == 0
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 3: Wait for in-cluster NFS server to be ready (in-cluster mode only)
|
||||
# ------------------------------------------------------------------
|
||||
- name: Wait for NFS server to be ready
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: apps/v1
|
||||
kind: StatefulSet
|
||||
namespace: "{{ nfs_provisioner_namespace }}"
|
||||
name: nfs-server
|
||||
register: __nfs_provisioner_server_status
|
||||
until: >-
|
||||
__nfs_provisioner_server_status.resources | length > 0 and
|
||||
(__nfs_provisioner_server_status.resources[0].status.readyReplicas | default(0)) >= 1
|
||||
retries: "{{ __nfs_provisioner_wait_retries }}"
|
||||
delay: 10
|
||||
when: nfs_provisioner_external_server | length == 0
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 4: Resolve NFS server address, then deploy nfs-subdir-external-provisioner
|
||||
# ------------------------------------------------------------------
|
||||
- name: Set NFS server address (external)
|
||||
ansible.builtin.set_fact:
|
||||
__nfs_provisioner_server_addr: "{{ nfs_provisioner_external_server }}"
|
||||
__nfs_provisioner_server_path: "{{ nfs_provisioner_external_path }}"
|
||||
when: nfs_provisioner_external_server | length > 0
|
||||
|
||||
- name: Retrieve in-cluster NFS server ClusterIP
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: v1
|
||||
kind: Service
|
||||
namespace: "{{ nfs_provisioner_namespace }}"
|
||||
name: nfs-server
|
||||
register: __nfs_provisioner_svc
|
||||
when: nfs_provisioner_external_server | length == 0
|
||||
|
||||
- name: Set NFS server address (in-cluster)
|
||||
ansible.builtin.set_fact:
|
||||
__nfs_provisioner_server_addr: "{{ __nfs_provisioner_svc.resources[0].spec.clusterIP }}"
|
||||
__nfs_provisioner_server_path: "{{ nfs_provisioner_export_path }}"
|
||||
when: nfs_provisioner_external_server | length == 0
|
||||
|
||||
- name: Deploy nfs-subdir-external-provisioner
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nfs-provisioner
|
||||
namespace: "{{ nfs_provisioner_namespace }}"
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nfs-provisioner
|
||||
strategy:
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nfs-provisioner
|
||||
spec:
|
||||
serviceAccountName: nfs-provisioner
|
||||
containers:
|
||||
- name: nfs-provisioner
|
||||
image: "{{ nfs_provisioner_image }}"
|
||||
env:
|
||||
- name: PROVISIONER_NAME
|
||||
value: "{{ nfs_provisioner_name }}"
|
||||
- name: NFS_SERVER
|
||||
value: "{{ __nfs_provisioner_server_addr }}"
|
||||
- name: NFS_PATH
|
||||
value: "{{ __nfs_provisioner_server_path }}"
|
||||
volumeMounts:
|
||||
- name: nfs-client-root
|
||||
mountPath: /persistentvolumes
|
||||
volumes:
|
||||
- name: nfs-client-root
|
||||
nfs:
|
||||
server: "{{ __nfs_provisioner_server_addr }}"
|
||||
path: "{{ __nfs_provisioner_server_path }}"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 5: Create StorageClass
|
||||
# ------------------------------------------------------------------
|
||||
- name: Create NFS StorageClass
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: StorageClass
|
||||
metadata:
|
||||
name: "{{ nfs_provisioner_storage_class_name }}"
|
||||
provisioner: "{{ nfs_provisioner_name }}"
|
||||
parameters:
|
||||
archiveOnDelete: "false"
|
||||
reclaimPolicy: Delete
|
||||
volumeBindingMode: Immediate
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 6: Wait for provisioner to be ready
|
||||
# ------------------------------------------------------------------
|
||||
- name: Wait for NFS provisioner deployment to be ready
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: apps/v1
|
||||
kind: Deployment
|
||||
namespace: "{{ nfs_provisioner_namespace }}"
|
||||
name: nfs-provisioner
|
||||
register: __nfs_provisioner_deploy_status
|
||||
until: >-
|
||||
__nfs_provisioner_deploy_status.resources | length > 0 and
|
||||
(__nfs_provisioner_deploy_status.resources[0].status.readyReplicas | default(0)) >= 1
|
||||
retries: "{{ __nfs_provisioner_wait_retries }}"
|
||||
delay: 10
|
||||
|
||||
- name: Display NFS provisioner summary
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "NFS provisioner deployment complete!"
|
||||
- " Namespace : {{ nfs_provisioner_namespace }}"
|
||||
- " NFS server : {{ __nfs_provisioner_server_addr }}:{{ __nfs_provisioner_server_path }}"
|
||||
- " Mode : {{ 'external' if nfs_provisioner_external_server | length > 0 else 'in-cluster (LVMS-backed)' }}"
|
||||
- " StorageClass : {{ nfs_provisioner_storage_class_name }} (ReadWriteMany)"
|
||||
3
roles/nfs_provisioner/vars/main.yml
Normal file
3
roles/nfs_provisioner/vars/main.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
# Computed internal variables - do not override
|
||||
__nfs_provisioner_wait_retries: "{{ (nfs_provisioner_wait_timeout / 10) | int }}"
|
||||
6
roles/ocp_service_account/defaults/main.yml
Normal file
6
roles/ocp_service_account/defaults/main.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
# ocp_service_account_name: "" # required — SA and ClusterRole name
|
||||
# ocp_service_account_namespace: "" # required — namespace for SA and token secret
|
||||
# ocp_service_account_cluster_role_rules: [] # required — list of RBAC policy rules
|
||||
|
||||
ocp_service_account_create_namespace: true
|
||||
29
roles/ocp_service_account/meta/argument_specs.yml
Normal file
29
roles/ocp_service_account/meta/argument_specs.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
argument_specs:
|
||||
main:
|
||||
short_description: Create an OpenShift ServiceAccount with scoped ClusterRole
|
||||
description:
|
||||
- Creates a ServiceAccount, ClusterRole, ClusterRoleBinding, and a
|
||||
long-lived token Secret. The token is registered as
|
||||
__ocp_service_account_token for downstream use.
|
||||
options:
|
||||
ocp_service_account_name:
|
||||
description: Name for the ServiceAccount, ClusterRole, and ClusterRoleBinding.
|
||||
type: str
|
||||
required: true
|
||||
ocp_service_account_namespace:
|
||||
description: Namespace where the ServiceAccount and token Secret are created.
|
||||
type: str
|
||||
required: true
|
||||
ocp_service_account_cluster_role_rules:
|
||||
description: >-
|
||||
List of RBAC policy rules for the ClusterRole.
|
||||
Each item follows the Kubernetes PolicyRule schema
|
||||
(apiGroups, resources, verbs).
|
||||
type: list
|
||||
elements: dict
|
||||
required: true
|
||||
ocp_service_account_create_namespace:
|
||||
description: Whether to create the namespace if it does not exist.
|
||||
type: bool
|
||||
default: true
|
||||
16
roles/ocp_service_account/meta/main.yml
Normal file
16
roles/ocp_service_account/meta/main.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: ptoal
|
||||
description: Create an OpenShift ServiceAccount with ClusterRole and long-lived token
|
||||
license: MIT
|
||||
min_ansible_version: "2.16"
|
||||
platforms:
|
||||
- name: GenericLinux
|
||||
versions:
|
||||
- all
|
||||
galaxy_tags:
|
||||
- openshift
|
||||
- rbac
|
||||
- serviceaccount
|
||||
|
||||
dependencies: []
|
||||
111
roles/ocp_service_account/tasks/main.yml
Normal file
111
roles/ocp_service_account/tasks/main.yml
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
# Create an OpenShift ServiceAccount with a scoped ClusterRole and long-lived token.
|
||||
#
|
||||
# Requires: ocp_service_account_name, ocp_service_account_namespace,
|
||||
# ocp_service_account_cluster_role_rules
|
||||
#
|
||||
# Registers: __ocp_service_account_token (decoded bearer token)
|
||||
|
||||
- name: Validate required variables
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ocp_service_account_name | length > 0
|
||||
- ocp_service_account_namespace | length > 0
|
||||
- ocp_service_account_cluster_role_rules | length > 0
|
||||
fail_msg: "ocp_service_account_name, ocp_service_account_namespace, and ocp_service_account_cluster_role_rules are required"
|
||||
|
||||
- name: Create namespace {{ ocp_service_account_namespace }}
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: "{{ ocp_service_account_namespace }}"
|
||||
when: ocp_service_account_create_namespace | bool
|
||||
|
||||
- name: Create ServiceAccount {{ ocp_service_account_name }}
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: "{{ ocp_service_account_name }}"
|
||||
namespace: "{{ ocp_service_account_namespace }}"
|
||||
labels:
|
||||
app.kubernetes.io/managed-by: ocp-service-account-role
|
||||
|
||||
- name: Create ClusterRole {{ ocp_service_account_name }}
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: "{{ ocp_service_account_name }}"
|
||||
labels:
|
||||
app.kubernetes.io/managed-by: ocp-service-account-role
|
||||
rules: "{{ ocp_service_account_cluster_role_rules }}"
|
||||
|
||||
- name: Create ClusterRoleBinding {{ ocp_service_account_name }}
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: "{{ ocp_service_account_name }}"
|
||||
labels:
|
||||
app.kubernetes.io/managed-by: ocp-service-account-role
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: "{{ ocp_service_account_name }}"
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: "{{ ocp_service_account_name }}"
|
||||
namespace: "{{ ocp_service_account_namespace }}"
|
||||
|
||||
- name: Create long-lived token Secret for {{ ocp_service_account_name }}
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: "{{ ocp_service_account_name }}-token"
|
||||
namespace: "{{ ocp_service_account_namespace }}"
|
||||
labels:
|
||||
app.kubernetes.io/managed-by: ocp-service-account-role
|
||||
app.kubernetes.io/instance: "{{ ocp_service_account_name }}"
|
||||
annotations:
|
||||
kubernetes.io/service-account.name: "{{ ocp_service_account_name }}"
|
||||
type: kubernetes.io/service-account-token
|
||||
|
||||
- name: Wait for token to be populated
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: v1
|
||||
kind: Secret
|
||||
namespace: "{{ ocp_service_account_namespace }}"
|
||||
name: "{{ ocp_service_account_name }}-token"
|
||||
register: __ocp_sa_token_secret
|
||||
until: >-
|
||||
__ocp_sa_token_secret.resources | length > 0 and
|
||||
(__ocp_sa_token_secret.resources[0].data.token | default('') | length > 0)
|
||||
retries: 12
|
||||
delay: 5
|
||||
|
||||
- name: Register SA token for downstream use
|
||||
ansible.builtin.set_fact:
|
||||
__ocp_service_account_token: "{{ __ocp_sa_token_secret.resources[0].data.token | b64decode }}"
|
||||
no_log: true
|
||||
|
||||
- name: Display SA token for vault storage
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "*** SERVICE ACCOUNT TOKEN — SAVE TO 1PASSWORD ***"
|
||||
- "ServiceAccount: {{ ocp_service_account_name }} ({{ ocp_service_account_namespace }})"
|
||||
- "Vault variable: vault_{{ ocp_service_account_name | regex_replace('-', '_') }}_token"
|
||||
- ""
|
||||
- "Token: {{ __ocp_service_account_token }}"
|
||||
58
roles/opa_server/defaults/main.yml
Normal file
58
roles/opa_server/defaults/main.yml
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
# --- Deployment ---
|
||||
opa_server_namespace: opa
|
||||
opa_server_name: opa
|
||||
opa_server_image: docker.io/openpolicyagent/opa:1.21.1-static
|
||||
opa_server_replicas: 1
|
||||
opa_server_port: 8443
|
||||
opa_server_log_level: info
|
||||
# Log every decision (including the job's extra_vars) to the pod log for auditing.
|
||||
opa_server_decision_logs: true
|
||||
opa_server_resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
opa_server_wait_timeout: 300
|
||||
|
||||
# --- Policies ---
|
||||
# Every *.rego file in this directory is loaded into the server.
|
||||
opa_server_policy_dir: "{{ role_path }}/files/policies"
|
||||
# Exposed to policies as data.aap_policy_config (see files/policies/aap_policy.rego).
|
||||
opa_server_policy_config:
|
||||
allowed_ee_registries:
|
||||
- aap.toal.ca/
|
||||
- registry.redhat.io/
|
||||
forbidden_extra_vars:
|
||||
- ansible_password
|
||||
- ansible_become_password
|
||||
- ansible_ssh_pass
|
||||
- ansible_become_pass
|
||||
change_freeze: false
|
||||
change_freeze_exempt_teams: []
|
||||
|
||||
# --- Bearer token (OPA --authentication=token) ---
|
||||
# Generated on first run and stored in Vault; reused afterwards.
|
||||
opa_server_vault_mount: kv
|
||||
opa_server_vault_path: toallab/opa-server
|
||||
|
||||
# --- AAP integration ---
|
||||
opa_server_configure_aap: true
|
||||
opa_server_aap_url: "{{ aap_gateway_url }}"
|
||||
opa_server_aap_username: admin
|
||||
opa_server_aap_namespace: aap
|
||||
opa_server_aap_admin_secret: aap-admin-password
|
||||
opa_server_aap_validate_certs: true
|
||||
opa_server_aap_request_timeout: 1.5
|
||||
opa_server_aap_request_retries: 2
|
||||
# Attach a policy to AAP resources. Jobs fail closed: any OPA error or violation
|
||||
# blocks the launch, so start with aap/smoke/allow_all and a test resource.
|
||||
# - type: organizations | inventories | job_templates
|
||||
# name: Default
|
||||
# query_path: aap/policy/decision
|
||||
opa_server_aap_query_paths: []
|
||||
|
||||
# --- Debugging ---
|
||||
opa_server_no_log: true
|
||||
65
roles/opa_server/files/policies/aap_policy.rego
Normal file
65
roles/opa_server/files/policies/aap_policy.rego
Normal file
@@ -0,0 +1,65 @@
|
||||
# AAP policy-as-code baseline.
|
||||
# Set opa_query_path to aap/policy/decision on an organization, inventory, or
|
||||
# job template. Input is the serialized Job (awx/main/tasks/policy.py).
|
||||
# Tunables come from data.aap_policy_config (opa_server_policy_config).
|
||||
package aap.policy
|
||||
|
||||
import rego.v1
|
||||
|
||||
decision := {
|
||||
"allowed": count(violations) == 0,
|
||||
"violations": sort([msg | some msg in violations]),
|
||||
}
|
||||
|
||||
default_config := {
|
||||
"allowed_ee_registries": [],
|
||||
"forbidden_extra_vars": [],
|
||||
"change_freeze": false,
|
||||
"change_freeze_exempt_teams": [],
|
||||
}
|
||||
|
||||
default user_config := {}
|
||||
|
||||
user_config := data.aap_policy_config
|
||||
|
||||
config := object.union(default_config, user_config)
|
||||
|
||||
# Execution environment must come from an approved registry
|
||||
violations contains msg if {
|
||||
count(config.allowed_ee_registries) > 0
|
||||
image := input.execution_environment.image
|
||||
not approved_registry(image)
|
||||
msg := sprintf(
|
||||
"Execution environment image %q is not from an approved registry (%s)",
|
||||
[image, concat(", ", config.allowed_ee_registries)],
|
||||
)
|
||||
}
|
||||
|
||||
approved_registry(image) if {
|
||||
some prefix in config.allowed_ee_registries
|
||||
startswith(image, prefix)
|
||||
}
|
||||
|
||||
# Credentials belong in AAP credentials, not launch-time extra vars
|
||||
violations contains msg if {
|
||||
extra_vars := object.get(input, "extra_vars", {})
|
||||
is_object(extra_vars)
|
||||
some key, _ in extra_vars
|
||||
lower(key) in {lower(k) | some k in config.forbidden_extra_vars}
|
||||
msg := sprintf("Extra variable %q may not be passed at launch; use a credential", [key])
|
||||
}
|
||||
|
||||
# Change freeze: only check-mode jobs unless the launcher is exempt
|
||||
violations contains msg if {
|
||||
config.change_freeze
|
||||
input.job_type == "run"
|
||||
not freeze_exempt
|
||||
msg := "A change freeze is in effect: only check-mode jobs are allowed"
|
||||
}
|
||||
|
||||
freeze_exempt if input.created_by.is_superuser
|
||||
|
||||
freeze_exempt if {
|
||||
some team in input.created_by.teams
|
||||
team.name in config.change_freeze_exempt_teams
|
||||
}
|
||||
8
roles/opa_server/files/policies/aap_smoke.rego
Normal file
8
roles/opa_server/files/policies/aap_smoke.rego
Normal file
@@ -0,0 +1,8 @@
|
||||
# Wiring checks for AAP policy enforcement.
|
||||
# aap/smoke/allow_all - always passes (safe first attachment)
|
||||
# aap/smoke/deny_all - always blocks, proving enforcement is active
|
||||
package aap.smoke
|
||||
|
||||
allow_all := {"allowed": true, "violations": []}
|
||||
|
||||
deny_all := {"allowed": false, "violations": ["aap.smoke.deny_all: policy enforcement is active"]}
|
||||
11
roles/opa_server/files/policies/authz.rego
Normal file
11
roles/opa_server/files/policies/authz.rego
Normal file
@@ -0,0 +1,11 @@
|
||||
# Server authorization (opa run --authorization=basic).
|
||||
# Health checks are open; everything else needs the bearer token.
|
||||
package system.authz
|
||||
|
||||
import rego.v1
|
||||
|
||||
default allow := false
|
||||
|
||||
allow if input.path == ["health"]
|
||||
|
||||
allow if input.identity == data.opa_authz.token
|
||||
87
roles/opa_server/files/tests/aap_policy_test.rego
Normal file
87
roles/opa_server/files/tests/aap_policy_test.rego
Normal file
@@ -0,0 +1,87 @@
|
||||
# Unit tests (not deployed). Run with:
|
||||
# podman run --rm -v ./roles/opa_server/files:/files:Z docker.io/openpolicyagent/opa:1.21.1-static \
|
||||
# test /files/policies /files/tests -v
|
||||
package aap.policy_test
|
||||
|
||||
import rego.v1
|
||||
|
||||
import data.aap.policy
|
||||
|
||||
cfg := {
|
||||
"allowed_ee_registries": ["aap.toal.ca/", "registry.redhat.io/"],
|
||||
"forbidden_extra_vars": ["ansible_password"],
|
||||
"change_freeze": false,
|
||||
"change_freeze_exempt_teams": ["Platform"],
|
||||
}
|
||||
|
||||
job := {
|
||||
"job_type": "run",
|
||||
"execution_environment": {"image": "aap.toal.ca/toallab/ee-demo:latest"},
|
||||
"extra_vars": {"target": "web"},
|
||||
"created_by": {"username": "dev", "is_superuser": false, "teams": [{"id": 1, "name": "Dev"}]},
|
||||
}
|
||||
|
||||
test_compliant_job_allowed if {
|
||||
d := policy.decision with input as job with data.aap_policy_config as cfg
|
||||
d.allowed
|
||||
d.violations == []
|
||||
}
|
||||
|
||||
test_unapproved_registry_denied if {
|
||||
j := object.union(job, {"execution_environment": {"image": "quay.io/scottharwell/cloud-ee:latest"}})
|
||||
d := policy.decision with input as j with data.aap_policy_config as cfg
|
||||
not d.allowed
|
||||
count(d.violations) == 1
|
||||
}
|
||||
|
||||
test_forbidden_extra_var_denied_case_insensitive if {
|
||||
j := object.union(job, {"extra_vars": {"Ansible_Password": "x"}})
|
||||
d := policy.decision with input as j with data.aap_policy_config as cfg
|
||||
not d.allowed
|
||||
}
|
||||
|
||||
test_null_extra_vars_and_ee_allowed if {
|
||||
j := object.union(job, {"extra_vars": null, "execution_environment": null})
|
||||
d := policy.decision with input as j with data.aap_policy_config as cfg
|
||||
d.allowed
|
||||
}
|
||||
|
||||
test_freeze_blocks_run if {
|
||||
d := policy.decision with input as job with data.aap_policy_config as object.union(cfg, {"change_freeze": true})
|
||||
not d.allowed
|
||||
}
|
||||
|
||||
test_freeze_allows_check if {
|
||||
j := object.union(job, {"job_type": "check"})
|
||||
d := policy.decision with input as j with data.aap_policy_config as object.union(cfg, {"change_freeze": true})
|
||||
d.allowed
|
||||
}
|
||||
|
||||
test_freeze_exempt_team if {
|
||||
j := object.union(job, {"created_by": {"is_superuser": false, "teams": [{"name": "Platform"}]}})
|
||||
d := policy.decision with input as j with data.aap_policy_config as object.union(cfg, {"change_freeze": true})
|
||||
d.allowed
|
||||
}
|
||||
|
||||
test_freeze_scheduled_job_without_creator if {
|
||||
j := object.union(job, {"created_by": null})
|
||||
d := policy.decision with input as j with data.aap_policy_config as object.union(cfg, {"change_freeze": true})
|
||||
not d.allowed
|
||||
}
|
||||
|
||||
test_no_config_allows_everything if {
|
||||
j := object.union(job, {"execution_environment": {"image": "quay.io/anything:latest"}})
|
||||
d := policy.decision with input as j
|
||||
d.allowed
|
||||
}
|
||||
|
||||
test_authz_health_open if {
|
||||
data.system.authz.allow with input as {"path": ["health"]}
|
||||
}
|
||||
|
||||
test_authz_requires_token if {
|
||||
not data.system.authz.allow with input as {"path": ["v1", "data"], "identity": "bad"}
|
||||
with data.opa_authz.token as "good"
|
||||
data.system.authz.allow with input as {"path": ["v1", "data"], "identity": "good"}
|
||||
with data.opa_authz.token as "good"
|
||||
}
|
||||
103
roles/opa_server/meta/argument_specs.yml
Normal file
103
roles/opa_server/meta/argument_specs.yml
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
argument_specs:
|
||||
main:
|
||||
short_description: Deploy an OPA server for AAP policy enforcement
|
||||
description:
|
||||
- Deploys Open Policy Agent with TLS (service-ca) and bearer-token authentication, loading
|
||||
the Rego policies in opa_server_policy_dir and opa_server_policy_config as
|
||||
data.aap_policy_config.
|
||||
- Configures AAP's PolicyAsCode settings to use it and optionally attaches opa_query_path
|
||||
to organizations, inventories, or job templates. AAP fails closed once a path is attached.
|
||||
- The bearer token is generated on first run and stored in Vault at opa_server_vault_path.
|
||||
options:
|
||||
opa_server_namespace:
|
||||
description: Namespace for the OPA server.
|
||||
type: str
|
||||
default: opa
|
||||
opa_server_name:
|
||||
description: Name for the Deployment, Service, and derived resources.
|
||||
type: str
|
||||
default: opa
|
||||
opa_server_image:
|
||||
description: OPA container image (pin an exact version).
|
||||
type: str
|
||||
default: docker.io/openpolicyagent/opa:1.21.1-static
|
||||
opa_server_replicas:
|
||||
description: Number of OPA replicas.
|
||||
type: int
|
||||
default: 1
|
||||
opa_server_port:
|
||||
description: HTTPS port OPA listens on and the Service exposes.
|
||||
type: int
|
||||
default: 8443
|
||||
opa_server_log_level:
|
||||
description: OPA log level.
|
||||
type: str
|
||||
choices: [debug, info, warn, error]
|
||||
default: info
|
||||
opa_server_decision_logs:
|
||||
description: Log every decision (including job extra_vars) to the pod log.
|
||||
type: bool
|
||||
default: true
|
||||
opa_server_resources:
|
||||
description: Container resource requests and limits.
|
||||
type: dict
|
||||
opa_server_wait_timeout:
|
||||
description: Seconds to wait for the OPA Deployment to become available.
|
||||
type: int
|
||||
default: 300
|
||||
opa_server_policy_dir:
|
||||
description: Directory whose *.rego files are loaded into OPA.
|
||||
type: path
|
||||
opa_server_policy_config:
|
||||
description: Policy tunables exposed as data.aap_policy_config.
|
||||
type: dict
|
||||
opa_server_vault_mount:
|
||||
description: Vault KV2 mount for the bearer token.
|
||||
type: str
|
||||
default: kv
|
||||
opa_server_vault_path:
|
||||
description: Vault KV2 path for the bearer token.
|
||||
type: str
|
||||
default: toallab/opa-server
|
||||
opa_server_configure_aap:
|
||||
description: Configure AAP's PolicyAsCode settings to use this server.
|
||||
type: bool
|
||||
default: true
|
||||
opa_server_aap_url:
|
||||
description: AAP gateway URL.
|
||||
type: str
|
||||
opa_server_aap_username:
|
||||
description: AAP admin username.
|
||||
type: str
|
||||
default: admin
|
||||
opa_server_aap_namespace:
|
||||
description: Namespace of the AAP instance (holds the admin password secret).
|
||||
type: str
|
||||
default: aap
|
||||
opa_server_aap_admin_secret:
|
||||
description: Secret holding the AAP admin password (key password).
|
||||
type: str
|
||||
default: aap-admin-password
|
||||
opa_server_aap_validate_certs:
|
||||
description: Validate the AAP gateway certificate.
|
||||
type: bool
|
||||
default: true
|
||||
opa_server_aap_request_timeout:
|
||||
description: OPA_REQUEST_TIMEOUT in seconds.
|
||||
type: float
|
||||
default: 1.5
|
||||
opa_server_aap_request_retries:
|
||||
description: OPA_REQUEST_RETRIES.
|
||||
type: int
|
||||
default: 2
|
||||
opa_server_aap_query_paths:
|
||||
description: Resources to attach a policy to (type organizations|inventories|job_templates,
|
||||
name, query_path).
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
opa_server_no_log:
|
||||
description: Set to false to expose secret values in task output.
|
||||
type: bool
|
||||
default: true
|
||||
17
roles/opa_server/meta/main.yml
Normal file
17
roles/opa_server/meta/main.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: ptoal
|
||||
description: Deploy an Open Policy Agent server on OpenShift for AAP policy enforcement
|
||||
license: MIT
|
||||
min_ansible_version: "2.16"
|
||||
platforms:
|
||||
- name: GenericLinux
|
||||
versions:
|
||||
- all
|
||||
galaxy_tags:
|
||||
- openshift
|
||||
- opa
|
||||
- policy
|
||||
- aap
|
||||
|
||||
dependencies: []
|
||||
95
roles/opa_server/tasks/aap.yml
Normal file
95
roles/opa_server/tasks/aap.yml
Normal file
@@ -0,0 +1,95 @@
|
||||
---
|
||||
# Point the AAP controller at OPA (PolicyAsCode settings) and optionally attach
|
||||
# query paths to organizations, inventories, or job templates.
|
||||
|
||||
- name: Read AAP admin password
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: v1
|
||||
kind: Secret
|
||||
namespace: "{{ opa_server_aap_namespace }}"
|
||||
name: "{{ opa_server_aap_admin_secret }}"
|
||||
register: __opa_server_aap_admin
|
||||
no_log: true
|
||||
|
||||
- name: Read the service CA bundle
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: v1
|
||||
kind: ConfigMap
|
||||
namespace: "{{ opa_server_namespace }}"
|
||||
name: openshift-service-ca.crt
|
||||
register: __opa_server_service_ca
|
||||
|
||||
- name: Resolve AAP API connection
|
||||
ansible.builtin.set_fact:
|
||||
__opa_server_aap_api: "{{ opa_server_aap_url | regex_replace('/$', '') }}/api/controller/v2"
|
||||
__opa_server_aap_password: "{{ __opa_server_aap_admin.resources[0].data.password | b64decode }}"
|
||||
__opa_server_aap_settings:
|
||||
OPA_HOST: "{{ __opa_server_host }}"
|
||||
OPA_PORT: "{{ opa_server_port | int }}"
|
||||
OPA_SSL: true
|
||||
OPA_AUTH_TYPE: Token
|
||||
OPA_AUTH_CA_CERT: "{{ __opa_server_service_ca.resources[0].data['service-ca.crt'] }}"
|
||||
OPA_REQUEST_TIMEOUT: "{{ opa_server_aap_request_timeout | float }}"
|
||||
OPA_REQUEST_RETRIES: "{{ opa_server_aap_request_retries | int }}"
|
||||
no_log: true
|
||||
|
||||
- name: Read AAP policy-as-code settings
|
||||
ansible.builtin.uri:
|
||||
url: "{{ __opa_server_aap_api }}/settings/policyascode/"
|
||||
user: "{{ opa_server_aap_username }}"
|
||||
password: "{{ __opa_server_aap_password }}"
|
||||
force_basic_auth: true
|
||||
validate_certs: "{{ opa_server_aap_validate_certs }}"
|
||||
register: __opa_server_aap_current
|
||||
no_log: "{{ opa_server_no_log }}"
|
||||
|
||||
# The token reads back as $encrypted$, so it is only resent when unset or rotated
|
||||
- name: Configure AAP policy-as-code settings
|
||||
ansible.builtin.uri:
|
||||
url: "{{ __opa_server_aap_api }}/settings/policyascode/"
|
||||
method: PATCH
|
||||
user: "{{ opa_server_aap_username }}"
|
||||
password: "{{ __opa_server_aap_password }}"
|
||||
force_basic_auth: true
|
||||
validate_certs: "{{ opa_server_aap_validate_certs }}"
|
||||
body_format: json
|
||||
body: "{{ __opa_server_aap_settings | combine({'OPA_AUTH_TOKEN': __opa_server_token}) }}"
|
||||
changed_when: true
|
||||
when: >-
|
||||
(__opa_server_aap_current.json | dict2items
|
||||
| selectattr('key', 'in', __opa_server_aap_settings.keys() | list)
|
||||
| items2dict) != __opa_server_aap_settings
|
||||
or (__opa_server_aap_current.json.OPA_AUTH_TOKEN | default('')) | length == 0
|
||||
or __opa_server_token_generated | bool
|
||||
no_log: "{{ opa_server_no_log }}"
|
||||
|
||||
- name: Look up resources to attach policies to
|
||||
ansible.builtin.uri:
|
||||
url: "{{ __opa_server_aap_api }}/{{ item.type }}/?name={{ item.name | urlencode }}"
|
||||
user: "{{ opa_server_aap_username }}"
|
||||
password: "{{ __opa_server_aap_password }}"
|
||||
force_basic_auth: true
|
||||
validate_certs: "{{ opa_server_aap_validate_certs }}"
|
||||
register: __opa_server_aap_targets
|
||||
failed_when: >-
|
||||
__opa_server_aap_targets.status != 200 or __opa_server_aap_targets.json.count != 1
|
||||
loop: "{{ opa_server_aap_query_paths }}"
|
||||
loop_control:
|
||||
label: "{{ item.type }}/{{ item.name }}"
|
||||
|
||||
- name: Attach OPA query paths
|
||||
ansible.builtin.uri:
|
||||
url: "{{ __opa_server_aap_api }}/{{ item.item.type }}/{{ item.json.results[0].id }}/"
|
||||
method: PATCH
|
||||
user: "{{ opa_server_aap_username }}"
|
||||
password: "{{ __opa_server_aap_password }}"
|
||||
force_basic_auth: true
|
||||
validate_certs: "{{ opa_server_aap_validate_certs }}"
|
||||
body_format: json
|
||||
body:
|
||||
opa_query_path: "{{ item.item.query_path }}"
|
||||
changed_when: true
|
||||
when: item.json.results[0].opa_query_path | default('') != item.item.query_path
|
||||
loop: "{{ __opa_server_aap_targets.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.type }}/{{ item.item.name }} -> {{ item.item.query_path }}"
|
||||
172
roles/opa_server/tasks/deploy.yml
Normal file
172
roles/opa_server/tasks/deploy.yml
Normal file
@@ -0,0 +1,172 @@
|
||||
---
|
||||
- name: Create OPA namespace
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: "{{ opa_server_namespace }}"
|
||||
|
||||
- name: Create OPA policy ConfigMap
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
apply: true
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: "{{ opa_server_name }}-policies"
|
||||
namespace: "{{ opa_server_namespace }}"
|
||||
data: >-
|
||||
{{ __opa_server_policies
|
||||
| combine({'aap_policy_config.json': {'aap_policy_config': opa_server_policy_config} | to_json}) }}
|
||||
|
||||
- name: Create OPA authorization token Secret
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: "{{ opa_server_name }}-authz"
|
||||
namespace: "{{ opa_server_namespace }}"
|
||||
type: Opaque
|
||||
stringData:
|
||||
opa_authz.json: "{{ {'opa_authz': {'token': __opa_server_token}} | to_json }}"
|
||||
no_log: "{{ opa_server_no_log }}"
|
||||
|
||||
- name: Create OPA Service with a service-ca serving certificate
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: "{{ opa_server_name }}"
|
||||
namespace: "{{ opa_server_namespace }}"
|
||||
annotations:
|
||||
service.beta.openshift.io/serving-cert-secret-name: "{{ __opa_server_tls_secret }}"
|
||||
spec:
|
||||
selector:
|
||||
app: "{{ opa_server_name }}"
|
||||
ports:
|
||||
- name: https
|
||||
port: "{{ opa_server_port | int }}"
|
||||
targetPort: https
|
||||
|
||||
- name: Wait for OPA serving certificate
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: v1
|
||||
kind: Secret
|
||||
namespace: "{{ opa_server_namespace }}"
|
||||
name: "{{ __opa_server_tls_secret }}"
|
||||
register: __opa_server_cert
|
||||
until: __opa_server_cert.resources | length > 0
|
||||
retries: 30
|
||||
delay: 10
|
||||
no_log: true
|
||||
|
||||
- name: Create OPA Deployment
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: "{{ opa_server_name }}"
|
||||
namespace: "{{ opa_server_namespace }}"
|
||||
labels:
|
||||
app: "{{ opa_server_name }}"
|
||||
spec:
|
||||
replicas: "{{ opa_server_replicas | int }}"
|
||||
selector:
|
||||
matchLabels:
|
||||
app: "{{ opa_server_name }}"
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: "{{ opa_server_name }}"
|
||||
annotations:
|
||||
# Roll pods when policies, config, or the token change
|
||||
checksum/policies: "{{ (__opa_server_policies | to_json ~ opa_server_policy_config | to_json) | hash('sha256') }}"
|
||||
checksum/authz: "{{ __opa_server_token | hash('sha256') }}"
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: opa
|
||||
image: "{{ opa_server_image }}"
|
||||
args: >-
|
||||
{{ [
|
||||
'run', '--server',
|
||||
'--addr=0.0.0.0:' ~ opa_server_port,
|
||||
'--tls-cert-file=/certs/tls.crt',
|
||||
'--tls-private-key-file=/certs/tls.key',
|
||||
'--authentication=token',
|
||||
'--authorization=basic',
|
||||
'--ignore=.*',
|
||||
'--log-format=json',
|
||||
'--log-level=' ~ opa_server_log_level,
|
||||
]
|
||||
+ (['--set=decision_logs.console=true'] if opa_server_decision_logs | bool else [])
|
||||
+ ['/policies', '/authz'] }}
|
||||
ports:
|
||||
- name: https
|
||||
containerPort: "{{ opa_server_port | int }}"
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: https
|
||||
scheme: HTTPS
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: https
|
||||
scheme: HTTPS
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 20
|
||||
resources: "{{ opa_server_resources }}"
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: [ALL]
|
||||
volumeMounts:
|
||||
- name: policies
|
||||
mountPath: /policies
|
||||
readOnly: true
|
||||
- name: authz
|
||||
mountPath: /authz
|
||||
readOnly: true
|
||||
- name: certs
|
||||
mountPath: /certs
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: policies
|
||||
configMap:
|
||||
name: "{{ opa_server_name }}-policies"
|
||||
- name: authz
|
||||
secret:
|
||||
secretName: "{{ opa_server_name }}-authz"
|
||||
- name: certs
|
||||
secret:
|
||||
secretName: "{{ __opa_server_tls_secret }}"
|
||||
|
||||
- name: Wait for OPA to be available
|
||||
kubernetes.core.k8s_info:
|
||||
api_version: apps/v1
|
||||
kind: Deployment
|
||||
namespace: "{{ opa_server_namespace }}"
|
||||
name: "{{ opa_server_name }}"
|
||||
register: __opa_server_deploy
|
||||
until: >-
|
||||
__opa_server_deploy.resources | length > 0 and
|
||||
(__opa_server_deploy.resources[0].status.updatedReplicas | default(0)) == opa_server_replicas | int and
|
||||
(__opa_server_deploy.resources[0].status.availableReplicas | default(0)) == opa_server_replicas | int
|
||||
retries: "{{ __opa_server_wait_retries }}"
|
||||
delay: 10
|
||||
32
roles/opa_server/tasks/main.yml
Normal file
32
roles/opa_server/tasks/main.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
# Deploy an Open Policy Agent server for AAP policy enforcement (policy as code)
|
||||
# and point AAP's controller at it.
|
||||
#
|
||||
# OPA serves TLS with a service-ca certificate and requires a bearer token
|
||||
# (--authentication=token, system.authz policy). AAP connects in-cluster with
|
||||
# OPA_SSL=true, OPA_AUTH_TYPE=Token and the service CA bundle.
|
||||
#
|
||||
# AAP fails closed: once a resource has an opa_query_path, any OPA error or
|
||||
# violation blocks its jobs. Nothing is attached unless opa_server_aap_query_paths
|
||||
# lists it.
|
||||
#
|
||||
# All tasks are idempotent (kubernetes.core.k8s state: present).
|
||||
|
||||
- name: Resolve OPA bearer token from Vault
|
||||
ansible.builtin.include_tasks: vault_token.yml
|
||||
|
||||
- name: Deploy OPA server
|
||||
ansible.builtin.include_tasks: deploy.yml
|
||||
|
||||
- name: Configure AAP policy enforcement
|
||||
ansible.builtin.include_tasks: aap.yml
|
||||
when: opa_server_configure_aap | bool
|
||||
|
||||
- name: Display OPA deployment summary
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "OPA server ready!"
|
||||
- " Endpoint : https://{{ __opa_server_host }}:{{ opa_server_port }} (in-cluster)"
|
||||
- " Policies : {{ __opa_server_policies.keys() | list | join(', ') }}"
|
||||
- " Query paths : aap/policy/decision, aap/smoke/allow_all, aap/smoke/deny_all"
|
||||
- " Token : Vault {{ opa_server_vault_mount }}/{{ opa_server_vault_path }}"
|
||||
28
roles/opa_server/tasks/vault_token.yml
Normal file
28
roles/opa_server/tasks/vault_token.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
# Read the OPA bearer token from Vault; generate and store it if missing.
|
||||
# Sets __opa_server_token and __opa_server_token_generated.
|
||||
|
||||
- name: Read OPA token from Vault
|
||||
community.hashi_vault.vault_kv2_get:
|
||||
path: "{{ opa_server_vault_path }}"
|
||||
engine_mount_point: "{{ opa_server_vault_mount }}"
|
||||
register: __opa_server_vault_read
|
||||
failed_when: false
|
||||
no_log: "{{ opa_server_no_log }}"
|
||||
|
||||
- name: Resolve OPA token (generate if missing)
|
||||
ansible.builtin.set_fact:
|
||||
__opa_server_token_generated: "{{ ((__opa_server_vault_read.secret | default({})).token | default('')) | length == 0 }}"
|
||||
__opa_server_token: >-
|
||||
{{ (__opa_server_vault_read.secret | default({})).token
|
||||
| default(lookup('community.general.random_string', length=48, special=false)) }}
|
||||
no_log: "{{ opa_server_no_log }}"
|
||||
|
||||
- name: Write OPA token to Vault
|
||||
community.hashi_vault.vault_kv2_write:
|
||||
path: "{{ opa_server_vault_path }}"
|
||||
engine_mount_point: "{{ opa_server_vault_mount }}"
|
||||
data:
|
||||
token: "{{ __opa_server_token }}"
|
||||
when: __opa_server_token_generated | bool
|
||||
no_log: "{{ opa_server_no_log }}"
|
||||
9
roles/opa_server/vars/main.yml
Normal file
9
roles/opa_server/vars/main.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
# Computed internal variables - do not override
|
||||
__opa_server_wait_retries: "{{ (opa_server_wait_timeout / 10) | int }}"
|
||||
__opa_server_host: "{{ opa_server_name }}.{{ opa_server_namespace }}.svc"
|
||||
__opa_server_tls_secret: "{{ opa_server_name }}-tls"
|
||||
__opa_server_policy_files: "{{ query('ansible.builtin.fileglob', opa_server_policy_dir ~ '/*.rego') | sort }}"
|
||||
__opa_server_policies: >-
|
||||
{{ dict(__opa_server_policy_files | map('basename')
|
||||
| zip(query('ansible.builtin.file', *__opa_server_policy_files))) }}
|
||||
61
roles/opnsense_dns_override/README.md
Normal file
61
roles/opnsense_dns_override/README.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# opnsense_dns_override
|
||||
|
||||
Manages OPNsense Unbound DNS host overrides (A record) and domain forwards via the `oxlorg.opnsense` collection.
|
||||
|
||||
Accepts a list of entries, each specifying either a `host` override or a `forward` rule. All tasks delegate to localhost (OPNsense modules are API-based).
|
||||
|
||||
## Requirements
|
||||
|
||||
- `oxlorg.opnsense` collection
|
||||
- `module_defaults` for `group/oxlorg.opnsense.all` must be set at play level (firewall, api_key, api_secret)
|
||||
|
||||
## Role Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `opnsense_dns_override_entries` | `[]` | List of DNS override entries (see below) |
|
||||
|
||||
### Entry format
|
||||
|
||||
Each entry in `opnsense_dns_override_entries` requires:
|
||||
|
||||
| Field | Required | Description |
|
||||
|---|---|---|
|
||||
| `type` | yes | `host` for Unbound host override, `forward` for domain forwarding |
|
||||
| `value` | yes | Target IP address |
|
||||
| `hostname` | host only | Subdomain part (e.g. `api.sno`) |
|
||||
| `domain` | yes | Parent domain for host type, or full domain for forward type |
|
||||
|
||||
## Example Playbook
|
||||
|
||||
```yaml
|
||||
- name: Configure OPNsense DNS overrides
|
||||
hosts: gate.toal.ca
|
||||
gather_facts: false
|
||||
connection: local
|
||||
|
||||
module_defaults:
|
||||
group/oxlorg.opnsense.all:
|
||||
firewall: "{{ opnsense_host }}"
|
||||
api_key: "{{ opnsense_api_key }}"
|
||||
api_secret: "{{ opnsense_api_secret }}"
|
||||
|
||||
roles:
|
||||
- role: opnsense_dns_override
|
||||
opnsense_dns_override_entries:
|
||||
- hostname: api.sno
|
||||
domain: openshift.toal.ca
|
||||
value: 192.168.40.10
|
||||
type: host
|
||||
- domain: apps.sno.openshift.toal.ca
|
||||
value: 192.168.40.10
|
||||
type: forward
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Author
|
||||
|
||||
ptoal
|
||||
26
roles/opnsense_dns_override/defaults/main.yml
Normal file
26
roles/opnsense_dns_override/defaults/main.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
# List of DNS override entries to create in OPNsense Unbound.
|
||||
#
|
||||
# Each entry must have:
|
||||
# type: "host" for unbound_host (A record override) or
|
||||
# "forward" for unbound_forward (domain forwarding)
|
||||
#
|
||||
# For type "host":
|
||||
# hostname: subdomain part (e.g. "api.sno")
|
||||
# domain: parent domain (e.g. "openshift.toal.ca")
|
||||
# value: target IP address
|
||||
#
|
||||
# For type "forward":
|
||||
# domain: full domain to forward (e.g. "apps.sno.openshift.toal.ca")
|
||||
# value: target IP address
|
||||
#
|
||||
# Example:
|
||||
# opnsense_dns_override_entries:
|
||||
# - hostname: api.sno
|
||||
# domain: openshift.toal.ca
|
||||
# value: 192.168.40.10
|
||||
# type: host
|
||||
# - domain: apps.sno.openshift.toal.ca
|
||||
# value: 192.168.40.10
|
||||
# type: forward
|
||||
opnsense_dns_override_entries: []
|
||||
17
roles/opnsense_dns_override/meta/argument_specs.yml
Normal file
17
roles/opnsense_dns_override/meta/argument_specs.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
argument_specs:
|
||||
main:
|
||||
short_description: Manage OPNsense Unbound DNS overrides
|
||||
description:
|
||||
- Creates Unbound host overrides (A record) and domain forwards
|
||||
in OPNsense via the oxlorg.opnsense collection.
|
||||
- Requires oxlorg.opnsense module_defaults to be set at play level.
|
||||
options:
|
||||
opnsense_dns_override_entries:
|
||||
description: >-
|
||||
List of DNS override entries. Each entry requires C(type) ("host" or "forward"),
|
||||
C(value) (target IP), and either C(hostname)+C(domain) (for host type) or
|
||||
C(domain) (for forward type).
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
16
roles/opnsense_dns_override/meta/main.yml
Normal file
16
roles/opnsense_dns_override/meta/main.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: ptoal
|
||||
description: Manage OPNsense Unbound DNS host overrides and domain forwards
|
||||
license: MIT
|
||||
min_ansible_version: "2.16"
|
||||
platforms:
|
||||
- name: GenericLinux
|
||||
versions:
|
||||
- all
|
||||
galaxy_tags:
|
||||
- opnsense
|
||||
- dns
|
||||
- unbound
|
||||
|
||||
dependencies: []
|
||||
24
roles/opnsense_dns_override/tasks/main.yml
Normal file
24
roles/opnsense_dns_override/tasks/main.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
- name: Create Unbound host overrides
|
||||
oxlorg.opnsense.unbound_host:
|
||||
hostname: "{{ item.hostname }}"
|
||||
domain: "{{ item.domain }}"
|
||||
value: "{{ item.value }}"
|
||||
match_fields:
|
||||
- hostname
|
||||
- domain
|
||||
state: present
|
||||
delegate_to: localhost
|
||||
loop: "{{ opnsense_dns_override_entries | selectattr('type', 'eq', 'host') }}"
|
||||
loop_control:
|
||||
label: "{{ item.hostname }}.{{ item.domain }} -> {{ item.value }}"
|
||||
|
||||
- name: Create Unbound domain forwards
|
||||
oxlorg.opnsense.unbound_forward:
|
||||
domain: "{{ item.domain }}"
|
||||
target: "{{ item.value }}"
|
||||
state: present
|
||||
delegate_to: localhost
|
||||
loop: "{{ opnsense_dns_override_entries | selectattr('type', 'eq', 'forward') }}"
|
||||
loop_control:
|
||||
label: "{{ item.domain }} -> {{ item.value }}"
|
||||
21
roles/proxmox_vm/defaults/main.yml
Normal file
21
roles/proxmox_vm/defaults/main.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
# Proxmox connection
|
||||
# api_host / api_port are derived from the 'proxmox_api' inventory host.
|
||||
proxmox_node: pve1
|
||||
proxmox_api_user: ansible@pam
|
||||
proxmox_api_token_id: ansible
|
||||
proxmox_api_token_secret: "{{ vault_proxmox_token_secret }}"
|
||||
proxmox_validate_certs: false
|
||||
proxmox_storage: local-lvm
|
||||
|
||||
# VM spec
|
||||
sno_vm_name: "sno-{{ ocp_cluster_name }}"
|
||||
sno_vm_id: 0
|
||||
sno_cpu: 8
|
||||
sno_memory_mb: 32768
|
||||
sno_disk_gb: 120
|
||||
sno_pvc_disk_gb: 100
|
||||
sno_vnet: ocp
|
||||
sno_mac: ""
|
||||
sno_storage_vnet: storage
|
||||
sno_storage_mac: ""
|
||||
15
roles/proxmox_vm/meta/main.yml
Normal file
15
roles/proxmox_vm/meta/main.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: ptoal
|
||||
description: Create a Proxmox VM (q35/UEFI) for SNO deployments
|
||||
license: MIT
|
||||
min_ansible_version: "2.16"
|
||||
platforms:
|
||||
- name: GenericLinux
|
||||
versions:
|
||||
- all
|
||||
galaxy_tags:
|
||||
- proxmox
|
||||
- vm
|
||||
|
||||
dependencies: []
|
||||
101
roles/proxmox_vm/tasks/main.yml
Normal file
101
roles/proxmox_vm/tasks/main.yml
Normal file
@@ -0,0 +1,101 @@
|
||||
---
|
||||
# Create a Proxmox VM.
|
||||
# Uses q35 machine type with UEFI (required for SNO / RHCOS).
|
||||
# An empty ide2 CD-ROM slot is created for the agent installer ISO.
|
||||
|
||||
- name: Build net0 string
|
||||
ansible.builtin.set_fact:
|
||||
__sno_deploy_net0: >-
|
||||
virtio{{
|
||||
'=' + sno_mac if sno_mac | length > 0 else ''
|
||||
}},bridge={{ sno_vnet }}
|
||||
|
||||
- name: Build net1 (storage) string
|
||||
ansible.builtin.set_fact:
|
||||
__sno_deploy_net1: >-
|
||||
virtio{{
|
||||
'=' + sno_storage_mac if sno_storage_mac | length > 0 else ''
|
||||
}},bridge={{ sno_storage_vnet }}
|
||||
|
||||
- name: Create SNO VM in Proxmox
|
||||
community.proxmox.proxmox_kvm:
|
||||
api_host: "{{ hostvars['proxmox_api']['ansible_host'] }}"
|
||||
api_user: "{{ proxmox_api_user }}"
|
||||
api_port: "{{ hostvars['proxmox_api']['ansible_port'] }}"
|
||||
api_token_id: "{{ proxmox_api_token_id }}"
|
||||
api_token_secret: "{{ proxmox_api_token_secret }}"
|
||||
validate_certs: "{{ proxmox_validate_certs }}"
|
||||
node: "{{ proxmox_node }}"
|
||||
vmid: "{{ sno_vm_id | default(omit, true) }}"
|
||||
name: "{{ sno_vm_name }}"
|
||||
cores: "{{ sno_cpu }}"
|
||||
memory: "{{ sno_memory_mb }}"
|
||||
cpu: host
|
||||
numa_enabled: true
|
||||
machine: q35
|
||||
bios: ovmf
|
||||
efidisk0:
|
||||
storage: "{{ proxmox_storage }}"
|
||||
format: raw
|
||||
efitype: 4m
|
||||
pre_enrolled_keys: false
|
||||
scsi:
|
||||
scsi0: "{{ proxmox_storage }}:{{ sno_disk_gb }},format=raw,iothread=1,cache=writeback"
|
||||
scsi1: "{{ proxmox_storage }}:{{ sno_pvc_disk_gb }},format=raw,iothread=1,cache=writeback"
|
||||
scsihw: virtio-scsi-single
|
||||
ide:
|
||||
ide2: none,media=cdrom
|
||||
net:
|
||||
net0: "{{ __sno_deploy_net0 }}"
|
||||
net1: "{{ __sno_deploy_net1 }}"
|
||||
boot: "order=scsi0;ide2"
|
||||
onboot: true
|
||||
state: present
|
||||
register: __proxmox_vm_result
|
||||
|
||||
- name: Retrieve VM info
|
||||
community.proxmox.proxmox_vm_info:
|
||||
api_host: "{{ hostvars['proxmox_api']['ansible_host'] }}"
|
||||
api_user: "{{ proxmox_api_user }}"
|
||||
api_port: "{{ hostvars['proxmox_api']['ansible_port'] }}"
|
||||
api_token_id: "{{ proxmox_api_token_id }}"
|
||||
api_token_secret: "{{ proxmox_api_token_secret }}"
|
||||
validate_certs: "{{ proxmox_validate_certs }}"
|
||||
node: "{{ proxmox_node }}"
|
||||
name: "{{ sno_vm_name }}"
|
||||
type: qemu
|
||||
config: current
|
||||
register: __proxmox_vm_info
|
||||
retries: 5
|
||||
|
||||
- name: Set VM ID fact for subsequent plays
|
||||
ansible.builtin.set_fact:
|
||||
sno_vm_id: "{{ __proxmox_vm_info.proxmox_vms[0].vmid }}"
|
||||
cacheable: true
|
||||
|
||||
- name: Extract MAC address from VM config
|
||||
ansible.builtin.set_fact:
|
||||
sno_mac: >-
|
||||
{{ __proxmox_vm_info.proxmox_vms[0].config.net0
|
||||
| regex_search('([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})', '\1')
|
||||
| first }}
|
||||
cacheable: true
|
||||
when: sno_mac | length == 0
|
||||
|
||||
- name: Extract storage MAC address from VM config
|
||||
ansible.builtin.set_fact:
|
||||
sno_storage_mac: >-
|
||||
{{ __proxmox_vm_info.proxmox_vms[0].config.net1
|
||||
| regex_search('([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})', '\1')
|
||||
| first }}
|
||||
cacheable: true
|
||||
when: sno_storage_mac | length == 0
|
||||
|
||||
- name: Display VM details
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "VM Name : {{ sno_vm_name }}"
|
||||
- "VM ID : {{ sno_vm_id }}"
|
||||
- "MAC (net0) : {{ sno_mac }}"
|
||||
- "MAC (net1) : {{ sno_storage_mac }}"
|
||||
verbosity: 1
|
||||
111
roles/rhoai_maas/defaults/main.yml
Normal file
111
roles/rhoai_maas/defaults/main.yml
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
# --- OLM operators ---
|
||||
# Installed in order. RHOAI is pinned to stable-3.5: the MaaS DSC layout
|
||||
# (aigateway.modelsAsAService) changed between 3.4 and 3.5, so don't float on stable-3.x.
|
||||
# RHCL must be 1.4.x (Kuadrant >= 1.4.2 for AuthPolicy spec.defaults.rules).
|
||||
# cert-manager is a prerequisite but already installed by deploy_openshift.yml.
|
||||
rhoai_maas_operators:
|
||||
- name: rhcl-operator
|
||||
namespace: openshift-operators
|
||||
channel: stable-1.4
|
||||
- name: leader-worker-set
|
||||
namespace: openshift-lws-operator
|
||||
channel: stable-v1.0
|
||||
operator_group_target_namespaces:
|
||||
- openshift-lws-operator
|
||||
- name: rhods-operator
|
||||
namespace: redhat-ods-operator
|
||||
channel: stable-3.5
|
||||
namespace_labels:
|
||||
openshift.io/cluster-monitoring: "true"
|
||||
rhoai_maas_wait_timeout: 900
|
||||
|
||||
# --- Gateway ---
|
||||
# Empty = maas.<cluster ingress domain>.
|
||||
rhoai_maas_hostname: ""
|
||||
# The gateway gets its own single-host certificate. Don't use the *.apps wildcard:
|
||||
# browsers would reuse the MaaS HTTP/2 connection for every other *.apps host
|
||||
# (console, dashboards), and the MaaS gateway answers those with 404.
|
||||
rhoai_maas_gateway_cert_secret: maas-gateway-tls
|
||||
# cert-manager ClusterIssuer for the gateway certificate. Empty = use the existing
|
||||
# rhoai_maas_gateway_cert_secret as-is and don't create a Certificate.
|
||||
rhoai_maas_gateway_cert_issuer: letsencrypt-production
|
||||
# No cloud LB on this cluster: run the gateway Service as ClusterIP and expose it
|
||||
# through a passthrough Route on the default router. Set to LoadBalancer with MetalLB.
|
||||
rhoai_maas_gateway_service_type: ClusterIP
|
||||
rhoai_maas_gateway_create_route: true
|
||||
|
||||
# --- MaaS database ---
|
||||
# PostgreSQL is provisioned by the cloudnativepg_operator role (see the playbook);
|
||||
# this secret holds its credentials (type kubernetes.io/basic-auth).
|
||||
rhoai_maas_db_secret_name: maas-postgres-secret
|
||||
rhoai_maas_db_secret_namespace: rhoai-maas-db
|
||||
rhoai_maas_db_sslmode: require
|
||||
rhoai_maas_infra_namespace: redhat-ai-gateway-infra
|
||||
|
||||
# --- DataScienceCluster ---
|
||||
# Minimal component set for MaaS. Anything not listed here is set to Removed.
|
||||
rhoai_maas_dsc_name: default-dsc
|
||||
rhoai_maas_dsc_managed_components:
|
||||
- dashboard
|
||||
- kserve
|
||||
- aigateway
|
||||
- ogx
|
||||
rhoai_maas_dsc_removed_components:
|
||||
- aipipelines
|
||||
- feastoperator
|
||||
- kueue
|
||||
- mlflowoperator
|
||||
- modelregistry
|
||||
- ray
|
||||
- trainer
|
||||
- trainingoperator
|
||||
- trustyai
|
||||
- workbenches
|
||||
|
||||
# --- Models (ExternalModel fronting in-cluster OpenAI-compatible servers) ---
|
||||
# The api_key_secret must already exist in the model namespace with key `api-key`;
|
||||
# the role only labels it for MaaS. Names must not collide with existing Services
|
||||
# in the namespace (the controller creates a Service per provider/model).
|
||||
rhoai_maas_external_models:
|
||||
- name: qwen35-9b-coder
|
||||
namespace: llm-coding-assistant
|
||||
display_name: Qwen3.5 9B Coder (llama.cpp)
|
||||
description: Qwen3.5-9B Q5_K_M served by llama.cpp on the desktop GPU node
|
||||
provider_name: llama-coder-provider
|
||||
provider_type: openai
|
||||
endpoint: llama-coder.llm-coding-assistant.svc.cluster.local
|
||||
port: 8080
|
||||
tls: false
|
||||
api_key_secret: llama-coder-api-key
|
||||
# llama-server serves a single model and ignores the request's model field.
|
||||
target_model: Qwen3.5-9B-Q5_K_M.gguf
|
||||
api_format: openai-chat
|
||||
path: /v1/chat/completions
|
||||
|
||||
# --- Access policy and quota (CR spec shapes, models-as-a-service namespace) ---
|
||||
rhoai_maas_auth_policies:
|
||||
- name: llama-coder-access
|
||||
display_name: llama-coder access
|
||||
description: All authenticated users can call the llama-coder model
|
||||
groups:
|
||||
- system:authenticated
|
||||
users: []
|
||||
model_refs:
|
||||
- name: qwen35-9b-coder
|
||||
namespace: llm-coding-assistant
|
||||
|
||||
rhoai_maas_subscriptions:
|
||||
- name: llama-coder-default
|
||||
display_name: llama-coder default
|
||||
description: 200k tokens/hour per user for all authenticated users
|
||||
priority: 10
|
||||
groups:
|
||||
- system:authenticated
|
||||
users: []
|
||||
model_refs:
|
||||
- name: qwen35-9b-coder
|
||||
namespace: llm-coding-assistant
|
||||
tokenRateLimits:
|
||||
- limit: 200000
|
||||
window: 1h
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user