This commit is contained in:
0
ansible_manager/__init__.py
Normal file
0
ansible_manager/__init__.py
Normal file
8
ansible_manager/admin.py
Normal file
8
ansible_manager/admin.py
Normal file
@ -0,0 +1,8 @@
|
||||
from django.contrib import admin
|
||||
from .models import AnsibleJob
|
||||
|
||||
|
||||
@admin.register(AnsibleJob)
|
||||
class AnsibleTaskAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'status', 'owner', 'created_at', 'updated_at')
|
||||
readonly_fields = ('status', 'result')
|
6
ansible_manager/apps.py
Normal file
6
ansible_manager/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AnsibleManagerConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'ansible_manager'
|
12
ansible_manager/forms.py
Normal file
12
ansible_manager/forms.py
Normal file
@ -0,0 +1,12 @@
|
||||
from django import forms
|
||||
from .models import AnsibleJob
|
||||
|
||||
class AnsibleJobForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = AnsibleJob
|
||||
fields = ['name', 'playbook_content', 'inventory_content']
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'inventory_content': forms.Textarea(attrs={'class': 'form-control'}),
|
||||
'playbook_content': forms.Textarea(attrs={'class': 'form-control'}),
|
||||
}
|
31
ansible_manager/migrations/0001_initial.py
Normal file
31
ansible_manager/migrations/0001_initial.py
Normal file
@ -0,0 +1,31 @@
|
||||
# Generated by Django 4.2.14 on 2024-12-12 14:20
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AnsibleTask',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200)),
|
||||
('playbook_file', models.FileField(upload_to='playbooks/')),
|
||||
('inventory_file', models.FileField(blank=True, null=True, upload_to='inventories/')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('status', models.CharField(choices=[('PENDING', 'Pending'), ('RUNNING', 'Running'), ('SUCCESS', 'Success'), ('FAILED', 'Failed')], default='PENDING', max_length=20)),
|
||||
('result', models.TextField(blank=True, null=True)),
|
||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
@ -0,0 +1,56 @@
|
||||
# Generated by Django 4.2.14 on 2024-12-12 22:22
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('ansible_manager', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ansibletask',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, help_text='생성 시간'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ansibletask',
|
||||
name='inventory_file',
|
||||
field=models.FileField(blank=True, help_text='Ansible Inventory 파일 경로 (선택 사항)', null=True, upload_to='ansible/inventories/'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ansibletask',
|
||||
name='name',
|
||||
field=models.CharField(help_text='작업 이름', max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ansibletask',
|
||||
name='owner',
|
||||
field=models.ForeignKey(help_text='작업 소유자', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ansibletask',
|
||||
name='playbook_file',
|
||||
field=models.FileField(help_text='Ansible Playbook 파일 경로', upload_to='ansible/playbooks/'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ansibletask',
|
||||
name='result',
|
||||
field=models.TextField(blank=True, help_text='실행 결과', null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ansibletask',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('PENDING', 'Pending'), ('RUNNING', 'Running'), ('SUCCESS', 'Success'), ('FAILED', 'Failed')], default='PENDING', help_text='작업 상태', max_length=20),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ansibletask',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True, help_text='업데이트 시간'),
|
||||
),
|
||||
]
|
@ -0,0 +1,33 @@
|
||||
# Generated by Django 4.2.14 on 2024-12-12 22:35
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('ansible_manager', '0002_alter_ansibletask_created_at_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AnsibleJob',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200)),
|
||||
('playbook_content', models.TextField(help_text='Ansible Playbook YAML 내용')),
|
||||
('inventory_content', models.TextField(help_text='Ansible Inventory 내용')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('status', models.CharField(choices=[('PENDING', 'Pending'), ('RUNNING', 'Running'), ('SUCCESS', 'Success'), ('FAILED', 'Failed')], default='PENDING', max_length=20)),
|
||||
('result', models.TextField(blank=True, null=True)),
|
||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='AnsibleTask',
|
||||
),
|
||||
]
|
0
ansible_manager/migrations/__init__.py
Normal file
0
ansible_manager/migrations/__init__.py
Normal file
23
ansible_manager/models.py
Normal file
23
ansible_manager/models.py
Normal file
@ -0,0 +1,23 @@
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
class AnsibleJob(models.Model):
|
||||
"""Ansible 작업 관리 모델"""
|
||||
STATUS_CHOICES = [
|
||||
('PENDING', 'Pending'),
|
||||
('RUNNING', 'Running'),
|
||||
('SUCCESS', 'Success'),
|
||||
('FAILED', 'Failed'),
|
||||
]
|
||||
|
||||
name = models.CharField(max_length=200)
|
||||
playbook_content = models.TextField(help_text="Ansible Playbook YAML 내용")
|
||||
inventory_content = models.TextField(help_text="Ansible Inventory 내용")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING')
|
||||
result = models.TextField(blank=True, null=True)
|
||||
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
17
ansible_manager/templates/ansible_manager/create_job.html
Normal file
17
ansible_manager/templates/ansible_manager/create_job.html
Normal file
@ -0,0 +1,17 @@
|
||||
{% extends "components/base.html" %}
|
||||
{% block main_area %}
|
||||
<h1>Create Ansible Job</h1>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<label for="id_name">Name:</label>
|
||||
{{ form.name }}
|
||||
|
||||
<label for="id_inventory_content">Inventory content:</label>
|
||||
{{ form.inventory_content }}
|
||||
|
||||
<label for="id_playbook_content">Playbook content:</label>
|
||||
{{ form.playbook_content }}
|
||||
|
||||
<button type="submit" class="btn btn-primary">Create</button>
|
||||
</form>
|
||||
{% endblock %}
|
11
ansible_manager/templates/ansible_manager/edit_job.html
Normal file
11
ansible_manager/templates/ansible_manager/edit_job.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends "components/base.html" %}
|
||||
|
||||
{% block main_area %}
|
||||
<h1>Edit Ansible Job</h1>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
<a href="{% url 'ansible_manager:job_list' %}" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
{% endblock %}
|
12
ansible_manager/templates/ansible_manager/job_detail.html
Normal file
12
ansible_manager/templates/ansible_manager/job_detail.html
Normal file
@ -0,0 +1,12 @@
|
||||
{% extends "components/base.html" %}
|
||||
{% block main_area %}
|
||||
<h1>Job Details: {{ job.name }}</h1>
|
||||
<p><strong>Status:</strong> {{ job.status }}</p>
|
||||
<p><strong>Created At:</strong> {{ job.created_at }}</p>
|
||||
<p><strong>Updated At:</strong> {{ job.updated_at }}</p>
|
||||
|
||||
<h3>Result:</h3>
|
||||
<pre>{{ job.result|escape|linebreaksbr|default:"No result yet." }}</pre>
|
||||
|
||||
<a href="{% url 'ansible_manager:job_list' %}" class="btn btn-secondary">Back to List</a>
|
||||
{% endblock %}
|
36
ansible_manager/templates/ansible_manager/job_list.html
Normal file
36
ansible_manager/templates/ansible_manager/job_list.html
Normal file
@ -0,0 +1,36 @@
|
||||
{% extends "components/base.html" %}
|
||||
{% block main_area %}
|
||||
<h1>Ansible Jobs</h1>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for job in jobs %}
|
||||
<tr>
|
||||
<td>{{ forloop.counter }}</td>
|
||||
<td>{{ job.name }}</td>
|
||||
<td>{{ job.status }}</td>
|
||||
<td>{{ job.created_at }}</td>
|
||||
<td>
|
||||
<a href="{% url 'ansible_manager:job_detail' job.id %}" class="btn btn-info">View</a>
|
||||
<a href="{% url 'ansible_manager:run_job' job.id %}" class="btn btn-success">Run</a>
|
||||
<a href="{% url 'ansible_manager:edit_job' job.id %}" class="btn btn-warning">Edit</a>
|
||||
<a href="{% url 'ansible_manager:delete_job' job.id %}" class="btn btn-danger">Delete</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5">No jobs available.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<a href="{% url 'ansible_manager:create_job' %}" class="btn btn-primary">Create New Job</a>
|
||||
{% endblock %}
|
3
ansible_manager/tests.py
Normal file
3
ansible_manager/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
13
ansible_manager/urls.py
Normal file
13
ansible_manager/urls.py
Normal file
@ -0,0 +1,13 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'ansible_manager'
|
||||
|
||||
urlpatterns = [
|
||||
path('create/', views.create_ansible_job, name='create_job'),
|
||||
path('jobs/<int:job_id>/edit/', views.edit_ansible_job, name='edit_job'),
|
||||
path('jobs/', views.job_list, name='job_list'),
|
||||
path('jobs/<int:job_id>/', views.job_detail, name='job_detail'),
|
||||
path('jobs/<int:job_id>/run/', views.run_job, name='run_job'),
|
||||
path('jobs/<int:job_id>/delete/', views.delete_job, name='delete_job'),
|
||||
]
|
118
ansible_manager/views.py
Normal file
118
ansible_manager/views.py
Normal file
@ -0,0 +1,118 @@
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from .models import AnsibleJob
|
||||
from .forms import AnsibleJobForm
|
||||
import subprocess
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
def run_ansible_job(request, job):
|
||||
user = request.user
|
||||
decrypted_key = ""
|
||||
|
||||
if user.encrypted_private_key:
|
||||
try:
|
||||
decrypted_key = user.decrypt_private_key().strip().replace("\r", "")
|
||||
except Exception as e:
|
||||
job.result = f"SSH 키 복호화 오류: {str(e)}"
|
||||
job.status = 'FAILED'
|
||||
job.save()
|
||||
return
|
||||
|
||||
job.status = 'RUNNING'
|
||||
job.save()
|
||||
|
||||
try:
|
||||
private_key = decrypted_key
|
||||
playbook_file = tempfile.NamedTemporaryFile(delete=False, mode="w", newline='')
|
||||
inventory_file = tempfile.NamedTemporaryFile(delete=False, mode="w", newline='')
|
||||
private_key_file = tempfile.NamedTemporaryFile(delete=False, mode="w", newline='')
|
||||
|
||||
try:
|
||||
playbook_file.write(job.playbook_content.strip())
|
||||
playbook_file.close()
|
||||
|
||||
inventory_file.write(job.inventory_content.strip())
|
||||
inventory_file.close()
|
||||
|
||||
private_key_file.write(f"{private_key}\n")
|
||||
private_key_file.close()
|
||||
|
||||
os.chmod(private_key_file.name, 0o600)
|
||||
|
||||
command = [
|
||||
"ansible-playbook", playbook_file.name,
|
||||
"-i", inventory_file.name,
|
||||
"--private-key", private_key_file.name,
|
||||
# "-vvv"
|
||||
]
|
||||
|
||||
result = subprocess.run(command, capture_output=True, text=True)
|
||||
job.result = result.stdout
|
||||
job.status = 'SUCCESS' if result.returncode == 0 else 'FAILED'
|
||||
finally:
|
||||
os.remove(playbook_file.name)
|
||||
os.remove(inventory_file.name)
|
||||
os.remove(private_key_file.name)
|
||||
|
||||
except Exception as e:
|
||||
job.result = f"오류 발생: {str(e)}"
|
||||
job.status = 'FAILED'
|
||||
|
||||
job.save()
|
||||
|
||||
|
||||
@login_required
|
||||
def create_ansible_job(request):
|
||||
if request.method == 'POST':
|
||||
form = AnsibleJobForm(request.POST)
|
||||
if form.is_valid():
|
||||
job = form.save(commit=False)
|
||||
job.owner = request.user
|
||||
job.status = 'PENDING'
|
||||
job.save()
|
||||
return redirect('ansible_manager:job_list')
|
||||
else:
|
||||
form = AnsibleJobForm()
|
||||
return render(request, 'ansible_manager/create_job.html', {'form': form})
|
||||
|
||||
@login_required
|
||||
def edit_ansible_job(request, job_id):
|
||||
job = get_object_or_404(AnsibleJob, id=job_id, owner=request.user)
|
||||
if request.method == 'POST':
|
||||
form = AnsibleJobForm(request.POST, instance=job)
|
||||
if form.is_valid():
|
||||
job = form.save(commit=False)
|
||||
job.owner = request.user
|
||||
job.status = 'PENDING'
|
||||
job.save()
|
||||
# return redirect('ansible_manager:job_detail', job_id=job.id)
|
||||
return redirect('ansible_manager:job_list')
|
||||
else:
|
||||
form = AnsibleJobForm(instance=job)
|
||||
return render(request, 'ansible_manager/edit_job.html', {'form': form, 'job': job})
|
||||
|
||||
@login_required
|
||||
def job_list(request):
|
||||
jobs = AnsibleJob.objects.filter(owner=request.user).order_by('-created_at')
|
||||
return render(request, 'ansible_manager/job_list.html', {'jobs': jobs})
|
||||
|
||||
|
||||
@login_required
|
||||
def run_job(request, job_id):
|
||||
job = get_object_or_404(AnsibleJob, id=job_id, owner=request.user)
|
||||
run_ansible_job(request, job)
|
||||
return redirect('ansible_manager:job_detail', job_id=job.id)
|
||||
|
||||
|
||||
@login_required
|
||||
def job_detail(request, job_id):
|
||||
job = get_object_or_404(AnsibleJob, id=job_id, owner=request.user)
|
||||
return render(request, 'ansible_manager/job_detail.html', {'job': job})
|
||||
|
||||
|
||||
@login_required
|
||||
def delete_job(request, job_id):
|
||||
job = get_object_or_404(AnsibleJob, id=job_id, owner=request.user)
|
||||
job.delete()
|
||||
return redirect('ansible_manager:job_list')
|
Reference in New Issue
Block a user