Reattempting Linode dynamic inventories.

This commit is contained in:
Kevin Thompson
2025-08-07 12:47:06 -05:00
parent 9e07592c4d
commit 1b3bdeb740
9 changed files with 430 additions and 55 deletions

View 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

View 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()

View File

@@ -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()

View 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: []

View File

@@ -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'

View 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)

View 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'

View 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