Skip to content

Commit 09fc277

Browse files
committed
[Update] Support history view
1 parent d32f070 commit 09fc277

10 files changed

Lines changed: 219 additions & 24 deletions

File tree

apps/ops/ansible/callback.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
# ~*~ coding: utf-8 ~*~
22

3+
import sys
4+
35
from ansible.plugins.callback import CallbackBase
46
from ansible.plugins.callback.default import CallbackModule
57

8+
from .display import TeeObj
9+
610

711
class AdHocResultCallback(CallbackModule):
812
"""
913
Task result Callback
1014
"""
11-
def __init__(self, display=None, options=None):
15+
def __init__(self, display=None, options=None, file_obj=None):
1216
# result_raw example: {
1317
# "ok": {"hostname": {"task_name": {},...},..},
1418
# "failed": {"hostname": {"task_name": {}..}, ..},
@@ -22,6 +26,8 @@ def __init__(self, display=None, options=None):
2226
self.results_raw = dict(ok={}, failed={}, unreachable={}, skipped={})
2327
self.results_summary = dict(contacted=[], dark={})
2428
super().__init__()
29+
if file_obj is not None:
30+
sys.stdout = TeeObj(file_obj)
2531

2632
def gather_result(self, t, res):
2733
self._clean_results(res._result, res._task.action)

apps/ops/ansible/display.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
4+
import sys
5+
6+
7+
class TeeObj:
8+
origin_stdout = sys.stdout
9+
10+
def __init__(self, file_obj):
11+
self.file_obj = file_obj
12+
13+
def write(self, msg):
14+
self.origin_stdout.write(msg)
15+
self.file_obj.write(msg.replace('*', ''))
16+
17+
def flush(self):
18+
self.origin_stdout.flush()
19+
self.file_obj.flush()

apps/ops/ansible/inventory.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ def parse_groups(self):
132132
parent.add_child_group(child)
133133

134134
def parse_hosts(self):
135+
group_all = self.get_or_create_group('all')
136+
ungrouped = self.get_or_create_group('ungrouped')
135137
for host_data in self.host_list:
136138
host = self.host_manager_class(host_data=host_data)
137139
self.hosts[host_data['hostname']] = host
@@ -140,6 +142,9 @@ def parse_hosts(self):
140142
for group_name in groups_data:
141143
group = self.get_or_create_group(group_name)
142144
group.add_host(host)
145+
else:
146+
ungrouped.add_host(host)
147+
group_all.add_host(host)
143148

144149
def parse_sources(self, cache=False):
145150
self.parse_groups()

apps/ops/ansible/runner.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from ansible.executor.playbook_executor import PlaybookExecutor
1010
from ansible.playbook.play import Play
1111
import ansible.constants as C
12+
from ansible.utils.display import Display
1213

1314
from .callback import AdHocResultCallback, PlaybookResultCallBack, \
1415
CommandResultCallback
@@ -21,6 +22,13 @@
2122
logger = get_logger(__name__)
2223

2324

25+
class CustomDisplay(Display):
26+
def display(self, msg, color=None, stderr=False, screen_only=False, log_only=False):
27+
pass
28+
29+
display = CustomDisplay()
30+
31+
2432
Options = namedtuple('Options', [
2533
'listtags', 'listtasks', 'listhosts', 'syntax', 'connection',
2634
'module_path', 'forks', 'remote_user', 'private_key_file', 'timeout',
@@ -123,20 +131,22 @@ class AdHocRunner:
123131
ADHoc Runner接口
124132
"""
125133
results_callback_class = AdHocResultCallback
134+
results_callback = None
126135
loader_class = DataLoader
127136
variable_manager_class = VariableManager
128-
options = get_default_options()
129137
default_options = get_default_options()
130138

131139
def __init__(self, inventory, options=None):
132-
if options:
133-
self.options = options
140+
self.options = self.update_options(options)
134141
self.inventory = inventory
135142
self.loader = DataLoader()
136143
self.variable_manager = VariableManager(
137144
loader=self.loader, inventory=self.inventory
138145
)
139146

147+
def get_result_callback(self, file_obj=None):
148+
return self.__class__.results_callback_class(file_obj=file_obj)
149+
140150
@staticmethod
141151
def check_module_args(module_name, module_args=''):
142152
if module_name in C.MODULE_REQUIRE_ARGS and not module_args:
@@ -160,19 +170,24 @@ def clean_tasks(self, tasks):
160170
cleaned_tasks.append(task)
161171
return cleaned_tasks
162172

163-
def set_option(self, k, v):
164-
kwargs = {k: v}
165-
self.options = self.options._replace(**kwargs)
173+
def update_options(self, options):
174+
if options and isinstance(options, dict):
175+
options = self.__class__.default_options._replace(**options)
176+
else:
177+
options = self.__class__.default_options
178+
return options
166179

167-
def run(self, tasks, pattern, play_name='Ansible Ad-hoc', gather_facts='no'):
180+
def run(self, tasks, pattern, play_name='Ansible Ad-hoc', gather_facts='no', file_obj=None):
168181
"""
169182
:param tasks: [{'action': {'module': 'shell', 'args': 'ls'}, ...}, ]
170183
:param pattern: all, *, or others
171184
:param play_name: The play name
185+
:param gather_facts:
186+
:param file_obj: logging to file_obj
172187
:return:
173188
"""
174189
self.check_pattern(pattern)
175-
results_callback = self.results_callback_class()
190+
self.results_callback = self.get_result_callback(file_obj)
176191
cleaned_tasks = self.clean_tasks(tasks)
177192

178193
play_source = dict(
@@ -193,16 +208,16 @@ def run(self, tasks, pattern, play_name='Ansible Ad-hoc', gather_facts='no'):
193208
variable_manager=self.variable_manager,
194209
loader=self.loader,
195210
options=self.options,
196-
stdout_callback=results_callback,
211+
stdout_callback=self.results_callback,
197212
passwords=self.options.passwords,
198213
)
199-
logger.debug("Get inventory matched hosts: {}".format(
214+
print("Get matched hosts: {}".format(
200215
self.inventory.get_matched_hosts(pattern)
201216
))
202217

203218
try:
204219
tqm.run(play)
205-
return results_callback
220+
return self.results_callback
206221
except Exception as e:
207222
raise AnsibleError(e)
208223
finally:

apps/ops/api.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
# ~*~ coding: utf-8 ~*~
2+
import uuid
3+
import re
24

3-
5+
from django.core.cache import cache
46
from django.shortcuts import get_object_or_404
57
from rest_framework import viewsets, generics
8+
from rest_framework.generics import RetrieveAPIView
69
from rest_framework.views import Response
710

811
from .hands import IsSuperUser
@@ -58,3 +61,26 @@ def get_queryset(self):
5861
adhoc = get_object_or_404(AdHoc, id=adhoc_id)
5962
self.queryset = self.queryset.filter(adhoc=adhoc)
6063
return self.queryset
64+
65+
66+
class AdHocHistoryOutputAPI(RetrieveAPIView):
67+
queryset = AdHocRunHistory.objects.all()
68+
permission_classes = (IsSuperUser,)
69+
buff_size = 1024 * 10
70+
end = False
71+
72+
def retrieve(self, request, *args, **kwargs):
73+
history = self.get_object()
74+
mark = request.query_params.get("mark") or str(uuid.uuid4())
75+
76+
with open(history.log_path, 'r') as f:
77+
offset = cache.get(mark, 0)
78+
f.seek(offset)
79+
data = f.read(self.buff_size).replace('\n', '\r\n')
80+
print(repr(data))
81+
mark = str(uuid.uuid4())
82+
cache.set(mark, f.tell(), 5)
83+
84+
if history.is_finished and data == '':
85+
self.end = True
86+
return Response({"data": data, 'end': self.end, 'mark': mark})

apps/ops/models.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@
22

33
import json
44
import uuid
5-
5+
import os
66
import time
7+
import datetime
8+
79
from django.db import models
10+
from django.conf import settings
811
from django.utils import timezone
912
from django.utils.translation import ugettext_lazy as _
10-
from django_celery_beat.models import CrontabSchedule, IntervalSchedule, PeriodicTask
13+
from django_celery_beat.models import CrontabSchedule, IntervalSchedule, \
14+
PeriodicTask
1115

1216
from common.utils import get_signer, get_logger
13-
from common.celery import delete_celery_periodic_task, create_or_update_celery_periodic_tasks, \
14-
disable_celery_periodic_task
17+
from common.celery import delete_celery_periodic_task, \
18+
create_or_update_celery_periodic_tasks, \
19+
disable_celery_periodic_task
1520
from .ansible import AdHocRunner, AnsibleError
1621
from .inventory import JMSInventory
1722

@@ -209,7 +214,8 @@ def _run_and_record(self):
209214
history = AdHocRunHistory(adhoc=self, task=self.task)
210215
time_start = time.time()
211216
try:
212-
raw, summary = self._run_only()
217+
with open(history.log_path, 'w') as f:
218+
raw, summary = self._run_only(file_obj=f)
213219
history.is_finished = True
214220
if summary.get('dark'):
215221
history.is_success = False
@@ -225,13 +231,15 @@ def _run_and_record(self):
225231
history.timedelta = time.time() - time_start
226232
history.save()
227233

228-
def _run_only(self):
229-
runner = AdHocRunner(self.inventory)
230-
for k, v in self.options.items():
231-
runner.set_option(k, v)
232-
234+
def _run_only(self, file_obj=None):
235+
runner = AdHocRunner(self.inventory, options=self.options)
233236
try:
234-
result = runner.run(self.tasks, self.pattern, self.task.name)
237+
result = runner.run(
238+
self.tasks,
239+
self.pattern,
240+
self.task.name,
241+
file_obj=file_obj,
242+
)
235243
return result.results_raw, result.results_summary
236244
except AnsibleError as e:
237245
logger.warn("Failed run adhoc {}, {}".format(self.task.name, e))
@@ -316,6 +324,14 @@ class AdHocRunHistory(models.Model):
316324
def short_id(self):
317325
return str(self.id).split('-')[-1]
318326

327+
@property
328+
def log_path(self):
329+
dt = datetime.datetime.now().strftime('%Y-%m-%d')
330+
log_dir = os.path.join(settings.PROJECT_DIR, 'data', 'ansible', dt)
331+
if not os.path.exists(log_dir):
332+
os.makedirs(log_dir)
333+
return os.path.join(log_dir, str(self.id) + '.log')
334+
319335
@property
320336
def result(self):
321337
if self._result:
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
{% load static %}
2+
<!doctype html>
3+
<html>
4+
<head>
5+
<title>term.js</title>
6+
<script src="{% static 'js/jquery-2.1.1.js' %}"></script>
7+
<style>
8+
html {
9+
background: #000;
10+
}
11+
h1 {
12+
margin-bottom: 20px;
13+
font: 20px/1.5 sans-serif;
14+
}
15+
.terminal {
16+
float: left;
17+
font-family: 'Monaco', 'Consolas', "DejaVu Sans Mono", "Liberation Mono", monospace;
18+
font-size: 14px;
19+
color: #f0f0f0;
20+
background-color: #555;
21+
padding: 20px 20px 20px;
22+
}
23+
.terminal-cursor {
24+
color: #000;
25+
background: #f0f0f0;
26+
}
27+
</style>
28+
</head>
29+
<body>
30+
<div class="container">
31+
<div id="term">
32+
</div>
33+
</div>
34+
</body>
35+
36+
37+
<script src="{% static 'js/term.js' %}"></script>
38+
<script>
39+
var rowHeight = 1;
40+
var colWidth = 1;
41+
var mark = '';
42+
var url = "{% url 'api-ops:history-output' pk=object.id %}";
43+
var term;
44+
var end = false;
45+
46+
function calWinSize() {
47+
var t = $('.terminal');
48+
console.log(t.height());
49+
rowHeight = 1.00 * t.height() / 24;
50+
colWidth = 1.00 * t.width() / 80;
51+
}
52+
function resize() {
53+
var rows = Math.floor(window.innerHeight / rowHeight) - 2;
54+
var cols = Math.floor(window.innerWidth / colWidth) - 5;
55+
term.resize(cols, rows);
56+
}
57+
function requestAndWrite() {
58+
if (!end) {
59+
$.ajax({
60+
url: url + '?mark=' + mark,
61+
method: "GET",
62+
contentType: "application/json; charset=utf-8"
63+
}).done(function(data, textStatue, jqXHR) {
64+
term.write(data.data);
65+
mark = data.mark;
66+
if (data.end){
67+
end = true
68+
}
69+
}).fail(function(jqXHR, textStatus, errorThrown) {
70+
});
71+
}
72+
}
73+
$(document).ready(function () {
74+
term = new Terminal({
75+
cols: 80,
76+
rows: 24,
77+
useStyle: true,
78+
screenKeys: false
79+
});
80+
term.open();
81+
term.on('data', function (data) {
82+
term.write(data.replace('\r', '\r\n'))
83+
});
84+
calWinSize();
85+
resize();
86+
$('.terminal').detach().appendTo('#term');
87+
term.write('\x1b[31mWelcome to term.js!\x1b[m\r\n');
88+
setInterval(function () {
89+
requestAndWrite()
90+
}, 100)
91+
});
92+
</script>
93+
</html>

apps/ops/urls/api_urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
urlpatterns = [
1717
url(r'^v1/tasks/(?P<pk>[0-9a-zA-Z\-]{36})/run/$', api.TaskRun.as_view(), name='task-run'),
18+
url(r'^v1/history/(?P<pk>[0-9a-zA-Z\-]{36})/output/$', api.AdHocHistoryOutputAPI.as_view(), name='history-output'),
1819
]
1920

2021
urlpatterns += router.urls

apps/ops/urls/view_urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@
1818
url(r'^adhoc/(?P<pk>[0-9a-zA-Z\-]{36})/$', views.AdHocDetailView.as_view(), name='adhoc-detail'),
1919
url(r'^adhoc/(?P<pk>[0-9a-zA-Z\-]{36})/history/$', views.AdHocHistoryView.as_view(), name='adhoc-history'),
2020
url(r'^adhoc/history/(?P<pk>[0-9a-zA-Z\-]{36})/$', views.AdHocHistoryDetailView.as_view(), name='adhoc-history-detail'),
21+
url(r'^adhoc/history/(?P<pk>[0-9a-zA-Z\-]{36})/output/$', views.AdHocHistoryOutputView.as_view(), name='adhoc-history-output'),
2122
]

0 commit comments

Comments
 (0)