--- # ansible/playbooks/day1_configure_netbox_catalog.yml # # Creates the tag taxonomy and custom fields in NetBox for service catalog # documentation. Run once (idempotent — uses name-based checks). # # Usage (from ansible/ directory): # ansible-playbook playbooks/day1_configure_netbox_catalog.yml # # Requires: # - vault_netbox_token in Ansible Vault # - netbox reachable at http://fire-station.local.mk-labs.cloud - name: Configure NetBox service catalog taxonomy hosts: localhost gather_facts: false vars_files: - "{{ playbook_dir }}/../group_vars/all/vault" vars: netbox_url: "http://fire-station.local.mk-labs.cloud" netbox_token: "{{ vault_netbox_token }}" netbox_api: "{{ netbox_url }}/api" headers: Authorization: "Token {{ netbox_token }}" Content-Type: "application/json" Accept: "application/json" # ── Tag definitions ─────────────────────────────────────────────────── tags: # Infrastructure type - name: k8s slug: k8s color: "2196f3" # blue description: "Workload running in the fastpass Kubernetes cluster" - name: vm slug: vm color: "4caf50" # green description: "Traditional VM or LXC on Proxmox" # Service tier - name: platform slug: platform color: "9c27b0" # purple description: "Platform/infrastructure service (not user-facing)" - name: application slug: application color: "ff9800" # orange description: "User-facing application workload" # Service categories - name: monitoring slug: monitoring color: "607d8b" # grey description: "Metrics, logging, alerting" - name: auth slug: auth color: "607d8b" description: "Authentication and SSO" - name: gitops slug: gitops color: "607d8b" description: "GitOps and CI/CD" - name: dashboard slug: dashboard color: "607d8b" description: "Dashboard and portal services" - name: storage slug: storage color: "607d8b" description: "Storage and file services" - name: dns slug: dns color: "607d8b" description: "DNS and name resolution" - name: automation slug: automation color: "607d8b" description: "Automation and orchestration" - name: networking slug: networking color: "607d8b" description: "Network infrastructure services" - name: inference slug: inference color: "607d8b" description: "AI/ML inference workloads" # ── Custom field definitions ────────────────────────────────────────── # object_types use app_label.model format custom_fields: - name: hostnames label: Hostnames type: longtext object_types: - ipam.ipaddress - virtualization.virtualmachine description: "All DNS names that resolve to this service (comma-separated)" ui_visible: always ui_editable: yes - name: namespace label: Namespace type: text object_types: - ipam.ipaddress - virtualization.virtualmachine description: "Kubernetes namespace (blank for VM-based services)" ui_visible: always ui_editable: yes - name: managed_by label: Managed By type: select object_types: - ipam.ipaddress - virtualization.virtualmachine description: "How this service is managed" choices: - ArgoCD - Ansible - Manual ui_visible: always ui_editable: yes - name: thematic_name label: Thematic Name type: text object_types: - ipam.ipaddress - virtualization.virtualmachine description: "Disney/Magic Kingdom thematic hostname for this service" ui_visible: always ui_editable: yes tasks: # ── Tags ───────────────────────────────────────────────────────────── - name: Fetch existing tags ansible.builtin.uri: url: "{{ netbox_api }}/extras/tags/?limit=200" method: GET headers: "{{ headers }}" return_content: true status_code: 200 register: existing_tags_response - name: Set existing tag slugs fact ansible.builtin.set_fact: existing_tag_slugs: "{{ existing_tags_response.json.results | map(attribute='slug') | list }}" - name: Create tags ansible.builtin.uri: url: "{{ netbox_api }}/extras/tags/" method: POST headers: "{{ headers }}" body_format: json body: name: "{{ item.name }}" slug: "{{ item.slug }}" color: "{{ item.color }}" description: "{{ item.description }}" status_code: 201 loop: "{{ tags }}" when: item.slug not in existing_tag_slugs register: tag_creation changed_when: tag_creation.status == 201 # ── Custom Fields ───────────────────────────────────────────────────── - name: Fetch existing custom fields ansible.builtin.uri: url: "{{ netbox_api }}/extras/custom-fields/?limit=200" method: GET headers: "{{ headers }}" return_content: true status_code: 200 register: existing_cf_response - name: Set existing custom field names fact ansible.builtin.set_fact: existing_cf_names: "{{ existing_cf_response.json.results | map(attribute='name') | list }}" - name: Create custom field — hostnames ansible.builtin.uri: url: "{{ netbox_api }}/extras/custom-fields/" method: POST headers: "{{ headers }}" body_format: json body: name: hostnames label: Hostnames type: longtext object_types: "{{ custom_fields[0].object_types }}" description: "{{ custom_fields[0].description }}" ui_visible: always ui_editable: yes status_code: 201 when: "'hostnames' not in existing_cf_names" register: cf_hostnames changed_when: cf_hostnames.status == 201 - name: Create custom field — namespace ansible.builtin.uri: url: "{{ netbox_api }}/extras/custom-fields/" method: POST headers: "{{ headers }}" body_format: json body: name: namespace label: Namespace type: text object_types: "{{ custom_fields[1].object_types }}" description: "{{ custom_fields[1].description }}" ui_visible: always ui_editable: yes status_code: 201 when: "'namespace' not in existing_cf_names" register: cf_namespace changed_when: cf_namespace.status == 201 - name: Create choice set for managed_by field ansible.builtin.uri: url: "{{ netbox_api }}/extras/custom-field-choice-sets/" method: POST headers: "{{ headers }}" body_format: json body: name: managed-by-choices extra_choices: - - ArgoCD - ArgoCD - - Ansible - Ansible - - Manual - Manual status_code: [201, 400] register: choice_set changed_when: choice_set.status == 201 - name: Fetch choice set ID ansible.builtin.uri: url: "{{ netbox_api }}/extras/custom-field-choice-sets/?name=managed-by-choices" method: GET headers: "{{ headers }}" return_content: true status_code: 200 register: choice_set_response - name: Create custom field — managed_by ansible.builtin.uri: url: "{{ netbox_api }}/extras/custom-fields/" method: POST headers: "{{ headers }}" body_format: json body: name: managed_by label: Managed By type: select object_types: "{{ custom_fields[2].object_types }}" description: "{{ custom_fields[2].description }}" choice_set: "{{ choice_set_response.json.results[0].id }}" ui_visible: always ui_editable: yes status_code: 201 when: "'managed_by' not in existing_cf_names" register: cf_managed_by changed_when: cf_managed_by.status == 201 - name: Create custom field — thematic_name ansible.builtin.uri: url: "{{ netbox_api }}/extras/custom-fields/" method: POST headers: "{{ headers }}" body_format: json body: name: thematic_name label: Thematic Name type: text object_types: "{{ custom_fields[3].object_types }}" description: "{{ custom_fields[3].description }}" ui_visible: always ui_editable: yes status_code: 201 when: "'thematic_name' not in existing_cf_names" register: cf_thematic_name changed_when: cf_thematic_name.status == 201 # ── Provisioning Pipeline Fields ────────────────────────────────────── # These fields drive the NetBox → n8n → Terraform pipeline. # proxmox_datastore already exists — PATCH it to add utilidor choice. # data_disk_enabled and data_disk_size_gb are new POSTs. - name: Fetch proxmox_datastore field ID ansible.builtin.uri: url: "{{ netbox_api }}/extras/custom-fields/?name=proxmox_datastore" method: GET headers: "{{ headers }}" return_content: true status_code: 200 register: proxmox_datastore_cf_response - name: Fetch proxmox-datastore-choices choice set ID ansible.builtin.uri: url: "{{ netbox_api }}/extras/custom-field-choice-sets/?name=proxmox-datastore-choices" method: GET headers: "{{ headers }}" return_content: true status_code: 200 register: datastore_choice_set_response - name: Create proxmox-datastore-choices choice set if missing ansible.builtin.uri: url: "{{ netbox_api }}/extras/custom-field-choice-sets/" method: POST headers: "{{ headers }}" body_format: json body: name: proxmox-datastore-choices extra_choices: - - liberty-tree - liberty-tree - - utilidor - utilidor status_code: [201, 400] register: datastore_choice_set_create changed_when: datastore_choice_set_create.status == 201 when: datastore_choice_set_response.json.count == 0 - name: Re-fetch proxmox-datastore-choices choice set ID after possible creation ansible.builtin.uri: url: "{{ netbox_api }}/extras/custom-field-choice-sets/?name=proxmox-datastore-choices" method: GET headers: "{{ headers }}" return_content: true status_code: 200 register: datastore_choice_set_response - name: Patch proxmox_datastore field to use choice set with utilidor ansible.builtin.uri: url: "{{ netbox_api }}/extras/custom-fields/{{ proxmox_datastore_cf_response.json.results[0].id }}/" method: PATCH headers: "{{ headers }}" body_format: json body: choice_set: "{{ datastore_choice_set_response.json.results[0].id }}" status_code: 200 when: proxmox_datastore_cf_response.json.count > 0 register: cf_datastore_patch changed_when: cf_datastore_patch.status == 200 - name: Create custom field — data_disk_enabled ansible.builtin.uri: url: "{{ netbox_api }}/extras/custom-fields/" method: POST headers: "{{ headers }}" body_format: json body: name: data_disk_enabled label: Data Disk Enabled type: boolean object_types: - virtualization.virtualmachine description: "When true, Terraform provisions a second disk for application data storage." default: false ui_visible: always ui_editable: yes status_code: 201 when: "'data_disk_enabled' not in existing_cf_names" register: cf_data_disk_enabled changed_when: cf_data_disk_enabled.status == 201 - name: Create custom field — data_disk_size_gb ansible.builtin.uri: url: "{{ netbox_api }}/extras/custom-fields/" method: POST headers: "{{ headers }}" body_format: json body: name: data_disk_size_gb label: Data Disk Size (GB) type: integer object_types: - virtualization.virtualmachine description: "Size in GB for the optional second data disk. Only used when data_disk_enabled is true." ui_visible: always ui_editable: yes status_code: 201 when: "'data_disk_size_gb' not in existing_cf_names" register: cf_data_disk_size changed_when: cf_data_disk_size.status == 201 # ── Summary ─────────────────────────────────────────────────────────── - name: Summary ansible.builtin.debug: msg: - "Tags created: {{ tag_creation.results | selectattr('status', 'equalto', 201) | list | length }}" - "Tags skipped (already exist): {{ tag_creation.results | selectattr('skipped', 'defined') | list | length }}" - "Custom fields configured: hostnames, namespace, managed_by, thematic_name, proxmox_datastore (patched), data_disk_enabled, data_disk_size_gb"