1+ from fastapi import FastAPI , HTTPException , BackgroundTasks
2+ from fastapi .middleware .cors import CORSMiddleware
3+ from pydantic import BaseModel
4+ from typing import List , Dict , Optional , Any
5+ import os
6+ import yaml
7+ import configparser
8+ import subprocess
9+ import asyncio
10+ from pathlib import Path
11+ import uuid
12+ from datetime import datetime
13+
14+ app = FastAPI (title = "Ansible Dashboard" )
15+
16+ app .add_middleware (
17+ CORSMiddleware ,
18+ allow_origins = ["*" ],
19+ allow_credentials = True ,
20+ allow_methods = ["*" ],
21+ allow_headers = ["*" ],
22+ )
23+
24+ # Store for running jobs
25+ jobs_store : Dict [str , Dict [str , Any ]] = {}
26+
27+ # Base paths
28+ # Check if running in Docker (Ansible folder is mounted at /app/Ansible)
29+ if Path ("/app/Ansible" ).exists ():
30+ ANSIBLE_BASE = Path ("/app/Ansible" )
31+ else :
32+ ANSIBLE_BASE = Path (__file__ ).parent .parent .parent / "Ansible"
33+
34+ class InventoryEntry (BaseModel ):
35+ name : str
36+ host : str
37+ user : str
38+ group : str
39+
40+ class AnsibleFolder (BaseModel ):
41+ name : str
42+ path : str
43+ has_inventory : bool
44+ has_vars : bool
45+ has_playbooks : bool
46+ playbooks : List [str ]
47+
48+ class PlaybookRequest (BaseModel ):
49+ folder : str
50+ playbook : str
51+ inventory : str
52+ vars : Dict [str , Any ]
53+
54+ class JobStatus (BaseModel ):
55+ job_id : str
56+ status : str
57+ output : str
58+ started_at : str
59+ completed_at : Optional [str ]
60+
61+ @app .get ("/" )
62+ async def root ():
63+ return {"message" : "Ansible Dashboard API" }
64+
65+ @app .get ("/api/folders" , response_model = List [AnsibleFolder ])
66+ async def get_ansible_folders ():
67+ """Get all Ansible folders with their details"""
68+ folders = []
69+
70+ if not ANSIBLE_BASE .exists ():
71+ return folders
72+
73+ for folder in ANSIBLE_BASE .iterdir ():
74+ if folder .is_dir () and not folder .name .startswith ('.' ):
75+ playbooks = []
76+ has_inventory = False
77+ has_vars = False
78+
79+ # Check for inventory files
80+ if (folder / "inventory.ini" ).exists () or (folder / "hosts" ).exists ():
81+ has_inventory = True
82+
83+ # Check for vars files
84+ if (folder / "vars.yml" ).exists () or (folder / "variables.yml" ).exists ():
85+ has_vars = True
86+
87+ # Find playbook files
88+ for file in folder .glob ("*.yml" ):
89+ if file .name not in ["vars.yml" , "variables.yml" ]:
90+ playbooks .append (file .name )
91+
92+ folders .append (AnsibleFolder (
93+ name = folder .name ,
94+ path = str (folder ),
95+ has_inventory = has_inventory ,
96+ has_vars = has_vars ,
97+ has_playbooks = len (playbooks ) > 0 ,
98+ playbooks = playbooks
99+ ))
100+
101+ return sorted (folders , key = lambda x : x .name )
102+
103+ @app .get ("/api/folders/{folder_name}/inventory" )
104+ async def get_inventory (folder_name : str ):
105+ """Parse and return inventory file content"""
106+ folder_path = ANSIBLE_BASE / folder_name
107+
108+ inventory_file = folder_path / "inventory.ini"
109+ if not inventory_file .exists ():
110+ inventory_file = folder_path / "hosts"
111+
112+ if not inventory_file .exists ():
113+ raise HTTPException (status_code = 404 , detail = "Inventory file not found" )
114+
115+ config = configparser .ConfigParser (allow_no_value = True )
116+ config .read (inventory_file )
117+
118+ inventory_data = {}
119+ for section in config .sections ():
120+ hosts = []
121+ for key in config [section ]:
122+ # Parse inventory line
123+ parts = key .split ()
124+ host_info = {"name" : parts [0 ] if parts else key }
125+
126+ for part in parts [1 :]:
127+ if "=" in part :
128+ k , v = part .split ("=" , 1 )
129+ host_info [k ] = v
130+
131+ hosts .append (host_info )
132+ inventory_data [section ] = hosts
133+
134+ return {
135+ "content" : inventory_data ,
136+ "raw" : inventory_file .read_text ()
137+ }
138+
139+ @app .get ("/api/folders/{folder_name}/vars" )
140+ async def get_vars (folder_name : str ):
141+ """Parse and return vars file content"""
142+ folder_path = ANSIBLE_BASE / folder_name
143+
144+ vars_file = folder_path / "vars.yml"
145+ if not vars_file .exists ():
146+ vars_file = folder_path / "variables.yml"
147+
148+ if not vars_file .exists ():
149+ raise HTTPException (status_code = 404 , detail = "Vars file not found" )
150+
151+ with open (vars_file , 'r' ) as f :
152+ vars_data = yaml .safe_load (f ) or {}
153+
154+ return {
155+ "content" : vars_data ,
156+ "raw" : vars_file .read_text ()
157+ }
158+
159+ @app .post ("/api/folders/{folder_name}/inventory" )
160+ async def update_inventory (folder_name : str , content : Dict [str , Any ]):
161+ """Update inventory file"""
162+ folder_path = ANSIBLE_BASE / folder_name
163+ inventory_file = folder_path / "inventory.ini"
164+
165+ if "raw" in content :
166+ # Save raw content
167+ inventory_file .write_text (content ["raw" ])
168+ else :
169+ # Generate from structured data
170+ lines = []
171+ for group , hosts in content .items ():
172+ lines .append (f"[{ group } ]" )
173+ for host in hosts :
174+ host_line = host .get ("name" , "" )
175+ for key , value in host .items ():
176+ if key != "name" :
177+ host_line += f" { key } ={ value } "
178+ lines .append (host_line )
179+ lines .append ("" )
180+
181+ inventory_file .write_text ("\n " .join (lines ))
182+
183+ return {"success" : True }
184+
185+ @app .post ("/api/folders/{folder_name}/vars" )
186+ async def update_vars (folder_name : str , content : Dict [str , Any ]):
187+ """Update vars file"""
188+ folder_path = ANSIBLE_BASE / folder_name
189+ vars_file = folder_path / "vars.yml"
190+
191+ if "raw" in content :
192+ vars_file .write_text (content ["raw" ])
193+ else :
194+ with open (vars_file , 'w' ) as f :
195+ yaml .dump (content , f , default_flow_style = False )
196+
197+ return {"success" : True }
198+
199+ async def run_ansible_playbook (job_id : str , folder : str , playbook : str , inventory : str ):
200+ """Run ansible playbook in background"""
201+ folder_path = ANSIBLE_BASE / folder
202+ playbook_path = folder_path / playbook
203+ inventory_path = folder_path / inventory
204+
205+ jobs_store [job_id ]["status" ] = "running"
206+
207+ try :
208+ # Change to the folder directory to respect ansible.cfg
209+ process = await asyncio .create_subprocess_exec (
210+ "ansible-playbook" ,
211+ "-i" , str (inventory_path ),
212+ str (playbook_path ),
213+ cwd = str (folder_path ),
214+ stdout = asyncio .subprocess .PIPE ,
215+ stderr = asyncio .subprocess .STDOUT
216+ )
217+
218+ output , _ = await process .communicate ()
219+
220+ jobs_store [job_id ]["output" ] = output .decode ()
221+ jobs_store [job_id ]["status" ] = "completed" if process .returncode == 0 else "failed"
222+ jobs_store [job_id ]["completed_at" ] = datetime .now ().isoformat ()
223+ jobs_store [job_id ]["return_code" ] = process .returncode
224+
225+ except Exception as e :
226+ jobs_store [job_id ]["status" ] = "error"
227+ jobs_store [job_id ]["output" ] = str (e )
228+ jobs_store [job_id ]["completed_at" ] = datetime .now ().isoformat ()
229+
230+ @app .post ("/api/run" )
231+ async def run_playbook (request : PlaybookRequest , background_tasks : BackgroundTasks ):
232+ """Run ansible playbook"""
233+ job_id = str (uuid .uuid4 ())
234+
235+ jobs_store [job_id ] = {
236+ "job_id" : job_id ,
237+ "status" : "queued" ,
238+ "output" : "" ,
239+ "started_at" : datetime .now ().isoformat (),
240+ "completed_at" : None ,
241+ "folder" : request .folder ,
242+ "playbook" : request .playbook
243+ }
244+
245+ # Update vars if provided
246+ if request .vars :
247+ folder_path = ANSIBLE_BASE / request .folder
248+ vars_file = folder_path / "vars.yml"
249+ with open (vars_file , 'w' ) as f :
250+ yaml .dump (request .vars , f , default_flow_style = False )
251+
252+ background_tasks .add_task (
253+ run_ansible_playbook ,
254+ job_id ,
255+ request .folder ,
256+ request .playbook ,
257+ request .inventory
258+ )
259+
260+ return {"job_id" : job_id }
261+
262+ @app .get ("/api/jobs/{job_id}" , response_model = JobStatus )
263+ async def get_job_status (job_id : str ):
264+ """Get job status"""
265+ if job_id not in jobs_store :
266+ raise HTTPException (status_code = 404 , detail = "Job not found" )
267+
268+ return jobs_store [job_id ]
269+
270+ @app .get ("/api/jobs" )
271+ async def get_all_jobs ():
272+ """Get all jobs"""
273+ return list (jobs_store .values ())
274+
275+ if __name__ == "__main__" :
276+ import uvicorn
277+ uvicorn .run (app , host = "0.0.0.0" , port = 8000 )
0 commit comments