Reattempting Linode dynamic inventories.
This commit is contained in:
@@ -1,9 +1,96 @@
|
|||||||
---
|
---
|
||||||
- name: Generate Linode inventory file
|
- name: Update Linode Dynamic Inventory
|
||||||
|
hosts: localhost
|
||||||
|
gather_facts: true
|
||||||
|
connection: local
|
||||||
|
|
||||||
|
vars:
|
||||||
|
# Override these variables as needed
|
||||||
|
linode_api_token: "{{ lookup('env', 'LINODE_API_TOKEN') }}"
|
||||||
|
linode_inventory_output_dir: "/tmp/linode_inventory"
|
||||||
|
inventory_format: "json" # or "ini"
|
||||||
|
awx_integration: true
|
||||||
|
cleanup_temp_files: false
|
||||||
|
|
||||||
|
# Optional filters
|
||||||
|
include_only_running: false
|
||||||
|
specific_regions: [] # e.g., ['us-east', 'us-west']
|
||||||
|
specific_tags: [] # e.g., ['production', 'web']
|
||||||
|
|
||||||
|
pre_tasks:
|
||||||
|
- name: Check for Linode API token
|
||||||
|
ansible.builtin.fail:
|
||||||
|
msg: "LINODE_API_TOKEN environment variable or linode_api_token variable must be set"
|
||||||
|
when:
|
||||||
|
- linode_api_token is undefined or linode_api_token == ""
|
||||||
|
- lookup('env', 'LINODE_API_TOKEN') == ""
|
||||||
|
|
||||||
|
- name: Display configuration
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: |
|
||||||
|
Linode Inventory Configuration:
|
||||||
|
Output directory: {{ linode_inventory_output_dir }}
|
||||||
|
Output format: {{ inventory_format }}
|
||||||
|
AWX integration: {{ awx_integration }}
|
||||||
|
Include only running: {{ include_only_running }}
|
||||||
|
|
||||||
|
roles:
|
||||||
|
- role: linode_inventory
|
||||||
|
vars:
|
||||||
|
linode_api_token: "{{ linode_api_token }}"
|
||||||
|
|
||||||
|
post_tasks:
|
||||||
|
- name: Display next steps
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: |
|
||||||
|
Inventory update complete!
|
||||||
|
|
||||||
|
Next steps for AWX integration:
|
||||||
|
1. Copy the inventory script to your SCM repository
|
||||||
|
2. Create a custom inventory source in AWX
|
||||||
|
3. Point it to the linode_inventory.py script
|
||||||
|
4. Set up the Linode API credential
|
||||||
|
|
||||||
|
Files created:
|
||||||
|
- JSON inventory: {{ linode_inventory_output_dir }}/{{ linode_inventory_output_file }}
|
||||||
|
{% if inventory_format == "ini" %}
|
||||||
|
- INI inventory: {{ linode_inventory_output_dir }}/linode_static_inventory.ini
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
# Optional: Run against discovered Linode hosts
|
||||||
|
- name: Example task against discovered Linode hosts
|
||||||
hosts: localhost
|
hosts: localhost
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
roles:
|
tasks:
|
||||||
- roles/inventory/linode
|
- name: Load dynamic inventory
|
||||||
vars:
|
ansible.builtin.include_vars:
|
||||||
linode_token: "{{ lookup('env', 'LINODE_TOKEN') }}"
|
file: "{{ linode_inventory_output_dir | default('/tmp/linode_inventory') }}/{{ linode_inventory_output_file | default('linode_inventory.json') }}"
|
||||||
|
name: dynamic_inventory
|
||||||
|
|
||||||
|
- name: Add discovered hosts to in-memory inventory
|
||||||
|
ansible.builtin.add_host:
|
||||||
|
name: "{{ item.key }}"
|
||||||
|
groups: "{{ group_names | default(['discovered_linodes']) }}"
|
||||||
|
ansible_host: "{{ item.value.ansible_host }}"
|
||||||
|
linode_id: "{{ item.value.linode_id }}"
|
||||||
|
linode_region: "{{ item.value.linode_region }}"
|
||||||
|
linode_type: "{{ item.value.linode_type }}"
|
||||||
|
linode_status: "{{ item.value.linode_status }}"
|
||||||
|
loop: "{{ dynamic_inventory._meta.hostvars | dict2items }}"
|
||||||
|
when: item.value.linode_status == "running"
|
||||||
|
|
||||||
|
- name: Test connection to discovered Linode hosts
|
||||||
|
hosts: discovered_linodes
|
||||||
|
gather_facts: false
|
||||||
|
vars:
|
||||||
|
ansible_user: root
|
||||||
|
ansible_ssh_common_args: '-o StrictHostKeyChecking=no -o ConnectTimeout=10'
|
||||||
|
tasks:
|
||||||
|
- name: Test connectivity
|
||||||
|
ansible.builtin.ping:
|
||||||
|
register: ping_result
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Display connectivity status
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "{{ inventory_hostname }} ({{ ansible_host }}): {{ 'REACHABLE' if ping_result is succeeded else 'UNREACHABLE' }}"
|
||||||
|
26
roles/inventory/linode/defaults/main.yml
Normal file
26
roles/inventory/linode/defaults/main.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
# Default variables for linode_inventory role
|
||||||
|
|
||||||
|
# Linode API settings
|
||||||
|
linode_api_url: "https://api.linode.com/v4"
|
||||||
|
linode_inventory_output_dir: "/tmp"
|
||||||
|
linode_inventory_output_file: "linode_inventory.json"
|
||||||
|
|
||||||
|
# Inventory grouping options
|
||||||
|
create_region_groups: true
|
||||||
|
create_type_groups: true
|
||||||
|
create_status_groups: true
|
||||||
|
create_tag_groups: true
|
||||||
|
|
||||||
|
# Default groups to create
|
||||||
|
default_groups:
|
||||||
|
- all
|
||||||
|
- ungrouped
|
||||||
|
|
||||||
|
# Filter options
|
||||||
|
include_only_running: false
|
||||||
|
specific_regions: []
|
||||||
|
specific_tags: []
|
||||||
|
|
||||||
|
# Output format
|
||||||
|
inventory_format: "json" # json or ini
|
149
roles/inventory/linode/files/linode_inventory.py
Normal file
149
roles/inventory/linode/files/linode_inventory.py
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
|
||||||
|
class LinodeInventory:
|
||||||
|
def __init__(self):
|
||||||
|
self.api_token = os.environ.get('LINODE_API_TOKEN')
|
||||||
|
if not self.api_token:
|
||||||
|
raise ValueError("LINODE_API_TOKEN environment variable is required")
|
||||||
|
|
||||||
|
self.base_url = "https://api.linode.com/v4"
|
||||||
|
self.headers = {
|
||||||
|
"Authorization": f"Bearer {self.api_token}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_instances(self):
|
||||||
|
"""Fetch all Linode instances"""
|
||||||
|
url = f"{self.base_url}/linode/instances"
|
||||||
|
response = requests.get(url, headers=self.headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()['data']
|
||||||
|
|
||||||
|
def generate_inventory(self):
|
||||||
|
"""Generate Ansible inventory from Linode instances"""
|
||||||
|
inventory = {
|
||||||
|
'_meta': {
|
||||||
|
'hostvars': {}
|
||||||
|
},
|
||||||
|
'all': {
|
||||||
|
'children': ['ungrouped']
|
||||||
|
},
|
||||||
|
'ungrouped': {
|
||||||
|
'hosts': []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Group definitions
|
||||||
|
regions = {}
|
||||||
|
types = {}
|
||||||
|
statuses = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
instances = self.get_instances()
|
||||||
|
|
||||||
|
for instance in instances:
|
||||||
|
# Use Linode label as hostname (this is what you wanted!)
|
||||||
|
hostname = instance['label']
|
||||||
|
|
||||||
|
# Get primary IPv4 address
|
||||||
|
ipv4_addresses = instance.get('ipv4', [])
|
||||||
|
primary_ip = ipv4_addresses[0] if ipv4_addresses else None
|
||||||
|
|
||||||
|
# Add to ungrouped hosts
|
||||||
|
inventory['ungrouped']['hosts'].append(hostname)
|
||||||
|
|
||||||
|
# Host variables
|
||||||
|
inventory['_meta']['hostvars'][hostname] = {
|
||||||
|
'ansible_host': primary_ip,
|
||||||
|
'linode_id': instance['id'],
|
||||||
|
'linode_label': instance['label'],
|
||||||
|
'linode_region': instance['region'],
|
||||||
|
'linode_type': instance['type'],
|
||||||
|
'linode_status': instance['status'],
|
||||||
|
'linode_ipv4': instance.get('ipv4', []),
|
||||||
|
'linode_ipv6': instance.get('ipv6'),
|
||||||
|
'linode_tags': instance.get('tags', []),
|
||||||
|
'linode_specs': instance.get('specs', {}),
|
||||||
|
'linode_hypervisor': instance.get('hypervisor'),
|
||||||
|
'linode_created': instance.get('created'),
|
||||||
|
'linode_updated': instance.get('updated')
|
||||||
|
}
|
||||||
|
|
||||||
|
# Group by region
|
||||||
|
region_group = f"region_{instance['region'].replace('-', '_')}"
|
||||||
|
if region_group not in regions:
|
||||||
|
regions[region_group] = {'hosts': []}
|
||||||
|
regions[region_group]['hosts'].append(hostname)
|
||||||
|
|
||||||
|
# Group by instance type
|
||||||
|
type_group = f"type_{instance['type'].replace('-', '_').replace('.', '_')}"
|
||||||
|
if type_group not in types:
|
||||||
|
types[type_group] = {'hosts': []}
|
||||||
|
types[type_group]['hosts'].append(hostname)
|
||||||
|
|
||||||
|
# Group by status
|
||||||
|
status_group = f"status_{instance['status']}"
|
||||||
|
if status_group not in statuses:
|
||||||
|
statuses[status_group] = {'hosts': []}
|
||||||
|
statuses[status_group]['hosts'].append(hostname)
|
||||||
|
|
||||||
|
# Group by tags
|
||||||
|
for tag in instance.get('tags', []):
|
||||||
|
tag_group = f"tag_{tag.replace('-', '_').replace(' ', '_')}"
|
||||||
|
if tag_group not in inventory:
|
||||||
|
inventory[tag_group] = {'hosts': []}
|
||||||
|
inventory[tag_group]['hosts'].append(hostname)
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"Error fetching Linode instances: {e}", file=sys.stderr)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Add all groups to inventory
|
||||||
|
inventory.update(regions)
|
||||||
|
inventory.update(types)
|
||||||
|
inventory.update(statuses)
|
||||||
|
|
||||||
|
# Add group children to 'all'
|
||||||
|
all_groups = list(regions.keys()) + list(types.keys()) + list(statuses.keys())
|
||||||
|
tag_groups = [k for k in inventory.keys() if k.startswith('tag_')]
|
||||||
|
all_groups.extend(tag_groups)
|
||||||
|
|
||||||
|
if all_groups:
|
||||||
|
inventory['all']['children'].extend(all_groups)
|
||||||
|
|
||||||
|
return inventory
|
||||||
|
|
||||||
|
def get_host_vars(self, hostname):
|
||||||
|
"""Get variables for a specific host"""
|
||||||
|
inventory = self.generate_inventory()
|
||||||
|
return inventory['_meta']['hostvars'].get(hostname, {})
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = ArgumentParser(description='Linode Dynamic Inventory')
|
||||||
|
parser.add_argument('--list', action='store_true', help='List all hosts')
|
||||||
|
parser.add_argument('--host', help='Get variables for specific host')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
inventory = LinodeInventory()
|
||||||
|
|
||||||
|
if args.list:
|
||||||
|
print(json.dumps(inventory.generate_inventory(), indent=2))
|
||||||
|
elif args.host:
|
||||||
|
print(json.dumps(inventory.get_host_vars(args.host), indent=2))
|
||||||
|
else:
|
||||||
|
parser.print_help()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@@ -1,39 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
|
||||||
import os
|
|
||||||
import requests
|
|
||||||
|
|
||||||
def main():
|
|
||||||
module = AnsibleModule(argument_spec={})
|
|
||||||
|
|
||||||
token = os.getenv("LINODE_TOKEN")
|
|
||||||
if not token:
|
|
||||||
module.fail_json(msg="LINODE_TOKEN is not set")
|
|
||||||
|
|
||||||
response = requests.get("https://api.linode.com/v4/linode/instances", headers={
|
|
||||||
"Authorization": f"Bearer {token}",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
})
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
module.fail_json(msg=f"Linode API error: {response.status_code}", details=response.text)
|
|
||||||
|
|
||||||
linodes = response.json().get("data", [])
|
|
||||||
results = {
|
|
||||||
"changed": False,
|
|
||||||
"linodes": [
|
|
||||||
{
|
|
||||||
"name": l["label"],
|
|
||||||
"ip": l["ipv4"][0] if l["ipv4"] else None,
|
|
||||||
"region": l["region"],
|
|
||||||
"tags": l["tags"],
|
|
||||||
"type": l["type"]
|
|
||||||
} for l in linodes
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exit_json(**results)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
30
roles/inventory/linode/meta/main.yml
Normal file
30
roles/inventory/linode/meta/main.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
galaxy_info:
|
||||||
|
author: Kevin M Thompson
|
||||||
|
description: Ansible role for Linode dynamic inventory management
|
||||||
|
company: Ewnix
|
||||||
|
license: MIT
|
||||||
|
min_ansible_version: 2.9
|
||||||
|
platforms:
|
||||||
|
- name: EL
|
||||||
|
versions:
|
||||||
|
- 7
|
||||||
|
- 8
|
||||||
|
- 9
|
||||||
|
- name: Ubuntu
|
||||||
|
versions:
|
||||||
|
- 18.04
|
||||||
|
- 20.04
|
||||||
|
- 22.04
|
||||||
|
- name: Debian
|
||||||
|
versions:
|
||||||
|
- 10
|
||||||
|
- 11
|
||||||
|
galaxy_tags:
|
||||||
|
- linode
|
||||||
|
- inventory
|
||||||
|
- dynamic
|
||||||
|
- cloud
|
||||||
|
- automation
|
||||||
|
|
||||||
|
dependencies: []
|
@@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Get Linode inventory using local module
|
|
||||||
linode_inventory:
|
|
||||||
register: linode_result
|
|
||||||
|
|
||||||
- name: Write inventory to file
|
|
||||||
copy:
|
|
||||||
dest: /tmp/linode_inventory.json
|
|
||||||
content: "{{ linode_result | to_nice_json }}"
|
|
||||||
mode: '0644'
|
|
||||||
|
|
97
roles/inventory/linode/tasks/main.yml
Normal file
97
roles/inventory/linode/tasks/main.yml
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
---
|
||||||
|
# Main tasks for linode_inventory role
|
||||||
|
|
||||||
|
- name: Validate required variables
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- linode_api_token is defined
|
||||||
|
- linode_api_token | length > 0
|
||||||
|
fail_msg: "linode_api_token must be defined and not empty"
|
||||||
|
quiet: true
|
||||||
|
|
||||||
|
- name: Ensure output directory exists
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ linode_inventory_output_dir }}"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
- name: Check if Python requests module is available
|
||||||
|
ansible.builtin.command: python3 -c "import requests"
|
||||||
|
register: python_requests_check
|
||||||
|
failed_when: false
|
||||||
|
changed_when: false
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
- name: Install Python requests if not available
|
||||||
|
ansible.builtin.pip:
|
||||||
|
name: requests
|
||||||
|
state: present
|
||||||
|
when: python_requests_check.rc != 0
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
- name: Copy Linode inventory script
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: linode_inventory.py
|
||||||
|
dest: "{{ linode_inventory_output_dir }}/linode_inventory.py"
|
||||||
|
mode: '0755'
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
- name: Execute Linode inventory script
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: python3 {{ linode_inventory_output_dir }}/linode_inventory.py --list
|
||||||
|
environment:
|
||||||
|
LINODE_API_TOKEN: "{{ linode_api_token }}"
|
||||||
|
register: linode_inventory_result
|
||||||
|
delegate_to: localhost
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Parse inventory JSON
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
linode_inventory_data: "{{ linode_inventory_result.stdout | from_json }}"
|
||||||
|
|
||||||
|
- name: Save inventory to file
|
||||||
|
ansible.builtin.copy:
|
||||||
|
content: "{{ linode_inventory_data | to_nice_json }}"
|
||||||
|
dest: "{{ temp_inventory_path }}"
|
||||||
|
mode: '0644'
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
- name: Display inventory summary
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: |
|
||||||
|
Linode Dynamic Inventory Summary:
|
||||||
|
Total hosts discovered: {{ linode_inventory_data._meta.hostvars | length }}
|
||||||
|
Groups created: {{ linode_inventory_data.keys() | reject('equalto', '_meta') | list | length }}
|
||||||
|
Inventory saved to: {{ temp_inventory_path }}
|
||||||
|
|
||||||
|
- name: Show discovered hosts
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "Host: {{ item.key }} ({{ item.value.ansible_host }}) - Region: {{ item.value.linode_region }} - Status: {{ item.value.linode_status }}"
|
||||||
|
loop: "{{ linode_inventory_data._meta.hostvars | dict2items }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.key }}"
|
||||||
|
|
||||||
|
- name: Create static inventory file (optional)
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: inventory.ini.j2
|
||||||
|
dest: "{{ linode_inventory_output_dir }}/linode_static_inventory.ini"
|
||||||
|
mode: '0644'
|
||||||
|
when: inventory_format == "ini"
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
# AWX/Tower specific tasks
|
||||||
|
- name: Create inventory update script for AWX
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: awx_inventory_update.sh.j2
|
||||||
|
dest: "{{ linode_inventory_output_dir }}/awx_inventory_update.sh"
|
||||||
|
mode: '0755'
|
||||||
|
delegate_to: localhost
|
||||||
|
when: awx_integration | default(false)
|
||||||
|
|
||||||
|
- name: Clean up temporary script
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ linode_inventory_output_dir }}/linode_inventory.py"
|
||||||
|
state: absent
|
||||||
|
delegate_to: localhost
|
||||||
|
when: cleanup_temp_files | default(true)
|
22
roles/inventory/linode/templates/inventory.ini.j2
Normal file
22
roles/inventory/linode/templates/inventory.ini.j2
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Generated Linode Inventory
|
||||||
|
# Generated on: {{ ansible_date_time.iso8601 }}
|
||||||
|
|
||||||
|
{% for host_name, host_vars in linode_inventory_data._meta.hostvars.items() %}
|
||||||
|
{{ host_name }} ansible_host={{ host_vars.ansible_host }} linode_id={{ host_vars.linode_id }} linode_region={{ host_vars.linode_region }} linode_type={{ host_vars.linode_type }} linode_status={{ host_vars.linode_status }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% for group_name, group_data in linode_inventory_data.items() %}
|
||||||
|
{% if group_name != '_meta' and group_name != 'all' %}
|
||||||
|
[{{ group_name }}]
|
||||||
|
{% if group_data.hosts is defined %}
|
||||||
|
{% for host in group_data.hosts %}
|
||||||
|
{{ host }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
[all:vars]
|
||||||
|
ansible_user=root
|
||||||
|
ansible_ssh_common_args='-o StrictHostKeyChecking=no'
|
14
roles/inventory/linode/vars/main.yml
Normal file
14
roles/inventory/linode/vars/main.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
# Internal variables for linode_inventory role
|
||||||
|
|
||||||
|
linode_inventory_script_path: "{{ role_path }}/files/linode_inventory.py"
|
||||||
|
temp_inventory_path: "{{ linode_inventory_output_dir }}/{{ linode_inventory_output_file }}"
|
||||||
|
|
||||||
|
# Required environment variables
|
||||||
|
required_env_vars:
|
||||||
|
- LINODE_API_TOKEN
|
||||||
|
|
||||||
|
# Python packages required
|
||||||
|
python_requirements:
|
||||||
|
- requests
|
||||||
|
- json
|
Reference in New Issue
Block a user