Commit 89fa0658 authored by ibuler's avatar ibuler

[Update] 修改任务执行

parent 847e37e6
# -*- coding: utf-8 -*-
#
from django.http import HttpResponse
from django.conf import settings
from django.utils.translation import ugettext as _
from django.views.decorators.csrf import csrf_exempt
from proxy.views import proxy_view
flower_url = settings.FLOWER_URL
@csrf_exempt
def celery_flower_view(request, path):
if not request.user.is_superuser:
return HttpResponse("Forbidden")
remote_url = 'http://{}/{}'.format(flower_url, path)
try:
response = proxy_view(request, remote_url)
except Exception as e:
msg = _("<h1>Flow service unavailable, check it</h1>") + \
'<br><br> <div>{}</div>'.format(e)
response = HttpResponse(msg)
return response
...@@ -382,7 +382,8 @@ defaults = { ...@@ -382,7 +382,8 @@ defaults = {
'SYSLOG_ADDR': '', # '192.168.0.1:514' 'SYSLOG_ADDR': '', # '192.168.0.1:514'
'SYSLOG_FACILITY': 'user', 'SYSLOG_FACILITY': 'user',
'PERM_SINGLE_ASSET_TO_UNGROUP_NODE': False, 'PERM_SINGLE_ASSET_TO_UNGROUP_NODE': False,
'WINDOWS_SSH_DEFAULT_SHELL': 'cmd' 'WINDOWS_SSH_DEFAULT_SHELL': 'cmd',
'FLOWER_URL': "127.0.0.1:5555"
} }
......
...@@ -623,3 +623,4 @@ BACKEND_ASSET_USER_AUTH_VAULT = False ...@@ -623,3 +623,4 @@ BACKEND_ASSET_USER_AUTH_VAULT = False
PERM_SINGLE_ASSET_TO_UNGROUP_NODE = CONFIG.PERM_SINGLE_ASSET_TO_UNGROUP_NODE PERM_SINGLE_ASSET_TO_UNGROUP_NODE = CONFIG.PERM_SINGLE_ASSET_TO_UNGROUP_NODE
WINDOWS_SSH_DEFAULT_SHELL = CONFIG.WINDOWS_SSH_DEFAULT_SHELL WINDOWS_SSH_DEFAULT_SHELL = CONFIG.WINDOWS_SSH_DEFAULT_SHELL
FLOWER_URL = CONFIG.FLOWER_URL
...@@ -7,7 +7,9 @@ from django.conf.urls.static import static ...@@ -7,7 +7,9 @@ from django.conf.urls.static import static
from django.conf.urls.i18n import i18n_patterns from django.conf.urls.i18n import i18n_patterns
from django.views.i18n import JavaScriptCatalog from django.views.i18n import JavaScriptCatalog
from .views import IndexView, LunaView, I18NView, HealthCheckView, redirect_format_api # from .views import IndexView, LunaView, I18NView, HealthCheckView, redirect_format_api
from . import views
from .celery_flower import celery_flower_view
from .swagger import get_swagger_view from .swagger import get_swagger_view
api_v1 = [ api_v1 = [
...@@ -40,6 +42,7 @@ app_view_patterns = [ ...@@ -40,6 +42,7 @@ app_view_patterns = [
path('orgs/', include('orgs.urls.views_urls', namespace='orgs')), path('orgs/', include('orgs.urls.views_urls', namespace='orgs')),
path('auth/', include('authentication.urls.view_urls'), name='auth'), path('auth/', include('authentication.urls.view_urls'), name='auth'),
path('applications/', include('applications.urls.views_urls', namespace='applications')), path('applications/', include('applications.urls.views_urls', namespace='applications')),
re_path(r'flower/(?P<path>.*)', celery_flower_view, name='flower-view'),
] ]
...@@ -57,13 +60,13 @@ js_i18n_patterns = i18n_patterns( ...@@ -57,13 +60,13 @@ js_i18n_patterns = i18n_patterns(
urlpatterns = [ urlpatterns = [
path('', IndexView.as_view(), name='index'), path('', views.IndexView.as_view(), name='index'),
path('api/v1/', include(api_v1)), path('api/v1/', include(api_v1)),
path('api/v2/', include(api_v2)), path('api/v2/', include(api_v2)),
re_path('api/(?P<app>\w+)/(?P<version>v\d)/.*', redirect_format_api), re_path('api/(?P<app>\w+)/(?P<version>v\d)/.*', views.redirect_format_api),
path('api/health/', HealthCheckView.as_view(), name="health"), path('api/health/', views.HealthCheckView.as_view(), name="health"),
path('luna/', LunaView.as_view(), name='luna-view'), path('luna/', views.LunaView.as_view(), name='luna-view'),
path('i18n/<str:lang>/', I18NView.as_view(), name='i18n-switch'), path('i18n/<str:lang>/', views.I18NView.as_view(), name='i18n-switch'),
path('settings/', include('settings.urls.view_urls', namespace='settings')), path('settings/', include('settings.urls.view_urls', namespace='settings')),
# External apps url # External apps url
......
...@@ -224,3 +224,6 @@ class HealthCheckView(APIView): ...@@ -224,3 +224,6 @@ class HealthCheckView(APIView):
def get(self, request): def get(self, request):
return JsonResponse({"status": 1, "time": int(time.time())}) return JsonResponse({"status": 1, "time": int(time.time())})
This diff is collapsed.
# Generated by Django 2.1.7 on 2019-09-19 13:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ops', '0007_auto_20190724_2002'),
]
operations = [
migrations.AddField(
model_name='task',
name='date_updated',
field=models.DateTimeField(auto_now=True, verbose_name='Date updated'),
),
migrations.AlterField(
model_name='task',
name='date_created',
field=models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Date created'),
),
migrations.AlterModelOptions(
name='task',
options={'get_latest_by': 'date_created',
'ordering': ('-date_updated',)},
),
]
...@@ -13,7 +13,7 @@ from django.utils import timezone ...@@ -13,7 +13,7 @@ from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django_celery_beat.models import PeriodicTask from django_celery_beat.models import PeriodicTask
from common.utils import get_signer, get_logger from common.utils import get_signer, get_logger, lazyproperty
from orgs.utils import set_to_root_org from orgs.utils import set_to_root_org
from ..celery.utils import delete_celery_periodic_task, \ from ..celery.utils import delete_celery_periodic_task, \
create_or_update_celery_periodic_tasks, \ create_or_update_celery_periodic_tasks, \
...@@ -42,7 +42,8 @@ class Task(models.Model): ...@@ -42,7 +42,8 @@ class Task(models.Model):
is_deleted = models.BooleanField(default=False) is_deleted = models.BooleanField(default=False)
comment = models.TextField(blank=True, verbose_name=_("Comment")) comment = models.TextField(blank=True, verbose_name=_("Comment"))
created_by = models.CharField(max_length=128, blank=True, default='') created_by = models.CharField(max_length=128, blank=True, default='')
date_created = models.DateTimeField(auto_now_add=True, db_index=True) date_created = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name=_("Date created"))
date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated"))
__latest_adhoc = None __latest_adhoc = None
_ignore_auto_created_by = True _ignore_auto_created_by = True
...@@ -51,16 +52,39 @@ class Task(models.Model): ...@@ -51,16 +52,39 @@ class Task(models.Model):
return str(self.id).split('-')[-1] return str(self.id).split('-')[-1]
@property @property
def latest_adhoc(self): def versions(self):
if not self.__latest_adhoc: return self.adhoc.all().count()
self.__latest_adhoc = self.get_latest_adhoc()
return self.__latest_adhoc @property
def is_success(self):
if self.latest_history:
return self.latest_history.is_success
else:
return False
@latest_adhoc.setter @property
def latest_adhoc(self, item): def timedelta(self):
self.__latest_adhoc = item if self.latest_history:
return self.latest_history.timedelta
else:
return 0
@property @property
def date_start(self):
if self.latest_history:
return self.latest_history.date_start
else:
return None
@property
def assets_amount(self):
return self.latest_adhoc.hosts.count()
@lazyproperty
def latest_adhoc(self):
return self.get_latest_adhoc()
@lazyproperty
def latest_history(self): def latest_history(self):
try: try:
return self.history.all().latest() return self.history.all().latest()
...@@ -139,6 +163,7 @@ class Task(models.Model): ...@@ -139,6 +163,7 @@ class Task(models.Model):
class Meta: class Meta:
db_table = 'ops_task' db_table = 'ops_task'
unique_together = ('name', 'created_by') unique_together = ('name', 'created_by')
ordering = ('-date_updated',)
get_latest_by = 'date_created' get_latest_by = 'date_created'
...@@ -246,6 +271,7 @@ class AdHoc(models.Model): ...@@ -246,6 +271,7 @@ class AdHoc(models.Model):
) )
def _run_only(self): def _run_only(self):
Task.objects.filter(id=self.task.id).update(date_updated=timezone.now())
runner = AdHocRunner(self.inventory, options=self.options) runner = AdHocRunner(self.inventory, options=self.options)
try: try:
result = runner.run( result = runner.run(
......
...@@ -19,7 +19,12 @@ class CeleryTaskSerializer(serializers.Serializer): ...@@ -19,7 +19,12 @@ class CeleryTaskSerializer(serializers.Serializer):
class TaskSerializer(serializers.ModelSerializer): class TaskSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Task model = Task
fields = '__all__' fields = [
'id', 'name', 'interval', 'crontab', 'is_periodic',
'is_deleted', 'comment', 'created_by', 'date_created',
'versions', 'is_success', 'timedelta', 'assets_amount',
'date_updated', 'history_summary',
]
class AdHocSerializer(serializers.ModelSerializer): class AdHocSerializer(serializers.ModelSerializer):
......
...@@ -128,7 +128,7 @@ def hello_callback(result): ...@@ -128,7 +128,7 @@ def hello_callback(result):
@shared_task @shared_task
def add(a, b): def add(a, b):
time.sleep(5) time.sleep(5)
return max(b) return a + b
@shared_task @shared_task
......
{% extends '_base_list.html' %} {% extends '_base_list.html' %}
{% load i18n %} {% load i18n static %}
{% load static %} {% block table_search %}{% endblock %}
{% block table_container %}
{% block content_left_head %} <table class="table table-striped table-bordered table-hover " id="task_list_table">
{# <div class="uc pull-left m-r-5"><a class="btn btn-sm btn-primary btn-create-asset"> {% trans "Create task" %} </a></div>#} <thead>
{% endblock %} <tr>
<th class="text-center">
<input id="" type="checkbox" class="ipt_check_all">
{% block table_search %} </th>
<form id="search_form" method="get" action="" class="pull-right form-inline"> <th class="text-left">{% trans 'Name' %}</th>
<div class="input-group">
<input type="text" class="form-control input-sm" name="keyword" placeholder="{% trans 'Search' %}" value="{{ keyword }}">
</div>
<div class="input-group">
<div class="input-group-btn">
<button id='search_btn' type="submit" class="btn btn-sm btn-primary">
{% trans "Search" %}
</button>
</div>
</div>
</form>
{% endblock %}
{% block table_head %}
<th class="text-center"></th>
<th class="text-center">{% trans 'Name' %}</th>
<th class="text-center">{% trans 'Run times' %}</th> <th class="text-center">{% trans 'Run times' %}</th>
<th class="text-center">{% trans 'Versions' %}</th> <th class="text-center">{% trans 'Versions' %}</th>
<th class="text-center">{% trans 'Hosts' %}</th> <th class="text-center">{% trans 'Hosts' %}</th>
...@@ -32,70 +16,72 @@ ...@@ -32,70 +16,72 @@
<th class="text-center">{% trans 'Date' %}</th> <th class="text-center">{% trans 'Date' %}</th>
<th class="text-center">{% trans 'Time' %}</th> <th class="text-center">{% trans 'Time' %}</th>
<th class="text-center">{% trans 'Action' %}</th> <th class="text-center">{% trans 'Action' %}</th>
{% endblock %}
{% block table_body %}
{% for object in task_list %}
<tr class="gradeX">
<td class="text-center"><input type="checkbox" class="cbx-term"> </td>
<td class="text-left"><a href="{% url 'ops:task-detail' pk=object.id %}">{{ object.name }}</a></td>
<td class="text-center">
<span class="text-danger">{{ object.history_summary.failed }}</span>/<span class="text-navy">{{ object.history_summary.success}}</span>/{{ object.history_summary.total}}
</td>
<td class="text-center">{{ object.adhoc.all | length}}</td>
<td class="text-center">{{ object.latest_adhoc.hosts | length}}</td>
<td class="text-center">
{% if object.latest_history %}
{% if object.latest_history.is_success %}
<i class="fa fa-check text-navy"></i>
{% else %}
<i class="fa fa-times text-danger"></i>
{% endif %}
{% endif %}
</td>
<td class="text-center">{{ object.latest_history.date_start }}</td>
<td class="text-center">
{% if object.latest_history %}
{{ object.latest_history.timedelta|floatformat }} s
{% endif %}
</td>
<td class="text-center">
<a data-uid="{{ object.id }}" class="btn btn-xs btn-primary btn-run">{% trans "Run" %}</a>
<a data-uid="{{ object.id }}" class="btn btn-xs btn-danger btn-del">{% trans "Delete" %}</a>
</td>
</tr> </tr>
{% endfor %} </thead>
</table>
{% endblock %} {% endblock %}
{% block content_bottom_left %}{% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
<script> <script>
$(document).ready(function() { $(document).ready(function () {
$('table').DataTable({ var options = {
"searching": false, ele: $('#task_list_table'),
"paging": false, buttons: [],
"bInfo" : false, columnDefs: [
"order": [], {targets: 1, createdCell: function (td, cellData, rowData) {
"columnDefs": [ cellData = htmlEscape(cellData);
{ "targets": 0, "orderable": false }, var innerHtml = '<a href="{% url "ops:task-detail" pk=DEFAULT_PK %}">' + cellData + '</a>'
{ "targets": 4, "orderable": false }, innerHtml = innerHtml.replace('{{ DEFAULT_PK }}', rowData.id);
{ "targets": 5, "orderable": false }, $(td).html(innerHtml);
{ "targets": 8, "orderable": false } }},
] {targets: 2, createdCell: function (td, cellData) {
}); var innerHtml = '<span class="text-danger">failed</span>/<span class="text-navy">success</span>/total';
$('.select2').select2({ if (cellData) {
dropdownAutoWidth : true, innerHtml = innerHtml.replace('failed', cellData.failed)
width: 'auto' .replace('success', cellData.success)
}); .replace('total', cellData.total);
$('#date .input-daterange').datepicker({ $(td).html(innerHtml);
format: "yyyy-mm-dd", } else {
todayBtn: "linked", $(td).html('')
keyboardNavigation: false, }
forceParse: false, }},
calendarWeeks: true, {targets: 5, createdCell: function (td, cellData) {
autoclose: true var successBtn = '<i class="fa fa-check text-navy"></i>';
}); var failedBtn = '<i class="fa fa-times text-danger"></i>';
if (cellData) {
$(td).html(successBtn)
} else {
$(td).html(failedBtn)
}
}},
{targets: 6, createdCell: function (td, cellData) {
$(td).html(toSafeLocalDateStr(cellData));
}},
{targets: 7, createdCell: function (td, cellData) {
var delta = readableSecond(cellData);
$(td).html(delta);
}},
{
targets: 8,
createdCell: function (td, cellData, rowData) {
var runBtn = '<a data-uid="ID" class="btn btn-xs btn-primary btn-run">{% trans "Run" %}</a> '.replace('ID', cellData);
var delBtn = '<a data-uid="ID" class="btn btn-xs btn-danger btn-del">{% trans "Delete" %}</a>'.replace('ID', cellData);
$(td).html(runBtn + delBtn)
}
}
],
ajax_url: '{% url "api-ops:task-list" %}',
columns: [
{data: "id"}, {data: "name", className: "text-left"}, {data: "history_summary", orderable: false},
{data: "versions", orderable: false}, {data: "assets_amount", orderable: false},
{data: "is_success", orderable: false}, {data: "date_updated"},
{data: "timedelta", orderable:false}, {data: "id", orderable: false},
],
order: [],
op_html: $('#actions').html()
};
jumpserver.initServerSideDataTable(options);
}).on('click', '.btn-del', function () { }).on('click', '.btn-del', function () {
var $this = $(this); var $this = $(this);
var name = $this.closest("tr").find(":nth-child(2)").children('a').html(); var name = $this.closest("tr").find(":nth-child(2)").children('a').html();
...@@ -122,7 +108,6 @@ $(document).ready(function() { ...@@ -122,7 +108,6 @@ $(document).ready(function() {
success: success, success: success,
success_message: "{% trans 'Task start: ' %}" + " " + name success_message: "{% trans 'Task start: ' %}" + " " + name
}); });
}) })
</script> </script>
{% endblock %} {% endblock %}
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.conf import settings from django.conf import settings
from django.views.generic import ListView, DetailView from django.views.generic import ListView, DetailView, TemplateView
from common.mixins import DatetimeSearchMixin from common.mixins import DatetimeSearchMixin
from common.permissions import PermissionsMixin, IsOrgAdmin from common.permissions import PermissionsMixin, IsOrgAdmin
...@@ -17,7 +17,7 @@ __all__ = [ ...@@ -17,7 +17,7 @@ __all__ = [
] ]
class TaskListView(PermissionsMixin, DatetimeSearchMixin, ListView): class TaskListView(PermissionsMixin, TemplateView):
paginate_by = settings.DISPLAY_PER_PAGE paginate_by = settings.DISPLAY_PER_PAGE
model = Task model = Task
ordering = ('-date_created',) ordering = ('-date_created',)
...@@ -26,27 +26,10 @@ class TaskListView(PermissionsMixin, DatetimeSearchMixin, ListView): ...@@ -26,27 +26,10 @@ class TaskListView(PermissionsMixin, DatetimeSearchMixin, ListView):
keyword = '' keyword = ''
permission_classes = [IsOrgAdmin] permission_classes = [IsOrgAdmin]
def get_queryset(self):
queryset = super().get_queryset()
if current_org.is_real():
queryset = queryset.filter(created_by=current_org.id)
else:
queryset = queryset.filter(created_by='')
self.keyword = self.request.GET.get('keyword', '')
if self.keyword:
queryset = queryset.filter(
name__icontains=self.keyword,
)
return queryset
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = { context = {
'app': _('Ops'), 'app': _('Ops'),
'action': _('Task list'), 'action': _('Task list'),
'date_from': self.date_from,
'date_to': self.date_to,
'keyword': self.keyword,
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
......
...@@ -1139,7 +1139,10 @@ function timeOffset(a, b) { ...@@ -1139,7 +1139,10 @@ function timeOffset(a, b) {
var start = safeDate(a); var start = safeDate(a);
var end = safeDate(b); var end = safeDate(b);
var offset = (end - start) / 1000; var offset = (end - start) / 1000;
return readableSecond(offset)
}
function readableSecond(offset) {
var days = offset / 3600 / 24; var days = offset / 3600 / 24;
var hours = offset / 3600; var hours = offset / 3600;
var minutes = offset / 60; var minutes = offset / 60;
......
...@@ -114,6 +114,9 @@ ...@@ -114,6 +114,9 @@
<ul class="nav nav-second-level"> <ul class="nav nav-second-level">
<li id="task"><a href="{% url 'ops:task-list' %}">{% trans 'Task list' %}</a></li> <li id="task"><a href="{% url 'ops:task-list' %}">{% trans 'Task list' %}</a></li>
<li id="command-execution"><a href="{% url 'ops:command-execution-start' %}">{% trans 'Batch command' %}</a></li> <li id="command-execution"><a href="{% url 'ops:command-execution-start' %}">{% trans 'Batch command' %}</a></li>
{% if request.user.is_superuser %}
<li><a href="{% url 'flower-view' path='' %}" target="_blank" >{% trans 'Task monitor' %}</a></li>
{% endif %}
</ul> </ul>
</li> </li>
{% endif %} {% endif %}
......
...@@ -200,9 +200,13 @@ def is_running(s, unlink=True): ...@@ -200,9 +200,13 @@ def is_running(s, unlink=True):
def parse_service(s): def parse_service(s):
all_services = ['gunicorn', 'celery_ansible', 'celery_default', 'beat'] all_services = [
'gunicorn', 'celery_ansible', 'celery_default', 'beat', 'flower'
]
if s == 'all': if s == 'all':
return all_services return all_services
elif s == 'gunicorn':
return ['gunicorn', 'flower']
elif s == "celery": elif s == "celery":
return ["celery_ansible", "celery_default"] return ["celery_ansible", "celery_default"]
elif "," in s: elif "," in s:
...@@ -240,17 +244,19 @@ def get_start_gunicorn_kwargs(): ...@@ -240,17 +244,19 @@ def get_start_gunicorn_kwargs():
def get_start_celery_ansible_kwargs(): def get_start_celery_ansible_kwargs():
print("\n- Start Celery as Distributed Task Queue") print("\n- Start Celery as Distributed Task Queue: Ansible")
return get_start_worker_kwargs('ansible', 4) return get_start_worker_kwargs('ansible', 4)
def get_start_celery_default_kwargs(): def get_start_celery_default_kwargs():
print("\n- Start Celery as Distributed Task Queue: Celery")
return get_start_worker_kwargs('celery', 2) return get_start_worker_kwargs('celery', 2)
def get_start_worker_kwargs(queue, num): def get_start_worker_kwargs(queue, num):
# Todo: Must set this environment, otherwise not no ansible result return # Todo: Must set this environment, otherwise not no ansible result return
os.environ.setdefault('PYTHONOPTIMIZE', '1') os.environ.setdefault('PYTHONOPTIMIZE', '1')
os.environ.setdefault('ANSIBLE_FORCE_COLOR', 'True')
if os.getuid() == 0: if os.getuid() == 0:
os.environ.setdefault('C_FORCE_ROOT', '1') os.environ.setdefault('C_FORCE_ROOT', '1')
...@@ -261,6 +267,24 @@ def get_start_worker_kwargs(queue, num): ...@@ -261,6 +267,24 @@ def get_start_worker_kwargs(queue, num):
'-l', 'INFO', '-l', 'INFO',
'-c', str(num), '-c', str(num),
'-Q', queue, '-Q', queue,
'-n', '{}@%h'.format(queue)
]
return {"cmd": cmd, "cwd": APPS_DIR}
def get_start_flower_kwargs():
print("\n- Start Flower as Task Monitor")
if os.getuid() == 0:
os.environ.setdefault('C_FORCE_ROOT', '1')
cmd = [
'celery', 'flower',
'-A', 'ops',
'-l', 'INFO',
'--url_prefix=flower',
'--auto_refresh=False',
'--max_tasks=1000',
'--tasks_columns=uuid,name,args,state,received,started,runtime,worker'
] ]
return {"cmd": cmd, "cwd": APPS_DIR} return {"cmd": cmd, "cwd": APPS_DIR}
...@@ -333,6 +357,7 @@ def start_service(s): ...@@ -333,6 +357,7 @@ def start_service(s):
"celery_ansible": get_start_celery_ansible_kwargs, "celery_ansible": get_start_celery_ansible_kwargs,
"celery_default": get_start_celery_default_kwargs, "celery_default": get_start_celery_default_kwargs,
"beat": get_start_beat_kwargs, "beat": get_start_beat_kwargs,
"flower": get_start_flower_kwargs,
} }
kwargs = services_kwargs.get(s)() kwargs = services_kwargs.get(s)()
...@@ -449,7 +474,7 @@ if __name__ == '__main__': ...@@ -449,7 +474,7 @@ if __name__ == '__main__':
) )
parser.add_argument( parser.add_argument(
"service", type=str, default="all", nargs="?", "service", type=str, default="all", nargs="?",
choices=("all", "gunicorn", "celery", "beat", "celery,beat"), choices=("all", "gunicorn", "celery", "beat", "celery,beat", "flower"),
help="The service to start", help="The service to start",
) )
parser.add_argument('-d', '--daemon', nargs="?", const=1) parser.add_argument('-d', '--daemon', nargs="?", const=1)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment