mirror of https://github.com/jumpserver/jumpserver
Command (#2134)
* [Update] 任务区分org * [Update] 修改翻译 * [Update] 使用id而不是hostname * [Update] 执行命令 * [Update] 修改一些东西 * [Update] 暂存 * [Update] 用户执行命令 * [Update] 添加资产授权模块-tree * [Update] 暂时这样 * [Update] 批量命令执行 * [Update] 修改表结构 * [Update] 更新翻译 * [Update] 删除cloud模块无效中文翻译pull/2141/head
parent
d91599ffab
commit
3d13f3a17d
|
@ -1,6 +1,3 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from .user import *
|
||||
from .label import Label
|
||||
from .cluster import *
|
||||
|
|
|
@ -145,6 +145,13 @@ class Asset(OrgModelMixin):
|
|||
return True, ''
|
||||
return False, warning
|
||||
|
||||
def support_ansible(self):
|
||||
if self.platform in ("Windows", "Windows2016", "Other"):
|
||||
return False
|
||||
if self.protocol != 'ssh':
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_unixlike(self):
|
||||
if self.platform not in ("Windows", "Windows2016"):
|
||||
return True
|
||||
|
@ -257,7 +264,8 @@ class Asset(OrgModelMixin):
|
|||
from random import seed, choice
|
||||
import forgery_py
|
||||
from django.db import IntegrityError
|
||||
|
||||
from .node import Node
|
||||
nodes = list(Node.objects.all())
|
||||
seed()
|
||||
for i in range(count):
|
||||
ip = [str(i) for i in random.sample(range(255), 4)]
|
||||
|
@ -268,6 +276,11 @@ class Asset(OrgModelMixin):
|
|||
created_by='Fake')
|
||||
try:
|
||||
asset.save()
|
||||
if nodes and len(nodes) > 3:
|
||||
_nodes = random.sample(nodes, 3)
|
||||
else:
|
||||
_nodes = [Node.default_node()]
|
||||
asset.nodes.set(_nodes)
|
||||
asset.system_users = [choice(SystemUser.objects.all()) for i in range(3)]
|
||||
logger.debug('Generate fake asset : %s' % asset.ip)
|
||||
except IntegrityError:
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import uuid
|
||||
import re
|
||||
|
||||
from django.db import models
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
|
@ -35,7 +36,7 @@ class CommandFilterRule(OrgModelMixin):
|
|||
(TYPE_COMMAND, _('Command')),
|
||||
)
|
||||
|
||||
ACTION_DENY, ACTION_ALLOW = range(2)
|
||||
ACTION_DENY, ACTION_ALLOW, ACTION_UNKNOWN = range(3)
|
||||
ACTION_CHOICES = (
|
||||
(ACTION_DENY, _('Deny')),
|
||||
(ACTION_ALLOW, _('Allow')),
|
||||
|
@ -53,8 +54,34 @@ class CommandFilterRule(OrgModelMixin):
|
|||
date_updated = models.DateTimeField(auto_now=True)
|
||||
created_by = models.CharField(max_length=128, blank=True, default='', verbose_name=_('Created by'))
|
||||
|
||||
__pattern = None
|
||||
|
||||
class Meta:
|
||||
ordering = ('-priority', 'action')
|
||||
|
||||
@property
|
||||
def _pattern(self):
|
||||
if self.__pattern:
|
||||
return self.__pattern
|
||||
if self.type == 'command':
|
||||
regex = []
|
||||
for cmd in self.content.split('\r\n'):
|
||||
cmd = cmd.replace(' ', '\s+')
|
||||
regex.append(r'\b{0}\b'.format(cmd))
|
||||
self.__pattern = re.compile(r'{}'.format('|'.join(regex)))
|
||||
else:
|
||||
self.__pattern = re.compile(r'{0}'.format(self.content))
|
||||
return self.__pattern
|
||||
|
||||
def match(self, data):
|
||||
found = self._pattern.search(data)
|
||||
if not found:
|
||||
return self.ACTION_UNKNOWN, ''
|
||||
|
||||
if self.action == self.ACTION_ALLOW:
|
||||
return self.ACTION_ALLOW, found.group()
|
||||
else:
|
||||
return self.ACTION_DENY, found.group()
|
||||
|
||||
def __str__(self):
|
||||
return '{} % {}'.format(self.type, self.content)
|
||||
|
|
|
@ -173,6 +173,15 @@ class SystemUser(AssetUser):
|
|||
).distinct()
|
||||
return rules
|
||||
|
||||
def is_command_can_run(self, command):
|
||||
for rule in self.cmd_filter_rules:
|
||||
action, matched_cmd = rule.match(command)
|
||||
if action == rule.ACTION_ALLOW:
|
||||
return True, None
|
||||
elif action == rule.ACTION_DENY:
|
||||
return False, matched_cmd
|
||||
return True, None
|
||||
|
||||
@classmethod
|
||||
def get_system_user_by_id_or_cached(cls, sid):
|
||||
cached = cache.get(cls.cache_key.format(sid))
|
||||
|
|
|
@ -68,6 +68,8 @@ class NodeSerializer(serializers.ModelSerializer):
|
|||
|
||||
@staticmethod
|
||||
def get_assets_amount(obj):
|
||||
if hasattr(obj, 'assets_amount'):
|
||||
return obj.assets_amount
|
||||
return obj.get_all_assets().count()
|
||||
|
||||
@staticmethod
|
||||
|
@ -86,7 +88,7 @@ class NodeSerializer(serializers.ModelSerializer):
|
|||
|
||||
|
||||
class NodeAssetsSerializer(serializers.ModelSerializer):
|
||||
assets = serializers.PrimaryKeyRelatedField(many=True, queryset = Asset.objects.all())
|
||||
assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all())
|
||||
|
||||
class Meta:
|
||||
model = Node
|
||||
|
|
|
@ -58,7 +58,7 @@ def on_system_user_nodes_change(sender, instance=None, **kwargs):
|
|||
def on_system_user_assets_change(sender, instance=None, **kwargs):
|
||||
if instance and kwargs["action"] == "post_add":
|
||||
assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
|
||||
push_system_user_to_assets(instance, assets)
|
||||
push_system_user_to_assets.delay(instance, assets)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Asset.nodes.through)
|
||||
|
|
|
@ -4,14 +4,15 @@ import re
|
|||
import os
|
||||
|
||||
from celery import shared_task
|
||||
from ops.celery import app as celery_app
|
||||
from django.core.cache import cache
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from common.utils import get_object_or_none, capacity_convert, \
|
||||
from common.utils import capacity_convert, \
|
||||
sum_capacity, encrypt_password, get_logger
|
||||
from ops.celery.utils import register_as_period_task, after_app_shutdown_clean, \
|
||||
after_app_ready_start
|
||||
from ops.celery import app as celery_app
|
||||
from orgs.utils import set_to_root_org
|
||||
|
||||
from .models import SystemUser, AdminUser, Asset
|
||||
from . import const
|
||||
|
@ -20,34 +21,34 @@ from . import const
|
|||
FORKS = 10
|
||||
TIMEOUT = 60
|
||||
logger = get_logger(__file__)
|
||||
CACHE_MAX_TIME = 60*60*60
|
||||
CACHE_MAX_TIME = 60*60*2
|
||||
disk_pattern = re.compile(r'^hd|sd|xvd|vd')
|
||||
PERIOD_TASK = os.environ.get("PERIOD_TASK", "off")
|
||||
|
||||
|
||||
@shared_task
|
||||
def set_assets_hardware_info(result, **kwargs):
|
||||
def set_assets_hardware_info(assets, result, **kwargs):
|
||||
"""
|
||||
Using ops task run result, to update asset info
|
||||
|
||||
@shared_task must be exit, because we using it as a task callback, is must
|
||||
be a celery task also
|
||||
:param assets:
|
||||
:param result:
|
||||
:param kwargs: {task_name: ""}
|
||||
:return:
|
||||
"""
|
||||
result_raw = result[0]
|
||||
assets_updated = []
|
||||
for hostname, info in result_raw.get('ok', {}).items():
|
||||
success_result = result_raw.get('ok', {})
|
||||
|
||||
for asset in assets:
|
||||
hostname = asset.hostname
|
||||
info = success_result.get(hostname, {})
|
||||
info = info.get('setup', {}).get('ansible_facts', {})
|
||||
if not info:
|
||||
logger.error("Get asset info failed: {}".format(hostname))
|
||||
logger.error(_("Get asset info failed: {}").format(hostname))
|
||||
continue
|
||||
|
||||
asset = Asset.objects.get_object_by_fullname(hostname)
|
||||
if not asset:
|
||||
continue
|
||||
|
||||
___vendor = info.get('ansible_system_vendor', 'Unknown')
|
||||
___model = info.get('ansible_product_name', 'Unknown')
|
||||
___sn = info.get('ansible_product_serial', 'Unknown')
|
||||
|
@ -94,34 +95,43 @@ def update_assets_hardware_info_util(assets, task_name=None):
|
|||
from ops.utils import update_or_create_ansible_task
|
||||
if task_name is None:
|
||||
task_name = _("Update some assets hardware info")
|
||||
# task_name = _("更新资产硬件信息")
|
||||
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
|
||||
hostname_list = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
|
||||
if not hostname_list:
|
||||
logger.info("Not hosts get, may be asset is not active or not unixlike platform")
|
||||
hosts = []
|
||||
for asset in assets:
|
||||
if not asset.is_active:
|
||||
msg = _("Asset has been disabled, skipped: {}").format(asset)
|
||||
logger.info(msg)
|
||||
continue
|
||||
if not asset.support_ansible():
|
||||
msg = _("Asset may not be support ansible, skipped: {}").format(asset)
|
||||
logger.info(msg)
|
||||
continue
|
||||
hosts.append(asset)
|
||||
if not hosts:
|
||||
logger.info(_("No assets matched, stop task"))
|
||||
return {}
|
||||
created_by = str(assets[0].org_id)
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name, hosts=hostname_list, tasks=tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System',
|
||||
task_name, hosts=hosts, tasks=tasks, created_by=created_by,
|
||||
pattern='all', options=const.TASK_OPTIONS, run_as_admin=True,
|
||||
)
|
||||
result = task.run()
|
||||
# Todo: may be somewhere using
|
||||
# Manual run callback function
|
||||
set_assets_hardware_info(result)
|
||||
set_assets_hardware_info(assets, result)
|
||||
return result
|
||||
|
||||
|
||||
@shared_task
|
||||
def update_asset_hardware_info_manual(asset):
|
||||
task_name = _("Update asset hardware info")
|
||||
task_name = _("Update asset hardware info: {}").format(asset.hostname)
|
||||
# task_name = _("更新资产硬件信息")
|
||||
return update_assets_hardware_info_util([asset], task_name=task_name)
|
||||
return update_assets_hardware_info_util(
|
||||
[asset], task_name=task_name
|
||||
)
|
||||
|
||||
|
||||
@celery_app.task
|
||||
@register_as_period_task(interval=3600)
|
||||
@after_app_ready_start
|
||||
@after_app_shutdown_clean
|
||||
@shared_task
|
||||
def update_assets_hardware_info_period():
|
||||
"""
|
||||
Update asset hardware period task
|
||||
|
@ -132,25 +142,28 @@ def update_assets_hardware_info_period():
|
|||
return
|
||||
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
from orgs.models import Organization
|
||||
orgs = Organization.objects.all().values_list('id', flat=True)
|
||||
orgs.append('')
|
||||
task_name = _("Update assets hardware info period")
|
||||
# task_name = _("定期更新资产硬件信息")
|
||||
hostname_list = [
|
||||
asset.fullname for asset in Asset.objects.all()
|
||||
if asset.is_active and asset.is_unixlike()
|
||||
]
|
||||
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
|
||||
|
||||
# Only create, schedule by celery beat
|
||||
update_or_create_ansible_task(
|
||||
task_name, hosts=hostname_list, tasks=tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System',
|
||||
interval=60*60*24, is_periodic=True, callback=set_assets_hardware_info.name,
|
||||
)
|
||||
# for org_id in orgs:
|
||||
# org_id = str(org_id)
|
||||
# hostname_list = [
|
||||
# asset for asset in Asset.objects.all()
|
||||
# if asset.is_active and asset.is_unixlike()
|
||||
# ]
|
||||
# tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
|
||||
#
|
||||
# # Only create, schedule by celery beat
|
||||
# update_or_create_ansible_task(
|
||||
# task_name, hosts=hostname_list, tasks=tasks, pattern='all',
|
||||
# options=const.TASK_OPTIONS, run_as_admin=True, created_by='System',
|
||||
# interval=60*60*24, is_periodic=True, callback=set_assets_hardware_info.name,
|
||||
# )
|
||||
|
||||
|
||||
## ADMIN USER CONNECTIVE ##
|
||||
|
||||
@shared_task
|
||||
def set_admin_user_connectability_info(result, **kwargs):
|
||||
admin_user = kwargs.get("admin_user")
|
||||
task_name = kwargs.get("task_name")
|
||||
|
@ -182,36 +195,39 @@ def test_admin_user_connectability_util(admin_user, task_name):
|
|||
from ops.utils import update_or_create_ansible_task
|
||||
|
||||
assets = admin_user.get_related_assets()
|
||||
hosts = [asset.fullname for asset in assets
|
||||
if asset.is_active and asset.is_unixlike()]
|
||||
hosts = []
|
||||
for asset in assets:
|
||||
if not asset.is_active:
|
||||
msg = _("Asset has been disabled, skipped: {}").format(asset)
|
||||
logger.info(msg)
|
||||
continue
|
||||
if not asset.support_ansible():
|
||||
msg = _("Asset may not be support ansible, skipped: {}").format(asset)
|
||||
logger.info(msg)
|
||||
continue
|
||||
hosts.append(asset)
|
||||
if not hosts:
|
||||
return
|
||||
logger.info(_("No assets matched, stop task"))
|
||||
return {}
|
||||
tasks = const.TEST_ADMIN_USER_CONN_TASKS
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System',
|
||||
options=const.TASK_OPTIONS, run_as_admin=True, created_by=admin_user.org_id,
|
||||
)
|
||||
result = task.run()
|
||||
set_admin_user_connectability_info(result, admin_user=admin_user.name)
|
||||
return result
|
||||
|
||||
|
||||
@celery_app.task
|
||||
@shared_task
|
||||
@register_as_period_task(interval=3600)
|
||||
@after_app_ready_start
|
||||
@after_app_shutdown_clean
|
||||
def test_admin_user_connectability_period():
|
||||
"""
|
||||
A period task that update the ansible task period
|
||||
"""
|
||||
if PERIOD_TASK != "on":
|
||||
logger.debug("Period task disabled, test admin user connectability pass")
|
||||
return
|
||||
|
||||
admin_users = AdminUser.objects.all()
|
||||
for admin_user in admin_users:
|
||||
task_name = _("Test admin user connectability period: {}".format(admin_user.name))
|
||||
# task_name = _("定期测试管理账号可连接性: {}".format(admin_user.name))
|
||||
task_name = _("Test admin user connectability period: {}").format(admin_user.name)
|
||||
test_admin_user_connectability_util(admin_user, task_name)
|
||||
|
||||
|
||||
|
@ -229,14 +245,25 @@ def test_asset_connectability_util(assets, task_name=None):
|
|||
if task_name is None:
|
||||
task_name = _("Test assets connectability")
|
||||
# task_name = _("测试资产可连接性")
|
||||
hosts = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
|
||||
hosts = []
|
||||
for asset in assets:
|
||||
if not asset.is_active:
|
||||
msg = _("Asset has been disabled, skip: {}").format(asset)
|
||||
logger.info(msg)
|
||||
continue
|
||||
if not asset.support_ansible():
|
||||
msg = _("Asset may not be support ansible, skip: {}").format(asset)
|
||||
logger.info(msg)
|
||||
continue
|
||||
hosts.append(asset)
|
||||
if not hosts:
|
||||
logger.info("No hosts, passed")
|
||||
logger.info(_("No assets, task stop"))
|
||||
return {}
|
||||
tasks = const.TEST_ADMIN_USER_CONN_TASKS
|
||||
created_by = assets[0].org_id
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System',
|
||||
options=const.TASK_OPTIONS, run_as_admin=True, created_by=created_by,
|
||||
)
|
||||
result = task.run()
|
||||
summary = result[1]
|
||||
|
@ -250,7 +277,8 @@ def test_asset_connectability_util(assets, task_name=None):
|
|||
|
||||
@shared_task
|
||||
def test_asset_connectability_manual(asset):
|
||||
summary = test_asset_connectability_util([asset])
|
||||
task_name = _("Test assets connectability: {}").format(asset)
|
||||
summary = test_asset_connectability_util([asset], task_name=task_name)
|
||||
|
||||
if summary.get('dark'):
|
||||
return False, summary['dark']
|
||||
|
@ -267,7 +295,7 @@ def set_system_user_connectablity_info(result, **kwargs):
|
|||
system_user = kwargs.get("system_user")
|
||||
if system_user is None:
|
||||
system_user = task_name.split(":")[-1]
|
||||
cache_key = const.SYSTEM_USER_CONN_CACHE_KEY.format(system_user)
|
||||
cache_key = const.SYSTEM_USER_CONN_CACHE_KEY.format(str(system_user.id))
|
||||
cache.set(cache_key, summary, CACHE_MAX_TIME)
|
||||
|
||||
|
||||
|
@ -281,19 +309,28 @@ def test_system_user_connectability_util(system_user, assets, task_name):
|
|||
:return:
|
||||
"""
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
# assets = system_user.get_assets()
|
||||
hosts = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
|
||||
hosts = []
|
||||
tasks = const.TEST_SYSTEM_USER_CONN_TASKS
|
||||
for asset in assets:
|
||||
if not asset.is_active:
|
||||
msg = _("Asset has been disabled, skip: {}").format(asset)
|
||||
logger.info(msg)
|
||||
continue
|
||||
if not asset.support_ansible():
|
||||
msg = _("Asset may not be support ansible, skip: {}").format(asset)
|
||||
logger.info(msg)
|
||||
continue
|
||||
hosts.append(asset)
|
||||
if not hosts:
|
||||
logger.info("No hosts, passed")
|
||||
logger.info(_("No assets matched, stop task"))
|
||||
return {}
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name, hosts=hosts, tasks=tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS,
|
||||
run_as=system_user.name, created_by="System",
|
||||
run_as=system_user, created_by=system_user.org_id,
|
||||
)
|
||||
result = task.run()
|
||||
set_system_user_connectablity_info(result, system_user=system_user.name)
|
||||
set_system_user_connectablity_info(result, system_user=system_user)
|
||||
return result
|
||||
|
||||
|
||||
|
@ -313,17 +350,13 @@ def test_system_user_connectability_a_asset(system_user, asset):
|
|||
|
||||
|
||||
@shared_task
|
||||
@register_as_period_task(interval=3600)
|
||||
@after_app_ready_start
|
||||
@after_app_shutdown_clean
|
||||
def test_system_user_connectability_period():
|
||||
if PERIOD_TASK != "on":
|
||||
logger.debug("Period task disabled, test system user connectability pass")
|
||||
return
|
||||
|
||||
system_users = SystemUser.objects.all()
|
||||
for system_user in system_users:
|
||||
task_name = _("Test system user connectability period: {}".format(system_user))
|
||||
task_name = _("Test system user connectability period: {}").format(system_user)
|
||||
# task_name = _("定期测试系统用户可连接性: {}".format(system_user))
|
||||
test_system_user_connectability_util(system_user, task_name)
|
||||
|
||||
|
@ -374,28 +407,33 @@ def get_push_system_user_tasks(system_user):
|
|||
|
||||
|
||||
@shared_task
|
||||
def push_system_user_util(system_users, assets, task_name):
|
||||
def push_system_user_util(system_user, assets, task_name):
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
tasks = []
|
||||
for system_user in system_users:
|
||||
if not system_user.is_need_push():
|
||||
msg = "push system user `{}` passed, may be not auto push or ssh " \
|
||||
"protocol is not ssh".format(system_user.name)
|
||||
if not system_user.is_need_push():
|
||||
msg = _("Push system user task skip, auto push not enable or "
|
||||
"protocol is not ssh: {}").format(system_user.name)
|
||||
logger.info(msg)
|
||||
return
|
||||
|
||||
tasks = get_push_system_user_tasks(system_user)
|
||||
hosts = []
|
||||
for asset in assets:
|
||||
if not asset.is_active:
|
||||
msg = _("Asset has been disabled, skip: {}").format(asset)
|
||||
logger.info(msg)
|
||||
continue
|
||||
tasks.extend(get_push_system_user_tasks(system_user))
|
||||
|
||||
if not tasks:
|
||||
logger.info("Not tasks, passed")
|
||||
return {}
|
||||
|
||||
hosts = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
|
||||
if not asset.support_ansible():
|
||||
msg = _("Asset may not be support ansible, skip: {}").format(asset)
|
||||
logger.info(msg)
|
||||
continue
|
||||
hosts.append(asset)
|
||||
if not hosts:
|
||||
logger.info("Not hosts, passed")
|
||||
logger.info(_("No assets matched, stop task"))
|
||||
return {}
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System'
|
||||
options=const.TASK_OPTIONS, run_as_admin=True,
|
||||
created_by=system_user.org_id,
|
||||
)
|
||||
return task.run()
|
||||
|
||||
|
@ -403,24 +441,22 @@ def push_system_user_util(system_users, assets, task_name):
|
|||
@shared_task
|
||||
def push_system_user_to_assets_manual(system_user):
|
||||
assets = system_user.get_assets()
|
||||
# task_name = "推送系统用户到入资产: {}".format(system_user.name)
|
||||
task_name = _("Push system users to assets: {}").format(system_user.name)
|
||||
return push_system_user_util([system_user], assets, task_name=task_name)
|
||||
return push_system_user_util(system_user, assets, task_name=task_name)
|
||||
|
||||
|
||||
@shared_task
|
||||
def push_system_user_a_asset_manual(system_user, asset):
|
||||
task_name = _("Push system users to asset: {} => {}").format(
|
||||
system_user.name, asset.fullname
|
||||
system_user.name, asset
|
||||
)
|
||||
return push_system_user_util([system_user], [asset], task_name=task_name)
|
||||
return push_system_user_util(system_user, [asset], task_name=task_name)
|
||||
|
||||
|
||||
@shared_task
|
||||
def push_system_user_to_assets(system_user, assets):
|
||||
# task_name = _("推送系统用户到入资产: {}").format(system_user.name)
|
||||
task_name = _("Push system users to assets: {}").format(system_user.name)
|
||||
return push_system_user_util.delay([system_user], assets, task_name)
|
||||
return push_system_user_util(system_user, assets, task_name)
|
||||
|
||||
|
||||
# @shared_task
|
||||
|
|
|
@ -452,7 +452,7 @@ $(document).ready(function(){
|
|||
$.each(rows, function (index, obj) {
|
||||
assets.push(obj.id)
|
||||
});
|
||||
var _node_id = current_node ? current_node : null;
|
||||
var _node_id = current_node ? current_node.node_id : null;
|
||||
$.ajax({
|
||||
url: "{% url "assets:asset-export" %}",
|
||||
method: 'POST',
|
||||
|
|
|
@ -71,7 +71,6 @@ function initTable() {
|
|||
} else {
|
||||
inited = true;
|
||||
}
|
||||
console.log("init table")
|
||||
url = "{% url 'api-perms:my-assets' %}";
|
||||
var options = {
|
||||
ele: $('#user_assets_table'),
|
||||
|
@ -108,7 +107,8 @@ function initTable() {
|
|||
|
||||
function onSelected(event, treeNode) {
|
||||
url = '{% url "api-perms:my-node-assets" node_id=DEFAULT_PK %}';
|
||||
url = url.replace("{{ DEFAULT_PK }}", treeNode.node_id);
|
||||
var node_id = treeNode.meta.node.id;
|
||||
url = url.replace("{{ DEFAULT_PK }}", node_id);
|
||||
setCookie('node_selected', treeNode.id);
|
||||
asset_table.ajax.url(url);
|
||||
asset_table.ajax.reload();
|
||||
|
@ -131,21 +131,10 @@ function initTree() {
|
|||
};
|
||||
|
||||
var zNodes = [];
|
||||
$.get("{% url 'api-perms:my-nodes' %}", function(data, status){
|
||||
$.each(data, function (index, value) {
|
||||
value["node_id"] = value["id"];
|
||||
value["id"] = value["tree_id"];
|
||||
if (value["tree_id"] !== value["tree_parent"]) {
|
||||
value["pId"] = value["tree_parent"];
|
||||
}
|
||||
value["isParent"] = value["is_node"];
|
||||
value['name'] = value['value'];
|
||||
});
|
||||
$.get("{% url 'api-perms:my-nodes-assets-as-tree' %}?show_assets=0", function(data, status){
|
||||
zNodes = data;
|
||||
$.fn.zTree.init($("#assetTree"), setting, zNodes);
|
||||
zTree = $.fn.zTree.getZTreeObj("assetTree");
|
||||
var root = zTree.getNodes()[0];
|
||||
zTree.expandNode(root);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,11 @@ from .models import Asset, SystemUser, Label
|
|||
|
||||
|
||||
def get_assets_by_id_list(id_list):
|
||||
return Asset.objects.filter(id__in=id_list)
|
||||
return Asset.objects.filter(id__in=id_list).filter(is_active=True)
|
||||
|
||||
|
||||
def get_system_users_by_id_list(id_list):
|
||||
return SystemUser.objects.filter(id__in=id_list)
|
||||
|
||||
|
||||
def get_assets_by_fullname_list(hostname_list):
|
||||
|
@ -21,6 +25,11 @@ def get_system_user_by_name(name):
|
|||
return system_user
|
||||
|
||||
|
||||
def get_system_user_by_id(id):
|
||||
system_user = get_object_or_none(SystemUser, id=id)
|
||||
return system_user
|
||||
|
||||
|
||||
class LabelFilter:
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
|
|
@ -216,6 +216,7 @@ class AssetExportView(LoginRequiredMixin, View):
|
|||
return HttpResponse('Json object not valid', status=400)
|
||||
|
||||
if not assets_id:
|
||||
print(node_id)
|
||||
node = get_object_or_none(Node, id=node_id) if node_id else Node.root()
|
||||
assets = node.get_all_assets()
|
||||
for asset in assets:
|
||||
|
|
|
@ -13,4 +13,5 @@ urlpatterns = [
|
|||
path('ftp-log/', views.FTPLogListView.as_view(), name='ftp-log-list'),
|
||||
path('operate-log/', views.OperateLogListView.as_view(), name='operate-log-list'),
|
||||
path('password-change-log/', views.PasswordChangeLogList.as_view(), name='password-change-log-list'),
|
||||
path('command-execution-log/', views.CommandExecutionListView.as_view(), name='command-execution-log-list'),
|
||||
]
|
||||
|
|
|
@ -7,6 +7,8 @@ from common.mixins import DatetimeSearchMixin
|
|||
from common.permissions import AdminUserRequiredMixin
|
||||
|
||||
from orgs.utils import current_org
|
||||
from ops.views import CommandExecutionListView as UserCommandExecutionListView
|
||||
from users.models import User
|
||||
from .models import FTPLog, OperateLog, PasswordChangeLog, UserLoginLog
|
||||
|
||||
|
||||
|
@ -187,7 +189,7 @@ class LoginLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
|
|||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Users'),
|
||||
'app': _('Audits'),
|
||||
'action': _('Login log'),
|
||||
'date_from': self.date_from,
|
||||
'date_to': self.date_to,
|
||||
|
@ -196,4 +198,35 @@ class LoginLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
|
|||
'user_list': self.get_org_users(),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class CommandExecutionListView(UserCommandExecutionListView):
|
||||
user_id = None
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = self._get_queryset()
|
||||
self.user_id = self.request.GET.get('user')
|
||||
org_users = self.get_user_list()
|
||||
if self.user_id:
|
||||
queryset = queryset.filter(user=self.user_id)
|
||||
else:
|
||||
queryset = queryset.filter(user__in=org_users)
|
||||
return queryset
|
||||
|
||||
def get_user_list(self):
|
||||
users = current_org.get_org_users()
|
||||
return users
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Audits'),
|
||||
'action': _('Command execution list'),
|
||||
'date_from': self.date_from,
|
||||
'date_to': self.date_to,
|
||||
'user_list': self.get_user_list(),
|
||||
'keyword': self.keyword,
|
||||
'user_id': self.user_id,
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
|
|
@ -65,6 +65,8 @@ def refresh_all_settings_on_django_ready(sender, **kwargs):
|
|||
|
||||
@receiver(pre_save, dispatch_uid="my_unique_identifier")
|
||||
def on_create_set_created_by(sender, instance=None, **kwargs):
|
||||
if getattr(instance, '_ignore_auto_created_by', False) is True:
|
||||
return
|
||||
if hasattr(instance, 'created_by') and not instance.created_by:
|
||||
if current_request and current_request.user.is_authenticated:
|
||||
instance.created_by = current_request.user.name
|
||||
|
|
|
@ -111,3 +111,13 @@ def sort(data):
|
|||
@register.filter
|
||||
def subtract(value, arg):
|
||||
return value - arg
|
||||
|
||||
|
||||
@register.filter
|
||||
def state_show(state):
|
||||
success = '<i class ="fa fa-check text-navy"> </i>'
|
||||
failed = '<i class ="fa fa-times text-danger"> </i>'
|
||||
if state:
|
||||
return success
|
||||
else:
|
||||
return failed
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class TreeNode:
|
||||
id = ""
|
||||
name = ""
|
||||
comment = ""
|
||||
title = ""
|
||||
isParent = False
|
||||
pId = ""
|
||||
open = False
|
||||
iconSkin = ""
|
||||
meta = {}
|
||||
|
||||
_tree = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
@classmethod
|
||||
def root(cls):
|
||||
return cls(id="#", name='Root', title='Root', isParent=True, open=True)
|
||||
|
||||
def get_parent(self):
|
||||
return self._tree.get_node(self.pId)
|
||||
|
||||
def get_parents(self):
|
||||
parent = self.get_parent()
|
||||
if parent == self._tree.root:
|
||||
return []
|
||||
parents = [parent]
|
||||
parents.extend(parent.get_parents())
|
||||
return parents
|
||||
|
||||
def add_child(self, child):
|
||||
self._tree.add_node(child, self)
|
||||
|
||||
def __str__(self):
|
||||
return '<{}: {}>'.format(self.id, self.name)
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
def __gt__(self, other):
|
||||
if self.isParent and not other.isParent:
|
||||
return False
|
||||
return self.id > other.id
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.id == other.id
|
||||
|
||||
def __lt__(self, other):
|
||||
if self.isParent and not other.isParent:
|
||||
return True
|
||||
return self.id < other.id
|
||||
|
||||
|
||||
class Tree:
|
||||
def __init__(self):
|
||||
self.nodes = {}
|
||||
self.root = TreeNode.root()
|
||||
self.root._tree = self
|
||||
|
||||
def add_node(self, node, parent=None):
|
||||
node._tree = self
|
||||
|
||||
if not parent:
|
||||
parent = self.root
|
||||
if parent.id not in self.nodes and parent != self.root:
|
||||
raise ValueError("Parent not in tree")
|
||||
elif node in parent.get_parents():
|
||||
raise ValueError("Parent must not be node parent")
|
||||
node.pId = parent.id
|
||||
parent.isParent = True
|
||||
self.nodes[node.id] = node
|
||||
|
||||
def get_nodes(self):
|
||||
return sorted(self.nodes.values())
|
||||
|
||||
def get_node(self, tid):
|
||||
return self.nodes.get(tid) or TreeNode.root()
|
||||
|
||||
|
||||
class TreeNodeSerializer(serializers.Serializer):
|
||||
id = serializers.CharField(max_length=128)
|
||||
name = serializers.CharField(max_length=128)
|
||||
title = serializers.CharField(max_length=128)
|
||||
pId = serializers.CharField(max_length=128)
|
||||
isParent = serializers.BooleanField(default=False)
|
||||
open = serializers.BooleanField(default=False)
|
||||
iconSkin = serializers.CharField(max_length=128, allow_blank=True)
|
||||
meta = serializers.JSONField()
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
@ -1,18 +1,16 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
|
||||
import sys
|
||||
import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.plugins.callback import CallbackBase
|
||||
from ansible.plugins.callback.default import CallbackModule
|
||||
|
||||
from .display import TeeObj
|
||||
from ansible.plugins.callback.minimal import CallbackModule as CMDCallBackModule
|
||||
|
||||
|
||||
class AdHocResultCallback(CallbackModule):
|
||||
"""
|
||||
Task result Callback
|
||||
"""
|
||||
def __init__(self, display=None, options=None, file_obj=None):
|
||||
class CallbackMixin:
|
||||
def __init__(self, display=None):
|
||||
# result_raw example: {
|
||||
# "ok": {"hostname": {"task_name": {},...},..},
|
||||
# "failed": {"hostname": {"task_name": {}..}, ..},
|
||||
|
@ -20,63 +18,129 @@ class AdHocResultCallback(CallbackModule):
|
|||
# "skipped": {"hostname": {"task_name": {}, ..}, ..},
|
||||
# }
|
||||
# results_summary example: {
|
||||
# "contacted": {"hostname",...},
|
||||
# "contacted": {"hostname": {"task_name": {}}, "hostname": {}},
|
||||
# "dark": {"hostname": {"task_name": {}, "task_name": {}},...,},
|
||||
# "success": True
|
||||
# }
|
||||
self.results_raw = dict(ok={}, failed={}, unreachable={}, skipped={})
|
||||
self.results_summary = dict(contacted=[], dark={})
|
||||
self.results_raw = dict(
|
||||
ok=defaultdict(dict),
|
||||
failed=defaultdict(dict),
|
||||
unreachable=defaultdict(dict),
|
||||
skippe=defaultdict(dict),
|
||||
)
|
||||
self.results_summary = dict(
|
||||
contacted=defaultdict(dict),
|
||||
dark=defaultdict(dict),
|
||||
success=True
|
||||
)
|
||||
self.results = {
|
||||
'raw': self.results_raw,
|
||||
'summary': self.results_summary,
|
||||
}
|
||||
super().__init__()
|
||||
if file_obj is not None:
|
||||
sys.stdout = TeeObj(file_obj)
|
||||
if display:
|
||||
self._display = display
|
||||
self._display.columns = 79
|
||||
|
||||
def gather_result(self, t, res):
|
||||
self._clean_results(res._result, res._task.action)
|
||||
host = res._host.get_name()
|
||||
task_name = res.task_name
|
||||
task_result = res._result
|
||||
def display(self, msg):
|
||||
self._display.display(msg)
|
||||
|
||||
if self.results_raw[t].get(host):
|
||||
self.results_raw[t][host][task_name] = task_result
|
||||
else:
|
||||
self.results_raw[t][host] = {task_name: task_result}
|
||||
def gather_result(self, t, result):
|
||||
self._clean_results(result._result, result._task.action)
|
||||
host = result._host.get_name()
|
||||
task_name = result.task_name
|
||||
task_result = result._result
|
||||
|
||||
self.results_raw[t][host][task_name] = task_result
|
||||
self.clean_result(t, host, task_name, task_result)
|
||||
|
||||
|
||||
class AdHocResultCallback(CallbackMixin, CallbackModule, CMDCallBackModule):
|
||||
"""
|
||||
Task result Callback
|
||||
"""
|
||||
def clean_result(self, t, host, task_name, task_result):
|
||||
contacted = self.results_summary["contacted"]
|
||||
dark = self.results_summary["dark"]
|
||||
if t in ("ok", "skipped") and host not in dark:
|
||||
if host not in contacted:
|
||||
contacted.append(host)
|
||||
else:
|
||||
if dark.get(host):
|
||||
dark[host][task_name] = task_result.values
|
||||
|
||||
if task_result.get('rc') is not None:
|
||||
cmd = task_result.get('cmd')
|
||||
if isinstance(cmd, list):
|
||||
cmd = " ".join(cmd)
|
||||
else:
|
||||
dark[host] = {task_name: task_result}
|
||||
if host in contacted:
|
||||
contacted.remove(host)
|
||||
cmd = str(cmd)
|
||||
detail = {
|
||||
'cmd': cmd,
|
||||
'stderr': task_result.get('stderr'),
|
||||
'stdout': task_result.get('stdout'),
|
||||
'rc': task_result.get('rc'),
|
||||
'delta': task_result.get('delta'),
|
||||
'msg': task_result.get('msg', '')
|
||||
}
|
||||
else:
|
||||
detail = {
|
||||
"changed": task_result.get('changed', False),
|
||||
"msg": task_result.get('msg', '')
|
||||
}
|
||||
|
||||
if t in ("ok", "skipped"):
|
||||
contacted[host][task_name] = detail
|
||||
else:
|
||||
dark[host][task_name] = detail
|
||||
|
||||
def v2_runner_on_failed(self, result, ignore_errors=False):
|
||||
self.results_summary['success'] = False
|
||||
self.gather_result("failed", result)
|
||||
super().v2_runner_on_failed(result, ignore_errors=ignore_errors)
|
||||
|
||||
if result._task.action in C.MODULE_NO_JSON:
|
||||
CMDCallBackModule.v2_runner_on_failed(self,
|
||||
result, ignore_errors=ignore_errors
|
||||
)
|
||||
else:
|
||||
super().v2_runner_on_failed(
|
||||
result, ignore_errors=ignore_errors
|
||||
)
|
||||
|
||||
def v2_runner_on_ok(self, result):
|
||||
self.gather_result("ok", result)
|
||||
super().v2_runner_on_ok(result)
|
||||
if result._task.action in C.MODULE_NO_JSON:
|
||||
CMDCallBackModule.v2_runner_on_ok(self, result)
|
||||
else:
|
||||
super().v2_runner_on_ok(result)
|
||||
|
||||
def v2_runner_on_skipped(self, result):
|
||||
self.gather_result("skipped", result)
|
||||
super().v2_runner_on_skipped(result)
|
||||
|
||||
def v2_runner_on_unreachable(self, result):
|
||||
self.results_summary['success'] = False
|
||||
self.gather_result("unreachable", result)
|
||||
super().v2_runner_on_unreachable(result)
|
||||
|
||||
def on_playbook_start(self, name):
|
||||
date_start = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
self.display(
|
||||
"{} Start task: {}\r\n".format(date_start, name)
|
||||
)
|
||||
|
||||
def on_playbook_end(self, name):
|
||||
date_finished = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
self.display(
|
||||
"{} Task finish\r\n".format(date_finished)
|
||||
)
|
||||
|
||||
def display_skipped_hosts(self):
|
||||
pass
|
||||
|
||||
def display_ok_hosts(self):
|
||||
pass
|
||||
|
||||
|
||||
class CommandResultCallback(AdHocResultCallback):
|
||||
"""
|
||||
Command result callback
|
||||
"""
|
||||
def __init__(self, display=None):
|
||||
def __init__(self, display=None, **kwargs):
|
||||
# results_command: {
|
||||
# "cmd": "",
|
||||
# "stderr": "",
|
||||
|
|
|
@ -17,7 +17,7 @@ from common.utils import get_logger
|
|||
from .exceptions import AnsibleError
|
||||
|
||||
|
||||
__all__ = ["AdHocRunner", "PlayBookRunner"]
|
||||
__all__ = ["AdHocRunner", "PlayBookRunner", "CommandRunner"]
|
||||
C.HOST_KEY_CHECKING = False
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
@ -45,7 +45,7 @@ def get_default_options():
|
|||
listtasks=False,
|
||||
listhosts=False,
|
||||
syntax=False,
|
||||
timeout=60,
|
||||
timeout=30,
|
||||
connection='ssh',
|
||||
module_path='',
|
||||
forks=10,
|
||||
|
@ -145,7 +145,7 @@ class AdHocRunner:
|
|||
)
|
||||
|
||||
def get_result_callback(self, file_obj=None):
|
||||
return self.__class__.results_callback_class(file_obj=file_obj)
|
||||
return self.__class__.results_callback_class()
|
||||
|
||||
@staticmethod
|
||||
def check_module_args(module_name, module_args=''):
|
||||
|
@ -177,17 +177,16 @@ class AdHocRunner:
|
|||
options = self.__class__.default_options
|
||||
return options
|
||||
|
||||
def run(self, tasks, pattern, play_name='Ansible Ad-hoc', gather_facts='no', file_obj=None):
|
||||
def run(self, tasks, pattern, play_name='Ansible Ad-hoc', gather_facts='no'):
|
||||
"""
|
||||
:param tasks: [{'action': {'module': 'shell', 'args': 'ls'}, ...}, ]
|
||||
:param pattern: all, *, or others
|
||||
:param play_name: The play name
|
||||
:param gather_facts:
|
||||
:param file_obj: logging to file_obj
|
||||
:return:
|
||||
"""
|
||||
self.check_pattern(pattern)
|
||||
self.results_callback = self.get_result_callback(file_obj)
|
||||
self.results_callback = self.get_result_callback()
|
||||
cleaned_tasks = self.clean_tasks(tasks)
|
||||
|
||||
play_source = dict(
|
||||
|
@ -211,10 +210,6 @@ class AdHocRunner:
|
|||
stdout_callback=self.results_callback,
|
||||
passwords=self.options.passwords,
|
||||
)
|
||||
print("Get matched hosts: {}".format(
|
||||
self.inventory.get_matched_hosts(pattern)
|
||||
))
|
||||
|
||||
try:
|
||||
tqm.run(play)
|
||||
return self.results_callback
|
||||
|
@ -229,16 +224,14 @@ class CommandRunner(AdHocRunner):
|
|||
results_callback_class = CommandResultCallback
|
||||
modules_choices = ('shell', 'raw', 'command', 'script')
|
||||
|
||||
def execute(self, cmd, pattern, module=None):
|
||||
def execute(self, cmd, pattern, module='shell'):
|
||||
if module and module not in self.modules_choices:
|
||||
raise AnsibleError("Module should in {}".format(self.modules_choices))
|
||||
else:
|
||||
module = "shell"
|
||||
|
||||
tasks = [
|
||||
{"action": {"module": module, "args": cmd}}
|
||||
]
|
||||
hosts = self.inventory.get_hosts(pattern=pattern)
|
||||
name = "Run command {} on {}".format(cmd, ", ".join([host.name for host in hosts]))
|
||||
name = "Run command {} on {}'s hosts".format(cmd, len(hosts))
|
||||
return self.run(tasks, pattern, play_name=name)
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from .adhoc import *
|
||||
from .celery import *
|
||||
from .command import *
|
|
@ -1,26 +1,32 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
import uuid
|
||||
import os
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import viewsets, generics
|
||||
from rest_framework.views import Response
|
||||
|
||||
from common.permissions import IsOrgAdmin
|
||||
from .models import Task, AdHoc, AdHocRunHistory, CeleryTask
|
||||
from .serializers import TaskSerializer, AdHocSerializer, \
|
||||
from orgs.utils import current_org
|
||||
from ..models import Task, AdHoc, AdHocRunHistory
|
||||
from ..serializers import TaskSerializer, AdHocSerializer, \
|
||||
AdHocRunHistorySerializer
|
||||
from .tasks import run_ansible_task
|
||||
from ..tasks import run_ansible_task
|
||||
|
||||
__all__ = [
|
||||
'TaskViewSet', 'TaskRun', 'AdHocViewSet', 'AdHocRunHistoryViewSet'
|
||||
]
|
||||
|
||||
|
||||
class TaskViewSet(viewsets.ModelViewSet):
|
||||
queryset = Task.objects.all()
|
||||
serializer_class = TaskSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
# label = None
|
||||
# help_text = ''
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
if current_org:
|
||||
queryset = queryset.filter(created_by=current_org.id)
|
||||
return queryset
|
||||
|
||||
|
||||
class TaskRun(generics.RetrieveAPIView):
|
||||
|
@ -47,7 +53,7 @@ class AdHocViewSet(viewsets.ModelViewSet):
|
|||
return self.queryset
|
||||
|
||||
|
||||
class AdHocRunHistorySet(viewsets.ModelViewSet):
|
||||
class AdHocRunHistoryViewSet(viewsets.ModelViewSet):
|
||||
queryset = AdHocRunHistory.objects.all()
|
||||
serializer_class = AdHocRunHistorySerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
@ -66,28 +72,6 @@ class AdHocRunHistorySet(viewsets.ModelViewSet):
|
|||
return self.queryset
|
||||
|
||||
|
||||
class CeleryTaskLogApi(generics.RetrieveAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
buff_size = 1024 * 10
|
||||
end = False
|
||||
queryset = CeleryTask.objects.all()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
mark = request.query_params.get("mark") or str(uuid.uuid4())
|
||||
task = self.get_object()
|
||||
log_path = task.full_log_path
|
||||
|
||||
if not log_path or not os.path.isfile(log_path):
|
||||
return Response({"data": _("Waiting ...")}, status=203)
|
||||
|
||||
with open(log_path, 'r') as f:
|
||||
offset = cache.get(mark, 0)
|
||||
f.seek(offset)
|
||||
data = f.read(self.buff_size).replace('\n', '\r\n')
|
||||
mark = str(uuid.uuid4())
|
||||
cache.set(mark, f.tell(), 5)
|
||||
|
||||
if data == '' and task.is_finished():
|
||||
self.end = True
|
||||
return Response({"data": data, 'end': self.end, 'mark': mark})
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import uuid
|
||||
import os
|
||||
|
||||
from celery.result import AsyncResult
|
||||
from django.core.cache import cache
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import generics
|
||||
from rest_framework.views import Response
|
||||
|
||||
from common.permissions import IsOrgAdmin, IsValidUser
|
||||
from ..models import CeleryTask
|
||||
from ..serializers import CeleryResultSerializer
|
||||
|
||||
|
||||
__all__ = ['CeleryTaskLogApi', 'CeleryResultApi']
|
||||
|
||||
|
||||
class CeleryTaskLogApi(generics.RetrieveAPIView):
|
||||
permission_classes = (IsValidUser,)
|
||||
buff_size = 1024 * 10
|
||||
end = False
|
||||
queryset = CeleryTask.objects.all()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
mark = request.query_params.get("mark") or str(uuid.uuid4())
|
||||
task = self.get_object()
|
||||
log_path = task.full_log_path
|
||||
|
||||
if not log_path or not os.path.isfile(log_path):
|
||||
return Response({"data": _("Waiting ...")}, status=203)
|
||||
|
||||
with open(log_path, 'r') as f:
|
||||
offset = cache.get(mark, 0)
|
||||
f.seek(offset)
|
||||
data = f.read(self.buff_size).replace('\n', '\r\n')
|
||||
mark = str(uuid.uuid4())
|
||||
cache.set(mark, f.tell(), 5)
|
||||
|
||||
if data == '' and task.is_finished():
|
||||
self.end = True
|
||||
return Response({"data": data, 'end': self.end, 'mark': mark})
|
||||
|
||||
|
||||
class CeleryResultApi(generics.RetrieveAPIView):
|
||||
permission_classes = (IsValidUser,)
|
||||
serializer_class = CeleryResultSerializer
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
return AsyncResult(pk)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework import viewsets
|
||||
|
||||
from common.permissions import IsValidUser
|
||||
from ..models import CommandExecution
|
||||
from ..serializers import CommandExecutionSerializer
|
||||
from ..tasks import run_command_execution
|
||||
|
||||
|
||||
class CommandExecutionViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = CommandExecutionSerializer
|
||||
permission_classes = (IsValidUser,)
|
||||
task = None
|
||||
|
||||
def get_queryset(self):
|
||||
return CommandExecution.objects.filter(
|
||||
user_id=str(self.request.user.id)
|
||||
)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
instance = serializer.save()
|
||||
instance.user = self.request.user
|
||||
instance.save()
|
||||
run_command_execution.apply_async(
|
||||
args=(instance.id,), task_id=str(instance.id)
|
||||
)
|
|
@ -27,7 +27,6 @@ def on_app_ready(sender=None, headers=None, body=None, **kwargs):
|
|||
if cache.get("CELERY_APP_READY", 0) == 1:
|
||||
return
|
||||
cache.set("CELERY_APP_READY", 1, 10)
|
||||
logger.debug("App ready signal recv")
|
||||
tasks = get_after_app_ready_tasks()
|
||||
logger.debug("Start need start task: [{}]".format(
|
||||
", ".join(tasks))
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django import forms
|
||||
|
||||
from assets.models import SystemUser
|
||||
from .models import CommandExecution
|
||||
|
||||
|
||||
class CommandExecutionForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = CommandExecution
|
||||
fields = ['run_as', 'command']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
run_as_field = self.fields.get('run_as')
|
||||
run_as_field.queryset = SystemUser.objects.all()
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
|
||||
from .ansible.inventory import BaseInventory
|
||||
from assets.utils import get_assets_by_fullname_list, get_system_user_by_name
|
||||
from assets.utils import get_assets_by_id_list, get_system_user_by_id
|
||||
|
||||
__all__ = [
|
||||
'JMSInventory'
|
||||
|
@ -14,19 +14,18 @@ class JMSInventory(BaseInventory):
|
|||
JMS Inventory is the manager with jumpserver assets, so you can
|
||||
write you own manager, construct you inventory
|
||||
"""
|
||||
def __init__(self, hostname_list, run_as_admin=False, run_as=None, become_info=None):
|
||||
def __init__(self, assets, run_as_admin=False, run_as=None, become_info=None):
|
||||
"""
|
||||
:param hostname_list: ["test1", ]
|
||||
:param host_id_list: ["test1", ]
|
||||
:param run_as_admin: True 是否使用管理用户去执行, 每台服务器的管理用户可能不同
|
||||
:param run_as: 是否统一使用某个系统用户去执行
|
||||
:param become_info: 是否become成某个用户去执行
|
||||
"""
|
||||
self.hostname_list = hostname_list
|
||||
self.assets = assets
|
||||
self.using_admin = run_as_admin
|
||||
self.run_as = run_as
|
||||
self.become_info = become_info
|
||||
|
||||
assets = self.get_jms_assets()
|
||||
host_list = []
|
||||
|
||||
for asset in assets:
|
||||
|
@ -43,14 +42,10 @@ class JMSInventory(BaseInventory):
|
|||
host.update(become_info)
|
||||
super().__init__(host_list=host_list)
|
||||
|
||||
def get_jms_assets(self):
|
||||
assets = get_assets_by_fullname_list(self.hostname_list)
|
||||
return assets
|
||||
|
||||
def convert_to_ansible(self, asset, run_as_admin=False):
|
||||
info = {
|
||||
'id': asset.id,
|
||||
'hostname': asset.fullname,
|
||||
'hostname': asset.hostname,
|
||||
'ip': asset.ip,
|
||||
'port': asset.port,
|
||||
'vars': dict(),
|
||||
|
@ -75,7 +70,7 @@ class JMSInventory(BaseInventory):
|
|||
return info
|
||||
|
||||
def get_run_user_info(self):
|
||||
system_user = get_system_user_by_name(self.run_as)
|
||||
system_user = self.run_as
|
||||
if not system_user:
|
||||
return {}
|
||||
else:
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
# Generated by Django 2.1.4 on 2018-12-07 09:44
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0023_auto_20181016_1650'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('ops', '0002_celerytask'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CommandExecution',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('command', models.TextField(verbose_name='Command')),
|
||||
('_result', models.TextField(blank=True, null=True, verbose_name='Result')),
|
||||
('is_finished', models.BooleanField(default=False)),
|
||||
('date_created', models.DateTimeField(auto_now_add=True)),
|
||||
('date_start', models.DateTimeField(null=True)),
|
||||
('date_finished', models.DateTimeField(null=True)),
|
||||
('hosts', models.ManyToManyField(to='assets.Asset')),
|
||||
('run_as', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.SystemUser')),
|
||||
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='adhoc',
|
||||
name='run_as',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='adhoc',
|
||||
name='hosts',
|
||||
field=models.ManyToManyField(to='assets.Asset', verbose_name='Host'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='created_by',
|
||||
field=models.CharField(blank=True, default='', max_length=128),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='name',
|
||||
field=models.CharField(max_length=128, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='task',
|
||||
unique_together={('name', 'created_by')},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 2.1.4 on 2018-12-07 09:44
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0023_auto_20181016_1650'),
|
||||
('ops', '0003_auto_20181207_1744'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='adhoc',
|
||||
name='run_as',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.SystemUser'),
|
||||
),
|
||||
]
|
|
@ -2,4 +2,5 @@
|
|||
#
|
||||
|
||||
from .adhoc import *
|
||||
from .celery import *
|
||||
from .celery import *
|
||||
from .command import *
|
||||
|
|
|
@ -34,16 +34,17 @@ class Task(models.Model):
|
|||
One task can have some versions of adhoc, run a task only run the latest version adhoc
|
||||
"""
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'))
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
interval = models.IntegerField(verbose_name=_("Interval"), null=True, blank=True, help_text=_("Units: seconds"))
|
||||
crontab = models.CharField(verbose_name=_("Crontab"), null=True, blank=True, max_length=128, help_text=_("5 * * * *"))
|
||||
is_periodic = models.BooleanField(default=False)
|
||||
callback = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Callback")) # Callback must be a registered celery task
|
||||
is_deleted = models.BooleanField(default=False)
|
||||
comment = models.TextField(blank=True, verbose_name=_("Comment"))
|
||||
created_by = models.CharField(max_length=128, blank=True, null=True, default='')
|
||||
created_by = models.CharField(max_length=128, blank=True, default='')
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
__latest_adhoc = None
|
||||
_ignore_auto_created_by = True
|
||||
|
||||
@property
|
||||
def short_id(self):
|
||||
|
@ -94,7 +95,7 @@ class Task(models.Model):
|
|||
update_fields=None):
|
||||
from ..tasks import run_ansible_task
|
||||
super().save(
|
||||
force_insert=force_insert, force_update=force_update,
|
||||
force_insert=force_insert, force_update=force_update,
|
||||
using=using, update_fields=update_fields,
|
||||
)
|
||||
|
||||
|
@ -108,7 +109,7 @@ class Task(models.Model):
|
|||
crontab = self.crontab
|
||||
|
||||
tasks = {
|
||||
self.name: {
|
||||
self.__str__(): {
|
||||
"task": run_ansible_task.name,
|
||||
"interval": interval,
|
||||
"crontab": crontab,
|
||||
|
@ -119,11 +120,11 @@ class Task(models.Model):
|
|||
}
|
||||
create_or_update_celery_periodic_tasks(tasks)
|
||||
else:
|
||||
disable_celery_periodic_task(self.name)
|
||||
disable_celery_periodic_task(self.__str__())
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
super().delete(using=using, keep_parents=keep_parents)
|
||||
delete_celery_periodic_task(self.name)
|
||||
delete_celery_periodic_task(self.__str__())
|
||||
|
||||
@property
|
||||
def schedule(self):
|
||||
|
@ -133,10 +134,11 @@ class Task(models.Model):
|
|||
return None
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
return self.name + '@' + str(self.created_by)
|
||||
|
||||
class Meta:
|
||||
db_table = 'ops_task'
|
||||
unique_together = ('name', 'created_by')
|
||||
get_latest_by = 'date_created'
|
||||
|
||||
|
||||
|
@ -157,8 +159,9 @@ class AdHoc(models.Model):
|
|||
pattern = models.CharField(max_length=64, default='{}', verbose_name=_('Pattern'))
|
||||
_options = models.CharField(max_length=1024, default='', verbose_name=_('Options'))
|
||||
_hosts = models.TextField(blank=True, verbose_name=_('Hosts')) # ['hostname1', 'hostname2']
|
||||
hosts = models.ManyToManyField('assets.Asset', verbose_name=_("Host"))
|
||||
run_as_admin = models.BooleanField(default=False, verbose_name=_('Run as admin'))
|
||||
run_as = models.CharField(max_length=128, default='', verbose_name=_("Run as"))
|
||||
run_as = models.ForeignKey('assets.SystemUser', null=True, on_delete=models.CASCADE)
|
||||
_become = models.CharField(max_length=1024, default='', verbose_name=_("Become"))
|
||||
created_by = models.CharField(max_length=64, default='', null=True, verbose_name=_('Create by'))
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
|
@ -174,14 +177,6 @@ class AdHoc(models.Model):
|
|||
else:
|
||||
raise SyntaxError('Tasks should be a list: {}'.format(item))
|
||||
|
||||
@property
|
||||
def hosts(self):
|
||||
return json.loads(self._hosts)
|
||||
|
||||
@hosts.setter
|
||||
def hosts(self, item):
|
||||
self._hosts = json.dumps(item)
|
||||
|
||||
@property
|
||||
def inventory(self):
|
||||
if self.become:
|
||||
|
@ -194,7 +189,7 @@ class AdHoc(models.Model):
|
|||
become_info = None
|
||||
|
||||
inventory = JMSInventory(
|
||||
self.hosts, run_as_admin=self.run_as_admin,
|
||||
self.hosts.all(), run_as_admin=self.run_as_admin,
|
||||
run_as=self.run_as, become_info=become_info
|
||||
)
|
||||
return inventory
|
||||
|
@ -242,14 +237,13 @@ class AdHoc(models.Model):
|
|||
history.timedelta = time.time() - time_start
|
||||
history.save()
|
||||
|
||||
def _run_only(self, file_obj=None):
|
||||
def _run_only(self):
|
||||
runner = AdHocRunner(self.inventory, options=self.options)
|
||||
try:
|
||||
result = runner.run(
|
||||
self.tasks,
|
||||
self.pattern,
|
||||
self.task.name,
|
||||
file_obj=file_obj,
|
||||
)
|
||||
return result.results_raw, result.results_summary
|
||||
except AnsibleError as e:
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import uuid
|
||||
import json
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ugettext
|
||||
from django.db import models
|
||||
|
||||
from ..ansible.runner import CommandRunner
|
||||
from ..inventory import JMSInventory
|
||||
|
||||
|
||||
class CommandExecution(models.Model):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
hosts = models.ManyToManyField('assets.Asset')
|
||||
run_as = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE)
|
||||
command = models.TextField(verbose_name=_("Command"))
|
||||
_result = models.TextField(blank=True, null=True, verbose_name=_('Result'))
|
||||
user = models.ForeignKey('users.User', on_delete=models.CASCADE, null=True)
|
||||
is_finished = models.BooleanField(default=False)
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
date_start = models.DateTimeField(null=True)
|
||||
date_finished = models.DateTimeField(null=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.command[:10]
|
||||
|
||||
@property
|
||||
def inventory(self):
|
||||
return JMSInventory(self.hosts.all(), run_as=self.run_as)
|
||||
|
||||
@property
|
||||
def result(self):
|
||||
if self._result:
|
||||
return json.loads(self._result)
|
||||
else:
|
||||
return {}
|
||||
|
||||
@result.setter
|
||||
def result(self, item):
|
||||
self._result = json.dumps(item)
|
||||
|
||||
@property
|
||||
def is_success(self):
|
||||
if 'error' in self.result:
|
||||
return False
|
||||
return True
|
||||
|
||||
def run(self):
|
||||
print('-'*10 + ' ' + ugettext('Task start') + ' ' + '-'*10)
|
||||
self.date_start = timezone.now()
|
||||
ok, msg = self.run_as.is_command_can_run(self.command)
|
||||
if ok:
|
||||
runner = CommandRunner(self.inventory)
|
||||
try:
|
||||
result = runner.execute(self.command, 'all')
|
||||
self.result = result.results_command
|
||||
except Exception as e:
|
||||
print("Error occur: {}".format(e))
|
||||
self.result = {"error": str(e)}
|
||||
else:
|
||||
msg = _("Command `{}` is forbidden ........").format(self.command)
|
||||
print('\033[31m' + msg + '\033[0m')
|
||||
self.result = {"error": msg}
|
||||
self.is_finished = True
|
||||
self.date_finished = timezone.now()
|
||||
self.save()
|
||||
print('-'*10 + ' ' + ugettext('Task end') + ' ' + '-'*10)
|
||||
return self.result
|
|
@ -1,8 +1,19 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
from __future__ import unicode_literals
|
||||
from rest_framework import serializers
|
||||
from django.shortcuts import reverse
|
||||
|
||||
from .models import Task, AdHoc, AdHocRunHistory
|
||||
from .models import Task, AdHoc, AdHocRunHistory, CommandExecution
|
||||
|
||||
|
||||
class CeleryResultSerializer(serializers.Serializer):
|
||||
id = serializers.UUIDField()
|
||||
result = serializers.JSONField()
|
||||
state = serializers.CharField(max_length=16)
|
||||
|
||||
|
||||
class CeleryTaskSerializer(serializers.Serializer):
|
||||
pass
|
||||
|
||||
|
||||
class TaskSerializer(serializers.ModelSerializer):
|
||||
|
@ -51,3 +62,23 @@ class AdHocRunHistorySerializer(serializers.ModelSerializer):
|
|||
fields = super().get_field_names(declared_fields, info)
|
||||
fields.extend(['summary', 'short_id'])
|
||||
return fields
|
||||
|
||||
|
||||
class CommandExecutionSerializer(serializers.ModelSerializer):
|
||||
result = serializers.JSONField(read_only=True)
|
||||
log_url = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = CommandExecution
|
||||
fields = [
|
||||
'id', 'hosts', 'run_as', 'command', 'result', 'log_url',
|
||||
'is_finished', 'date_created', 'date_finished'
|
||||
]
|
||||
read_only_fields = [
|
||||
'id', 'result', 'is_finished', 'log_url', 'date_created',
|
||||
'date_finished'
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_log_url(obj):
|
||||
return reverse('api-ops:celery-task-log', kwargs={'pk': obj.id})
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
from celery import shared_task, subtask
|
||||
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from .models import Task
|
||||
from .models import Task, CommandExecution
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
@ -28,6 +28,12 @@ def run_ansible_task(tid, callback=None, **kwargs):
|
|||
logger.error("No task found")
|
||||
|
||||
|
||||
@shared_task
|
||||
def run_command_execution(cid, **kwargs):
|
||||
execution = get_object_or_none(CommandExecution, id=cid)
|
||||
return execution.run()
|
||||
|
||||
|
||||
@shared_task
|
||||
def hello(name, callback=None):
|
||||
print("Hello {}".format(name))
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
var end = false;
|
||||
var error = false;
|
||||
var interval = 200;
|
||||
var success = true;
|
||||
|
||||
function calWinSize() {
|
||||
var t = $('#marker');
|
||||
|
@ -34,20 +35,19 @@
|
|||
{#colWidth = 1.00 * t.width() / 6;#}
|
||||
}
|
||||
function resize() {
|
||||
{#console.log(rowHeight, window.innerHeight);#}
|
||||
{#console.log(colWidth, window.innerWidth);#}
|
||||
var rows = Math.floor(window.innerHeight / rowHeight) - 1;
|
||||
var cols = Math.floor(window.innerWidth / colWidth) - 2;
|
||||
console.log(rows, cols);
|
||||
term.resize(cols, rows);
|
||||
}
|
||||
function requestAndWrite() {
|
||||
if (!end) {
|
||||
if (!end && success) {
|
||||
success = false;
|
||||
$.ajax({
|
||||
url: url + '?mark=' + mark,
|
||||
method: "GET",
|
||||
contentType: "application/json; charset=utf-8"
|
||||
}).done(function(data, textStatue, jqXHR) {
|
||||
success = true;
|
||||
if (jqXHR.status === 203) {
|
||||
error = true;
|
||||
term.write('.');
|
||||
|
@ -64,7 +64,14 @@
|
|||
}
|
||||
}
|
||||
$(document).ready(function () {
|
||||
term = new Terminal();
|
||||
term = new Terminal({
|
||||
cursorBlink: false,
|
||||
screenKeys: false,
|
||||
fontFamily: '"Monaco", "Consolas", "monospace"',
|
||||
fontSize: 12,
|
||||
rightClickSelectsWord: true,
|
||||
disableStdin: true
|
||||
});
|
||||
term.open(document.getElementById('term'));
|
||||
term.resize(80, 24);
|
||||
resize();
|
||||
|
|
|
@ -0,0 +1,254 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block custom_head_css_js %}
|
||||
<link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet">
|
||||
<script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.exhide.min.js' %}"></script>
|
||||
<script src="{% static 'js/jquery.form.min.js' %}"></script>
|
||||
<script src="{% static 'js/plugins/xterm/xterm.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'js/plugins/xterm/xterm.css' %}" />
|
||||
<script src="{% static 'js/plugins/xterm/addons/fit/fit.js' %}"></script>
|
||||
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
|
||||
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
|
||||
<style type="text/css">
|
||||
.xterm .xterm-screen canvas {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
.select2-container .select2-selection--single {
|
||||
height: 34px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content">
|
||||
<div class="row">
|
||||
<div class="col-lg-3" id="split-left" style="padding-left: 3px">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-content mailbox-content" style="padding-top: 0;padding-left: 1px">
|
||||
<div class="file-manager ">
|
||||
<div id="assetTree" class="ztree"></div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-9 animated fadeInRight" id="split-right">
|
||||
<div class="tree-toggle">
|
||||
<div class="btn btn-sm btn-primary tree-toggle-btn" onclick="toggle()">
|
||||
<i class="fa fa-angle-left fa-x" id="toggle-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mail-box-header" style="padding-top: 5px;">
|
||||
<form enctype="multipart/form-data" method="post" class="form-horizontal" action="">
|
||||
<div class="form-group">
|
||||
<div id="term" style="height: 100%;width: 100%"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-2">
|
||||
<select class="select2 form-control" id="system-users-select">
|
||||
{% for s in system_users %}
|
||||
{% if s.protocol == 'ssh' and s.login_mode == 'auto' %}
|
||||
<option value="{{ s.id }}">{{ s }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-10">
|
||||
<div class="input-group" style="height: 100%">
|
||||
<input type="text" id="command-text" class="form-control">
|
||||
<span class="input-group-btn">
|
||||
<button type="button" class="btn btn-primary btn-execute">{% trans 'Go' %}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
var zTree, show = 0;
|
||||
var systemUserId = null;
|
||||
|
||||
function initTree() {
|
||||
var setting = {
|
||||
check: {
|
||||
enable: true
|
||||
},
|
||||
view: {
|
||||
dblClickExpand: false,
|
||||
showLine: true
|
||||
},
|
||||
data: {
|
||||
simpleData: {
|
||||
enable: true
|
||||
}
|
||||
},
|
||||
edit: {
|
||||
enable: true,
|
||||
showRemoveBtn: false,
|
||||
showRenameBtn: false,
|
||||
drag: {
|
||||
isCopy: true,
|
||||
isMove: true
|
||||
}
|
||||
},
|
||||
callback: {
|
||||
onCheck: onCheck
|
||||
}
|
||||
};
|
||||
var url = "{% url 'api-perms:my-nodes-assets-as-tree' %}";
|
||||
if (systemUserId) {
|
||||
url += '?system_user=' + systemUserId
|
||||
}
|
||||
|
||||
$.get(url, function(data, status){
|
||||
$.fn.zTree.init($("#assetTree"), setting, data);
|
||||
zTree = $.fn.zTree.getZTreeObj("assetTree");
|
||||
});
|
||||
}
|
||||
|
||||
function getSelectedAssetsNode() {
|
||||
var nodes = zTree.getCheckedNodes(true);
|
||||
var assetsNodeId = [];
|
||||
var assetsNode = [];
|
||||
nodes.forEach(function (node) {
|
||||
if (node.meta.type === 'asset' && !node.isHidden && node.meta.asset.protocol === 'ssh') {
|
||||
if (assetsNodeId.indexOf(node.id) === -1) {
|
||||
assetsNodeId.push(node.id);
|
||||
assetsNode.push(node)
|
||||
}
|
||||
}
|
||||
});
|
||||
return assetsNode;
|
||||
}
|
||||
|
||||
function onCheck(e, treeId, treeNode) {
|
||||
var nodes = getSelectedAssetsNode();
|
||||
var nodes_names = nodes.map(function (node) {
|
||||
return node.name;
|
||||
});
|
||||
var message = "已选择资产: ";
|
||||
message += nodes_names.join(", ");
|
||||
message += "\r\n";
|
||||
message += "总共: " + nodes_names.length + "个\r\n";
|
||||
term.clear();
|
||||
term.write(message)
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (show === 0) {
|
||||
$("#split-left").hide(500, function () {
|
||||
$("#split-right").attr("class", "col-lg-12");
|
||||
$("#toggle-icon").attr("class", "fa fa-angle-right fa-x");
|
||||
show = 1;
|
||||
});
|
||||
} else {
|
||||
$("#split-right").attr("class", "col-lg-9");
|
||||
$("#toggle-icon").attr("class", "fa fa-angle-left fa-x");
|
||||
$("#split-left").show(500);
|
||||
show = 0;
|
||||
}
|
||||
}
|
||||
|
||||
var term = null;
|
||||
|
||||
function initResultTerminal() {
|
||||
term = new Terminal({
|
||||
cursorBlink: false,
|
||||
screenKeys: false,
|
||||
fontFamily: '"Monaco", "Consolas", "monospace"',
|
||||
fontSize: 12,
|
||||
rightClickSelectsWord: true,
|
||||
disableStdin: true,
|
||||
theme: {
|
||||
background: '#1f1b1b'
|
||||
}
|
||||
});
|
||||
term.open(document.getElementById('term'));
|
||||
term.write("选择左侧资产, 选择运行的系统用户,批量执行命令\r\n")
|
||||
}
|
||||
|
||||
$(document).ready(function(){
|
||||
systemUserId = $('#system-users-select').val();
|
||||
$(".select2").select2().on('select2:select', function(evt) {
|
||||
var data = evt.params.data;
|
||||
systemUserId = data.id;
|
||||
initTree();
|
||||
});
|
||||
initTree();
|
||||
initResultTerminal();
|
||||
}).on('click', '.btn-execute', function () {
|
||||
if (!term) {
|
||||
initResultTerminal()
|
||||
}
|
||||
var url = '{% url "api-ops:command-execution-list" %}';
|
||||
var run_as = systemUserId;
|
||||
var command = $("#command-text").val();
|
||||
var hosts = getSelectedAssetsNode().map(function (node) {
|
||||
return node.id;
|
||||
});
|
||||
var data = {
|
||||
hosts: hosts,
|
||||
run_as: run_as,
|
||||
command: command
|
||||
};
|
||||
var mark = '';
|
||||
var log_url = null;
|
||||
var end = false;
|
||||
var error = false;
|
||||
var int = null;
|
||||
var interval = 200;
|
||||
|
||||
function writeExecutionOutput() {
|
||||
if (!end) {
|
||||
$.ajax({
|
||||
url: log_url + '?mark=' + mark,
|
||||
method: "GET",
|
||||
contentType: "application/json; charset=utf-8"
|
||||
}).done(function(data, textStatue, jqXHR) {
|
||||
if (jqXHR.status === 203) {
|
||||
error = true;
|
||||
term.write('.');
|
||||
interval = 500;
|
||||
}
|
||||
if (jqXHR.status === 200){
|
||||
term.write(data.data);
|
||||
mark = data.mark;
|
||||
if (data.end){
|
||||
end = true;
|
||||
window.clearInterval(int)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
APIUpdateAttr({
|
||||
url: url,
|
||||
body: JSON.stringify(data),
|
||||
method: 'POST',
|
||||
flash_message: false,
|
||||
success: function (resp) {
|
||||
term.write("{% trans 'Pending' %}" + "...\r\n");
|
||||
log_url = resp.log_url;
|
||||
int = setInterval(function () {
|
||||
writeExecutionOutput()
|
||||
}, interval);
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,95 @@
|
|||
{% extends '_base_list.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load common_tags %}
|
||||
|
||||
{% block content_left_head %}
|
||||
{% endblock %}
|
||||
|
||||
{% block table_search %}
|
||||
<form id="search_form" method="get" action="" class="pull-right form-inline">
|
||||
<div class="form-group" id="date">
|
||||
<div class="input-daterange input-group" id="datepicker">
|
||||
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
|
||||
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_from" value="{{ date_from|date:'Y-m-d' }}">
|
||||
<span class="input-group-addon">to</span>
|
||||
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_to" value="{{ date_to|date:'Y-m-d' }}">
|
||||
</div>
|
||||
</div>
|
||||
{% if user_list %}
|
||||
<div class="input-group">
|
||||
<select class="select2 form-control" name="user">
|
||||
<option value="">{% trans 'User' %}</option>
|
||||
{% for u in user_list %}
|
||||
<option value="{{ u.id }}" {% if u.id == user_id %} selected {% endif %}>{{ u }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
<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 'Hosts' %}</th>
|
||||
<th class="text-center">{% trans 'User' %}</th>
|
||||
<th class="text-center">{% trans 'Command' %}</th>
|
||||
<th class="text-center">{% trans 'Output' %}</th>
|
||||
<th class="text-center">{% trans 'Finished' %}</th>
|
||||
<th class="text-center">{% trans 'Success' %}</th>
|
||||
<th class="text-center">{% trans 'Date start' %}</th>
|
||||
<th class="text-center">{% trans 'Date finished' %}</th>
|
||||
{% endblock %}
|
||||
|
||||
{% block table_body %}
|
||||
{% for object in object_list %}
|
||||
<tr class="gradeX">
|
||||
<td class="text-center"><input type="checkbox" class="cbx-term"></td>
|
||||
<td class="text-center">{{ object.hosts.count }}</td>
|
||||
<td class="text-center">{{ object.user }}</td>
|
||||
<td class="text-center">{{ object.command| truncatechars:16 }}</td>
|
||||
<td class="text-center"><a href="{% url "ops:celery-task-log" pk=object.id %}" target="_blank">查看</a></td>
|
||||
<td class="text-center">{{ object.is_finished | state_show | safe }}</td>
|
||||
<td class="text-center">{{ object.is_success | state_show | safe }}</td>
|
||||
<td class="text-center">{{ object.date_start }}</td>
|
||||
<td class="text-center">{{ object.date_finished }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('table').DataTable({
|
||||
"searching": false,
|
||||
"paging": false,
|
||||
"bInfo" : false,
|
||||
"order": []
|
||||
});
|
||||
$('.select2').select2({
|
||||
dropdownAutoWidth : true,
|
||||
width: 'auto'
|
||||
});
|
||||
$('#date .input-daterange').datepicker({
|
||||
format: "yyyy-mm-dd",
|
||||
todayBtn: "linked",
|
||||
keyboardNavigation: false,
|
||||
forceParse: false,
|
||||
calendarWeeks: true,
|
||||
autoclose: true
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -4,10 +4,11 @@
|
|||
|
||||
{% block custom_head_css_js %}
|
||||
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
|
||||
<link href="{% static "css/plugins/sweetalert/sweetalert.css" %}" rel="stylesheet">
|
||||
<link href="{% static 'css/plugins/sweetalert/sweetalert.css' %}" rel="stylesheet">
|
||||
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
|
||||
<script src="{% static "js/plugins/sweetalert/sweetalert.min.js" %}"></script>
|
||||
<script src="{% static 'js/plugins/sweetalert/sweetalert.min.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
|
@ -25,7 +26,7 @@
|
|||
<a href="{% url 'ops:task-history' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="text-center celery-task-log" onclick="window.open('{% url 'ops:celery-task-log' pk=object.latest_history.pk %}','', 'width=800,height=600,left=400,top=400')"><i class="fa fa-laptop"></i> {% trans 'Last run output' %} </a>
|
||||
<a class="text-center celery-task-log" onclick="window.open("{% url 'ops:celery-task-log' pk=object.latest_history.pk %}",'', 'width=800,height=600,left=400,top=400')"><i class="fa fa-laptop"></i> {% trans 'Last run output' %} </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -11,11 +11,12 @@ app_name = "ops"
|
|||
router = DefaultRouter()
|
||||
router.register(r'tasks', api.TaskViewSet, 'task')
|
||||
router.register(r'adhoc', api.AdHocViewSet, 'adhoc')
|
||||
router.register(r'history', api.AdHocRunHistorySet, 'history')
|
||||
router.register(r'command-executions', api.CommandExecutionViewSet, 'command-execution')
|
||||
|
||||
urlpatterns = [
|
||||
path('tasks/<uuid:pk>/run/', api.TaskRun.as_view(), name='task-run'),
|
||||
path('celery/task/<uuid:pk>/log/', api.CeleryTaskLogApi.as_view(), name='celery-task-log'),
|
||||
path('celery/task/<uuid:pk>/result/', api.CeleryResultApi.as_view(), name='celery-result'),
|
||||
]
|
||||
|
||||
urlpatterns += router.urls
|
||||
|
|
|
@ -18,4 +18,7 @@ urlpatterns = [
|
|||
path('adhoc/<uuid:pk>/history/', views.AdHocHistoryView.as_view(), name='adhoc-history'),
|
||||
path('adhoc/history/<uuid:pk>/', views.AdHocHistoryDetailView.as_view(), name='adhoc-history-detail'),
|
||||
path('celery/task/<uuid:pk>/log/', views.CeleryTaskLogView.as_view(), name='celery-task-log'),
|
||||
|
||||
path('command-execution/', views.CommandExecutionListView.as_view(), name='command-execution-list'),
|
||||
path('command-execution/start/', views.CommandExecutionStartView.as_view(), name='command-execution-start'),
|
||||
]
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from orgs.utils import set_to_root_org
|
||||
from .models import Task, AdHoc
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
@ -10,15 +12,14 @@ def get_task_by_id(task_id):
|
|||
|
||||
|
||||
def update_or_create_ansible_task(
|
||||
task_name, hosts, tasks,
|
||||
task_name, hosts, tasks, created_by,
|
||||
interval=None, crontab=None, is_periodic=False,
|
||||
callback=None, pattern='all', options=None,
|
||||
run_as_admin=False, run_as="", become_info=None,
|
||||
created_by=None,
|
||||
run_as_admin=False, run_as=None, become_info=None,
|
||||
):
|
||||
if not hosts or not tasks or not task_name:
|
||||
return
|
||||
|
||||
set_to_root_org()
|
||||
defaults = {
|
||||
'name': task_name,
|
||||
'interval': interval,
|
||||
|
@ -29,22 +30,27 @@ def update_or_create_ansible_task(
|
|||
}
|
||||
|
||||
created = False
|
||||
task, _ = Task.objects.update_or_create(
|
||||
defaults=defaults, name=task_name,
|
||||
task, ok = Task.objects.update_or_create(
|
||||
defaults=defaults, name=task_name, created_by=created_by
|
||||
)
|
||||
|
||||
adhoc = task.latest_adhoc
|
||||
adhoc = task.get_latest_adhoc()
|
||||
new_adhoc = AdHoc(task=task, pattern=pattern,
|
||||
run_as_admin=run_as_admin,
|
||||
run_as=run_as)
|
||||
new_adhoc.hosts = hosts
|
||||
new_adhoc.tasks = tasks
|
||||
new_adhoc.options = options
|
||||
new_adhoc.become = become_info
|
||||
|
||||
if not adhoc or adhoc != new_adhoc:
|
||||
print("Task create new adhoc: {}".format(task_name))
|
||||
hosts_same = True
|
||||
if adhoc:
|
||||
old_hosts = set([str(asset.id) for asset in adhoc.hosts.all()])
|
||||
new_hosts = set([str(asset.id) for asset in hosts])
|
||||
hosts_same = old_hosts == new_hosts
|
||||
|
||||
if not adhoc or adhoc != new_adhoc or not hosts_same:
|
||||
logger.info(_("Update task content: {}").format(task_name))
|
||||
new_adhoc.save()
|
||||
new_adhoc.hosts.set(hosts)
|
||||
task.latest_adhoc = new_adhoc
|
||||
created = True
|
||||
return task, created
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
from .adhoc import *
|
||||
from .celery import *
|
||||
from .command import *
|
|
@ -2,14 +2,22 @@
|
|||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.conf import settings
|
||||
from django.views.generic import ListView, DetailView, TemplateView
|
||||
from django.views.generic import ListView, DetailView
|
||||
|
||||
from common.mixins import DatetimeSearchMixin
|
||||
from .models import Task, AdHoc, AdHocRunHistory, CeleryTask
|
||||
from common.permissions import SuperUserRequiredMixin, AdminUserRequiredMixin
|
||||
from common.permissions import AdminUserRequiredMixin
|
||||
from orgs.utils import current_org
|
||||
from ..models import Task, AdHoc, AdHocRunHistory
|
||||
|
||||
|
||||
class TaskListView(SuperUserRequiredMixin, DatetimeSearchMixin, ListView):
|
||||
__all__ = [
|
||||
'TaskListView', 'TaskDetailView', 'TaskHistoryView',
|
||||
'TaskAdhocView', 'AdHocDetailView', 'AdHocHistoryDetailView',
|
||||
'AdHocHistoryView'
|
||||
]
|
||||
|
||||
|
||||
class TaskListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
|
||||
paginate_by = settings.DISPLAY_PER_PAGE
|
||||
model = Task
|
||||
ordering = ('-date_created',)
|
||||
|
@ -18,18 +26,23 @@ class TaskListView(SuperUserRequiredMixin, DatetimeSearchMixin, ListView):
|
|||
keyword = ''
|
||||
|
||||
def get_queryset(self):
|
||||
self.queryset = super().get_queryset()
|
||||
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', '')
|
||||
self.queryset = self.queryset.filter(
|
||||
queryset = queryset.filter(
|
||||
date_created__gt=self.date_from,
|
||||
date_created__lt=self.date_to
|
||||
)
|
||||
|
||||
if self.keyword:
|
||||
self.queryset = self.queryset.filter(
|
||||
queryset = queryset.filter(
|
||||
name__icontains=self.keyword,
|
||||
)
|
||||
return self.queryset
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
|
@ -43,10 +56,16 @@ class TaskListView(SuperUserRequiredMixin, DatetimeSearchMixin, ListView):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class TaskDetailView(SuperUserRequiredMixin, DetailView):
|
||||
class TaskDetailView(AdminUserRequiredMixin, DetailView):
|
||||
model = Task
|
||||
template_name = 'ops/task_detail.html'
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
if current_org:
|
||||
queryset = queryset.filter(created_by=current_org.id)
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Ops'),
|
||||
|
@ -56,7 +75,7 @@ class TaskDetailView(SuperUserRequiredMixin, DetailView):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class TaskAdhocView(SuperUserRequiredMixin, DetailView):
|
||||
class TaskAdhocView(AdminUserRequiredMixin, DetailView):
|
||||
model = Task
|
||||
template_name = 'ops/task_adhoc.html'
|
||||
|
||||
|
@ -69,7 +88,7 @@ class TaskAdhocView(SuperUserRequiredMixin, DetailView):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class TaskHistoryView(SuperUserRequiredMixin, DetailView):
|
||||
class TaskHistoryView(AdminUserRequiredMixin, DetailView):
|
||||
model = Task
|
||||
template_name = 'ops/task_history.html'
|
||||
|
||||
|
@ -82,7 +101,7 @@ class TaskHistoryView(SuperUserRequiredMixin, DetailView):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AdHocDetailView(SuperUserRequiredMixin, DetailView):
|
||||
class AdHocDetailView(AdminUserRequiredMixin, DetailView):
|
||||
model = AdHoc
|
||||
template_name = 'ops/adhoc_detail.html'
|
||||
|
||||
|
@ -95,7 +114,7 @@ class AdHocDetailView(SuperUserRequiredMixin, DetailView):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AdHocHistoryView(SuperUserRequiredMixin, DetailView):
|
||||
class AdHocHistoryView(AdminUserRequiredMixin, DetailView):
|
||||
model = AdHoc
|
||||
template_name = 'ops/adhoc_history.html'
|
||||
|
||||
|
@ -108,7 +127,7 @@ class AdHocHistoryView(SuperUserRequiredMixin, DetailView):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AdHocHistoryDetailView(SuperUserRequiredMixin, DetailView):
|
||||
class AdHocHistoryDetailView(AdminUserRequiredMixin, DetailView):
|
||||
model = AdHocRunHistory
|
||||
template_name = 'ops/adhoc_history_detail.html'
|
||||
|
||||
|
@ -121,6 +140,5 @@ class AdHocHistoryDetailView(SuperUserRequiredMixin, DetailView):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class CeleryTaskLogView(AdminUserRequiredMixin, DetailView):
|
||||
template_name = 'ops/celery_task_log.html'
|
||||
model = CeleryTask
|
||||
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.views.generic import DetailView
|
||||
|
||||
from common.permissions import AdminUserRequiredMixin
|
||||
from ..models import CeleryTask
|
||||
|
||||
|
||||
__all__ = ['CeleryTaskLogView']
|
||||
|
||||
|
||||
class CeleryTaskLogView(AdminUserRequiredMixin, DetailView):
|
||||
template_name = 'ops/celery_task_log.html'
|
||||
model = CeleryTask
|
|
@ -0,0 +1,76 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.conf import settings
|
||||
from django.views.generic import ListView, TemplateView
|
||||
|
||||
from common.mixins import DatetimeSearchMixin
|
||||
from ..models import CommandExecution
|
||||
from ..forms import CommandExecutionForm
|
||||
|
||||
|
||||
__all__ = [
|
||||
'CommandExecutionListView', 'CommandExecutionStartView'
|
||||
]
|
||||
|
||||
|
||||
class CommandExecutionListView(DatetimeSearchMixin, ListView):
|
||||
template_name = 'ops/command_execution_list.html'
|
||||
model = CommandExecution
|
||||
paginate_by = settings.DISPLAY_PER_PAGE
|
||||
ordering = ('-date_created',)
|
||||
context_object_name = 'task_list'
|
||||
keyword = ''
|
||||
|
||||
def _get_queryset(self):
|
||||
self.keyword = self.request.GET.get('keyword', '')
|
||||
queryset = super().get_queryset()
|
||||
if self.date_from:
|
||||
queryset = queryset.filter(date_start__gte=self.date_from)
|
||||
if self.date_to:
|
||||
queryset = queryset.filter(date_start__lte=self.date_to)
|
||||
if self.keyword:
|
||||
queryset = queryset.filter(command__icontains=self.keyword)
|
||||
return queryset
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = self._get_queryset().filter(user=self.request.user)
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Ops'),
|
||||
'action': _('Command execution list'),
|
||||
'date_from': self.date_from,
|
||||
'date_to': self.date_to,
|
||||
'keyword': self.keyword,
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class CommandExecutionStartView(TemplateView):
|
||||
template_name = 'ops/command_execution_create.html'
|
||||
form_class = CommandExecutionForm
|
||||
|
||||
def get_user_system_users(self):
|
||||
from perms.utils import AssetPermissionUtil
|
||||
user = self.request.user
|
||||
util = AssetPermissionUtil(user)
|
||||
system_users = [s for s in util.get_system_users() if s.protocol == 'ssh']
|
||||
return system_users
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
system_users = self.get_user_system_users()
|
||||
context = {
|
||||
'app': _('Ops'),
|
||||
'action': _('Command execution'),
|
||||
'form': self.get_form(),
|
||||
'system_users': system_users
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_form(self):
|
||||
return self.form_class()
|
|
@ -3,18 +3,20 @@
|
|||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.views import APIView, Response
|
||||
from rest_framework.generics import ListAPIView, get_object_or_404, RetrieveUpdateAPIView
|
||||
from rest_framework.generics import ListAPIView, get_object_or_404, \
|
||||
RetrieveUpdateAPIView
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
|
||||
from common.utils import set_or_append_attr_bulk
|
||||
from common.permissions import IsValidUser, IsOrgAdmin, IsOrgAdminOrAppUser
|
||||
from common.tree import TreeNode, TreeNodeSerializer
|
||||
from orgs.mixins import RootOrgViewMixin
|
||||
from orgs.utils import set_to_root_org
|
||||
from .utils import AssetPermissionUtil
|
||||
from .models import AssetPermission
|
||||
from .hands import AssetGrantedSerializer, User, UserGroup, Asset, Node, \
|
||||
NodeGrantedSerializer, SystemUser, NodeSerializer
|
||||
from orgs.utils import set_to_root_org
|
||||
from . import serializers
|
||||
from .mixins import AssetsFilterMixin
|
||||
|
||||
|
@ -25,6 +27,7 @@ __all__ = [
|
|||
'UserGroupGrantedNodesApi', 'UserGroupGrantedNodesWithAssetsApi', 'UserGroupGrantedNodeAssetsApi',
|
||||
'ValidateUserAssetPermissionApi', 'AssetPermissionRemoveUserApi', 'AssetPermissionAddUserApi',
|
||||
'AssetPermissionRemoveAssetApi', 'AssetPermissionAddAssetApi', 'UserGrantedNodeChildrenApi',
|
||||
'UserGrantedNodesWithAssetsAsTreeApi',
|
||||
]
|
||||
|
||||
|
||||
|
@ -186,6 +189,99 @@ class UserGrantedNodesWithAssetsApi(AssetsFilterMixin, ListAPIView):
|
|||
return super().get_permissions()
|
||||
|
||||
|
||||
class UserGrantedNodesWithAssetsAsTreeApi(ListAPIView):
|
||||
serializer_class = TreeNodeSerializer
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
show_assets = True
|
||||
system_user_id = None
|
||||
|
||||
def change_org_if_need(self):
|
||||
if self.request.user.is_superuser or \
|
||||
self.request.user.is_app or \
|
||||
self.kwargs.get('pk') is None:
|
||||
set_to_root_org()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.show_assets = request.query_params.get('show_assets', '1') == '1'
|
||||
self.system_user_id = request.query_params.get('system_user')
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def parse_node_to_tree_node(node):
|
||||
name = '{} ({})'.format(node.value, node.assets_amount)
|
||||
node_serializer = serializers.GrantedNodeSerializer(node)
|
||||
data = {
|
||||
'id': node.key,
|
||||
'name': name,
|
||||
'title': name,
|
||||
'pId': node.parent_key,
|
||||
'isParent': True,
|
||||
'open': node.is_root(),
|
||||
'meta': {
|
||||
'node': node_serializer.data,
|
||||
'type': 'node'
|
||||
}
|
||||
}
|
||||
tree_node = TreeNode(**data)
|
||||
return tree_node
|
||||
|
||||
@staticmethod
|
||||
def parse_asset_to_tree_node(node, asset, system_users):
|
||||
system_user_serializer = serializers.GrantedSystemUserSerializer(
|
||||
system_users, many=True
|
||||
)
|
||||
asset_serializer = serializers.GrantedAssetSerializer(asset)
|
||||
icon_skin = 'file'
|
||||
if asset.platform.lower() == 'windows':
|
||||
icon_skin = 'windows'
|
||||
elif asset.platform.lower() == 'linux':
|
||||
icon_skin = 'linux'
|
||||
data = {
|
||||
'id': str(asset.id),
|
||||
'name': asset.hostname,
|
||||
'title': asset.ip,
|
||||
'pId': node.key,
|
||||
'isParent': False,
|
||||
'open': False,
|
||||
'iconSkin': icon_skin,
|
||||
'meta': {
|
||||
'system_users': system_user_serializer.data,
|
||||
'type': 'asset',
|
||||
'asset': asset_serializer.data
|
||||
}
|
||||
}
|
||||
tree_node = TreeNode(**data)
|
||||
return tree_node
|
||||
|
||||
def get_permissions(self):
|
||||
if self.kwargs.get('pk') is None:
|
||||
self.permission_classes = (IsValidUser,)
|
||||
return super().get_permissions()
|
||||
|
||||
def get_queryset(self):
|
||||
self.change_org_if_need()
|
||||
user_id = self.kwargs.get('pk', '')
|
||||
queryset = []
|
||||
if not user_id:
|
||||
user = self.request.user
|
||||
else:
|
||||
user = get_object_or_404(User, id=user_id)
|
||||
util = AssetPermissionUtil(user)
|
||||
if self.system_user_id:
|
||||
util.filter_permission_with_system_user(system_user=self.system_user_id)
|
||||
nodes = util.get_nodes_with_assets()
|
||||
for node, assets in nodes.items():
|
||||
data = self.parse_node_to_tree_node(node)
|
||||
queryset.append(data)
|
||||
if not self.show_assets:
|
||||
continue
|
||||
for asset, system_users in assets.items():
|
||||
data = self.parse_asset_to_tree_node(node, asset, system_users)
|
||||
queryset.append(data)
|
||||
queryset = sorted(queryset)
|
||||
return queryset
|
||||
|
||||
|
||||
class UserGrantedNodeAssetsApi(AssetsFilterMixin, ListAPIView):
|
||||
"""
|
||||
查询用户授权的节点下的资产的api, 与上面api不同的是,只返回某个节点下的资产
|
||||
|
|
|
@ -5,9 +5,16 @@ from rest_framework import serializers
|
|||
|
||||
from common.fields import StringManyToManyField
|
||||
from .models import AssetPermission
|
||||
from assets.models import Node
|
||||
from assets.models import Node, Asset, SystemUser
|
||||
from assets.serializers import AssetGrantedSerializer
|
||||
|
||||
__all__ = [
|
||||
'AssetPermissionCreateUpdateSerializer', 'AssetPermissionListSerializer',
|
||||
'AssetPermissionUpdateUserSerializer', 'AssetPermissionUpdateAssetSerializer',
|
||||
'AssetPermissionNodeSerializer', 'GrantedNodeSerializer',
|
||||
'GrantedAssetSerializer', 'GrantedSystemUserSerializer',
|
||||
]
|
||||
|
||||
|
||||
class AssetPermissionCreateUpdateSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
|
@ -74,3 +81,29 @@ class AssetPermissionNodeSerializer(serializers.ModelSerializer):
|
|||
@staticmethod
|
||||
def get_tree_parent(obj):
|
||||
return obj.parent_key
|
||||
|
||||
|
||||
class GrantedNodeSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Node
|
||||
fields = [
|
||||
'id', 'name', 'key', 'value',
|
||||
]
|
||||
|
||||
|
||||
class GrantedAssetSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = [
|
||||
'id', 'hostname', 'ip', 'port', 'protocol', 'platform',
|
||||
'domain', 'is_active', 'comment'
|
||||
]
|
||||
|
||||
|
||||
class GrantedSystemUserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
fields = [
|
||||
'id', 'name', 'username', 'protocol', 'priority',
|
||||
'login_mode', 'comment'
|
||||
]
|
||||
|
|
|
@ -29,6 +29,11 @@ urlpatterns = [
|
|||
api.UserGrantedNodesWithAssetsApi.as_view(), name='user-nodes-assets'),
|
||||
path('user/nodes-assets/', api.UserGrantedNodesWithAssetsApi.as_view(),
|
||||
name='my-nodes-assets'),
|
||||
path('user/<uuid:pk>/nodes-assets/tree/',
|
||||
api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='user-nodes-assets-as-tree'),
|
||||
path('user/nodes-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(),
|
||||
name='my-nodes-assets-as-tree'),
|
||||
|
||||
|
||||
# 查询某个用户组授权的资产和资产组
|
||||
path('user-group/<uuid:pk>/assets/',
|
||||
|
|
|
@ -11,36 +11,41 @@ from .hands import Node
|
|||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class Tree:
|
||||
class GenerateTree:
|
||||
def __init__(self):
|
||||
self.__all_nodes = Node.objects.all().prefetch_related('assets')
|
||||
"""
|
||||
nodes: {"node_instance": {
|
||||
"asset_instance": set("system_user")
|
||||
}
|
||||
"""
|
||||
self.__all_nodes = Node.objects.all()
|
||||
self.__node_asset_map = defaultdict(set)
|
||||
self.nodes = defaultdict(dict)
|
||||
self.root = Node.root()
|
||||
self.init_node_asset_map()
|
||||
|
||||
def init_node_asset_map(self):
|
||||
for node in self.__all_nodes:
|
||||
assets = [a.id for a in node.assets.all()]
|
||||
for asset in assets:
|
||||
self.__node_asset_map[str(asset)].add(node)
|
||||
|
||||
def add_asset(self, asset, system_users):
|
||||
nodes = self.__node_asset_map.get(str(asset.id), [])
|
||||
nodes = asset.nodes.all()
|
||||
self.add_nodes(nodes)
|
||||
for node in nodes:
|
||||
self.nodes[node][asset].update(system_users)
|
||||
|
||||
def get_nodes(self):
|
||||
for node in self.nodes:
|
||||
assets = set(self.nodes.get(node).keys())
|
||||
for n in self.nodes.keys():
|
||||
if n.key.startswith(node.key + ':'):
|
||||
assets.update(set(self.nodes[n].keys()))
|
||||
node.assets_amount = len(assets)
|
||||
return self.nodes
|
||||
|
||||
def add_node(self, node):
|
||||
if node in self.nodes:
|
||||
return
|
||||
else:
|
||||
self.nodes[node] = defaultdict(set)
|
||||
if node.key == self.root.key:
|
||||
if node.is_root():
|
||||
return
|
||||
parent_key = ':'.join(node.key.split(':')[:-1])
|
||||
for n in self.__all_nodes:
|
||||
if n.key == parent_key:
|
||||
if n.key == node.parent_key:
|
||||
self.add_node(n)
|
||||
break
|
||||
|
||||
|
@ -107,6 +112,9 @@ class AssetPermissionUtil:
|
|||
self._permissions = permissions
|
||||
return permissions
|
||||
|
||||
def filter_permission_with_system_user(self, system_user):
|
||||
self._permissions = self.permissions.filter(system_users=system_user)
|
||||
|
||||
def get_nodes_direct(self):
|
||||
"""
|
||||
返回用户/组授权规则直接关联的节点
|
||||
|
@ -150,10 +158,17 @@ class AssetPermissionUtil:
|
|||
:return:
|
||||
"""
|
||||
assets = self.get_assets()
|
||||
tree = Tree()
|
||||
tree = GenerateTree()
|
||||
for asset, system_users in assets.items():
|
||||
tree.add_asset(asset, system_users)
|
||||
return tree.nodes
|
||||
return tree.get_nodes()
|
||||
|
||||
def get_system_users(self):
|
||||
system_users = set()
|
||||
permissions = self.permissions.prefetch_related('system_users')
|
||||
for perm in permissions:
|
||||
system_users.update(perm.system_users.all())
|
||||
return system_users
|
||||
|
||||
|
||||
def is_obj_attr_has(obj, val, attrs=("hostname", "ip", "comment")):
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,309 @@
|
|||
/* BASICS */
|
||||
|
||||
.CodeMirror {
|
||||
/* Set height, width, borders, and global font properties here */
|
||||
font-family: monospace;
|
||||
height: 300px;
|
||||
}
|
||||
.CodeMirror-scroll {
|
||||
/* Set scrolling behaviour here */
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* PADDING */
|
||||
|
||||
.CodeMirror-lines {
|
||||
padding: 4px 0; /* Vertical padding around content */
|
||||
}
|
||||
.CodeMirror pre {
|
||||
padding: 0 4px; /* Horizontal padding of content */
|
||||
}
|
||||
|
||||
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
|
||||
background-color: white; /* The little square between H and V scrollbars */
|
||||
}
|
||||
|
||||
/* GUTTER */
|
||||
|
||||
.CodeMirror-gutters {
|
||||
border-right: 1px solid #ddd;
|
||||
background-color: #f7f7f7;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.CodeMirror-linenumbers {}
|
||||
.CodeMirror-linenumber {
|
||||
padding: 0 3px 0 5px;
|
||||
min-width: 20px;
|
||||
text-align: right;
|
||||
color: #999;
|
||||
-moz-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.CodeMirror-guttermarker { color: black; }
|
||||
.CodeMirror-guttermarker-subtle { color: #999; }
|
||||
|
||||
/* CURSOR */
|
||||
|
||||
.CodeMirror div.CodeMirror-cursor {
|
||||
border-left: 1px solid black;
|
||||
}
|
||||
/* Shown when moving in bi-directional text */
|
||||
.CodeMirror div.CodeMirror-secondarycursor {
|
||||
border-left: 1px solid silver;
|
||||
}
|
||||
.CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursor {
|
||||
width: auto;
|
||||
border: 0;
|
||||
background: #7e7;
|
||||
}
|
||||
.CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursors {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.cm-animate-fat-cursor {
|
||||
width: auto;
|
||||
border: 0;
|
||||
-webkit-animation: blink 1.06s steps(1) infinite;
|
||||
-moz-animation: blink 1.06s steps(1) infinite;
|
||||
animation: blink 1.06s steps(1) infinite;
|
||||
}
|
||||
@-moz-keyframes blink {
|
||||
0% { background: #7e7; }
|
||||
50% { background: none; }
|
||||
100% { background: #7e7; }
|
||||
}
|
||||
@-webkit-keyframes blink {
|
||||
0% { background: #7e7; }
|
||||
50% { background: none; }
|
||||
100% { background: #7e7; }
|
||||
}
|
||||
@keyframes blink {
|
||||
0% { background: #7e7; }
|
||||
50% { background: none; }
|
||||
100% { background: #7e7; }
|
||||
}
|
||||
|
||||
/* Can style cursor different in overwrite (non-insert) mode */
|
||||
div.CodeMirror-overwrite div.CodeMirror-cursor {}
|
||||
|
||||
.cm-tab { display: inline-block; text-decoration: inherit; }
|
||||
|
||||
.CodeMirror-ruler {
|
||||
border-left: 1px solid #ccc;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* DEFAULT THEME */
|
||||
|
||||
.cm-s-default .cm-keyword {color: #708;}
|
||||
.cm-s-default .cm-atom {color: #219;}
|
||||
.cm-s-default .cm-number {color: #164;}
|
||||
.cm-s-default .cm-def {color: #00f;}
|
||||
.cm-s-default .cm-variable,
|
||||
.cm-s-default .cm-punctuation,
|
||||
.cm-s-default .cm-property,
|
||||
.cm-s-default .cm-operator {}
|
||||
.cm-s-default .cm-variable-2 {color: #05a;}
|
||||
.cm-s-default .cm-variable-3 {color: #085;}
|
||||
.cm-s-default .cm-comment {color: #a50;}
|
||||
.cm-s-default .cm-string {color: #a11;}
|
||||
.cm-s-default .cm-string-2 {color: #f50;}
|
||||
.cm-s-default .cm-meta {color: #555;}
|
||||
.cm-s-default .cm-qualifier {color: #555;}
|
||||
.cm-s-default .cm-builtin {color: #30a;}
|
||||
.cm-s-default .cm-bracket {color: #997;}
|
||||
.cm-s-default .cm-tag {color: #170;}
|
||||
.cm-s-default .cm-attribute {color: #00c;}
|
||||
.cm-s-default .cm-header {color: blue;}
|
||||
.cm-s-default .cm-quote {color: #090;}
|
||||
.cm-s-default .cm-hr {color: #999;}
|
||||
.cm-s-default .cm-link {color: #00c;}
|
||||
|
||||
.cm-negative {color: #d44;}
|
||||
.cm-positive {color: #292;}
|
||||
.cm-header, .cm-strong {font-weight: bold;}
|
||||
.cm-em {font-style: italic;}
|
||||
.cm-link {text-decoration: underline;}
|
||||
|
||||
.cm-s-default .cm-error {color: #f00;}
|
||||
.cm-invalidchar {color: #f00;}
|
||||
|
||||
/* Default styles for common addons */
|
||||
|
||||
div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
|
||||
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
|
||||
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
|
||||
.CodeMirror-activeline-background {background: #e8f2ff;}
|
||||
|
||||
/* STOP */
|
||||
|
||||
/* The rest of this file contains styles related to the mechanics of
|
||||
the editor. You probably shouldn't touch them. */
|
||||
|
||||
.CodeMirror {
|
||||
line-height: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.CodeMirror-scroll {
|
||||
/* 30px is the magic margin used to hide the element's real scrollbars */
|
||||
/* See overflow: hidden in .CodeMirror */
|
||||
margin-bottom: -30px; margin-right: -30px;
|
||||
padding-bottom: 30px;
|
||||
height: 100%;
|
||||
outline: none; /* Prevent dragging from highlighting the element */
|
||||
position: relative;
|
||||
-moz-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
.CodeMirror-sizer {
|
||||
position: relative;
|
||||
border-right: 30px solid transparent;
|
||||
-moz-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
/* The fake, visible scrollbars. Used to force redraw during scrolling
|
||||
before actuall scrolling happens, thus preventing shaking and
|
||||
flickering artifacts. */
|
||||
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
display: none;
|
||||
}
|
||||
.CodeMirror-vscrollbar {
|
||||
right: 0; top: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
.CodeMirror-hscrollbar {
|
||||
bottom: 0; left: 0;
|
||||
overflow-y: hidden;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
.CodeMirror-scrollbar-filler {
|
||||
right: 0; bottom: 0;
|
||||
}
|
||||
.CodeMirror-gutter-filler {
|
||||
left: 0; bottom: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-gutters {
|
||||
position: absolute; left: 0; top: 0;
|
||||
padding-bottom: 30px;
|
||||
z-index: 3;
|
||||
}
|
||||
.CodeMirror-gutter {
|
||||
white-space: normal;
|
||||
height: 100%;
|
||||
-moz-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
padding-bottom: 30px;
|
||||
margin-bottom: -32px;
|
||||
display: inline-block;
|
||||
/* Hack to make IE7 behave */
|
||||
*zoom:1;
|
||||
*display:inline;
|
||||
}
|
||||
.CodeMirror-gutter-elt {
|
||||
position: absolute;
|
||||
cursor: default;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.CodeMirror-lines {
|
||||
cursor: text;
|
||||
min-height: 1px; /* prevents collapsing before first draw */
|
||||
}
|
||||
.CodeMirror pre {
|
||||
/* Reset some styles that the rest of the page might have set */
|
||||
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
|
||||
border-width: 0;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
white-space: pre;
|
||||
word-wrap: normal;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
.CodeMirror-wrap pre {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.CodeMirror-linebackground {
|
||||
position: absolute;
|
||||
left: 0; right: 0; top: 0; bottom: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-linewidget {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.CodeMirror-widget {}
|
||||
|
||||
.CodeMirror-wrap .CodeMirror-scroll {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.CodeMirror-measure {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
}
|
||||
.CodeMirror-measure pre { position: static; }
|
||||
|
||||
.CodeMirror div.CodeMirror-cursor {
|
||||
position: absolute;
|
||||
border-right: none;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
div.CodeMirror-cursors {
|
||||
visibility: hidden;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
.CodeMirror-focused div.CodeMirror-cursors {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.CodeMirror-selected { background: #d9d9d9; }
|
||||
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
|
||||
.CodeMirror-crosshair { cursor: crosshair; }
|
||||
|
||||
.cm-searching {
|
||||
background: #ffa;
|
||||
background: rgba(255, 255, 0, .4);
|
||||
}
|
||||
|
||||
/* IE7 hack to prevent it from returning funny offsetTops on the spans */
|
||||
.CodeMirror span { *vertical-align: text-bottom; }
|
||||
|
||||
/* Used to force a border model for a node */
|
||||
.cm-force-border { padding-right: .1px; }
|
||||
|
||||
@media print {
|
||||
/* Hide the cursor when printing */
|
||||
.CodeMirror div.CodeMirror-cursors {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* Help users use markselection to safely style text background */
|
||||
span.CodeMirror-selectedtext { background: none; }
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,125 @@
|
|||
<!doctype html>
|
||||
|
||||
<title>CodeMirror: Language Modes</title>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel=stylesheet href="../doc/docs.css">
|
||||
|
||||
<div id=nav>
|
||||
<a href="http://codemirror.net"><h1>CodeMirror</h1><img id=logo src="../doc/logo.png"></a>
|
||||
|
||||
<ul>
|
||||
<li><a href="../index.html">Home</a>
|
||||
<li><a href="../doc/manual.html">Manual</a>
|
||||
<li><a href="https://github.com/codemirror/codemirror">Code</a>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a class=active href="#">Language modes</a>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<article>
|
||||
|
||||
<h2>Language modes</h2>
|
||||
|
||||
<p>This is a list of every mode in the distribution. Each mode lives
|
||||
in a subdirectory of the <code>mode/</code> directory, and typically
|
||||
defines a single JavaScript file that implements the mode. Loading
|
||||
such file will make the language available to CodeMirror, through
|
||||
the <a href="manual.html#option_mode"><code>mode</code></a>
|
||||
option.</p>
|
||||
|
||||
<div style="-webkit-columns: 100px 2; -moz-columns: 100px 2; columns: 100px 2;">
|
||||
<ul style="margin-top: 0">
|
||||
<li><a href="apl/index.html">APL</a></li>
|
||||
<li><a href="asterisk/index.html">Asterisk dialplan</a></li>
|
||||
<li><a href="clike/index.html">C, C++, C#</a></li>
|
||||
<li><a href="clojure/index.html">Clojure</a></li>
|
||||
<li><a href="cobol/index.html">COBOL</a></li>
|
||||
<li><a href="coffeescript/index.html">CoffeeScript</a></li>
|
||||
<li><a href="commonlisp/index.html">Common Lisp</a></li>
|
||||
<li><a href="css/index.html">CSS</a></li>
|
||||
<li><a href="cypher/index.html">Cypher</a></li>
|
||||
<li><a href="python/index.html">Cython</a></li>
|
||||
<li><a href="d/index.html">D</a></li>
|
||||
<li><a href="django/index.html">Django</a> (templating language)</li>
|
||||
<li><a href="diff/index.html">diff</a></li>
|
||||
<li><a href="dtd/index.html">DTD</a></li>
|
||||
<li><a href="dylan/index.html">Dylan</a></li>
|
||||
<li><a href="ecl/index.html">ECL</a></li>
|
||||
<li><a href="eiffel/index.html">Eiffel</a></li>
|
||||
<li><a href="erlang/index.html">Erlang</a></li>
|
||||
<li><a href="fortran/index.html">Fortran</a></li>
|
||||
<li><a href="mllike/index.html">F#</a></li>
|
||||
<li><a href="gas/index.html">Gas</a> (AT&T-style assembly)</li>
|
||||
<li><a href="gherkin/index.html">Gherkin</a></li>
|
||||
<li><a href="go/index.html">Go</a></li>
|
||||
<li><a href="groovy/index.html">Groovy</a></li>
|
||||
<li><a href="haml/index.html">HAML</a></li>
|
||||
<li><a href="haskell/index.html">Haskell</a></li>
|
||||
<li><a href="haxe/index.html">Haxe</a></li>
|
||||
<li><a href="htmlembedded/index.html">HTML embedded scripts</a></li>
|
||||
<li><a href="htmlmixed/index.html">HTML mixed-mode</a></li>
|
||||
<li><a href="http/index.html">HTTP</a></li>
|
||||
<li><a href="clike/index.html">Java</a></li>
|
||||
<li><a href="jade/index.html">Jade</a></li>
|
||||
<li><a href="javascript/index.html">JavaScript</a></li>
|
||||
<li><a href="jinja2/index.html">Jinja2</a></li>
|
||||
<li><a href="julia/index.html">Julia</a></li>
|
||||
<li><a href="kotlin/index.html">Kotlin</a></li>
|
||||
<li><a href="css/less.html">LESS</a></li>
|
||||
<li><a href="livescript/index.html">LiveScript</a></li>
|
||||
<li><a href="lua/index.html">Lua</a></li>
|
||||
<li><a href="markdown/index.html">Markdown</a> (<a href="gfm/index.html">GitHub-flavour</a>)</li>
|
||||
<li><a href="mirc/index.html">mIRC</a></li>
|
||||
<li><a href="modelica/index.html">Modelica</a></li>
|
||||
<li><a href="nginx/index.html">Nginx</a></li>
|
||||
<li><a href="ntriples/index.html">NTriples</a></li>
|
||||
<li><a href="mllike/index.html">OCaml</a></li>
|
||||
<li><a href="octave/index.html">Octave</a> (MATLAB)</li>
|
||||
<li><a href="pascal/index.html">Pascal</a></li>
|
||||
<li><a href="pegjs/index.html">PEG.js</a></li>
|
||||
<li><a href="perl/index.html">Perl</a></li>
|
||||
<li><a href="php/index.html">PHP</a></li>
|
||||
<li><a href="pig/index.html">Pig Latin</a></li>
|
||||
<li><a href="properties/index.html">Properties files</a></li>
|
||||
<li><a href="puppet/index.html">Puppet</a></li>
|
||||
<li><a href="python/index.html">Python</a></li>
|
||||
<li><a href="q/index.html">Q</a></li>
|
||||
<li><a href="r/index.html">R</a></li>
|
||||
<li><a href="rpm/index.html">RPM</a></li>
|
||||
<li><a href="rst/index.html">reStructuredText</a></li>
|
||||
<li><a href="ruby/index.html">Ruby</a></li>
|
||||
<li><a href="rust/index.html">Rust</a></li>
|
||||
<li><a href="sass/index.html">Sass</a></li>
|
||||
<li><a href="clike/scala.html">Scala</a></li>
|
||||
<li><a href="scheme/index.html">Scheme</a></li>
|
||||
<li><a href="css/scss.html">SCSS</a></li>
|
||||
<li><a href="shell/index.html">Shell</a></li>
|
||||
<li><a href="sieve/index.html">Sieve</a></li>
|
||||
<li><a href="slim/index.html">Slim</a></li>
|
||||
<li><a href="smalltalk/index.html">Smalltalk</a></li>
|
||||
<li><a href="smarty/index.html">Smarty</a></li>
|
||||
<li><a href="smartymixed/index.html">Smarty/HTML mixed</a></li>
|
||||
<li><a href="solr/index.html">Solr</a></li>
|
||||
<li><a href="sql/index.html">SQL</a> (several dialects)</li>
|
||||
<li><a href="sparql/index.html">SPARQL</a></li>
|
||||
<li><a href="stex/index.html">sTeX, LaTeX</a></li>
|
||||
<li><a href="tcl/index.html">Tcl</a></li>
|
||||
<li><a href="textile/index.html">Textile</a></li>
|
||||
<li><a href="tiddlywiki/index.html">Tiddlywiki</a></li>
|
||||
<li><a href="tiki/index.html">Tiki wiki</a></li>
|
||||
<li><a href="toml/index.html">TOML</a></li>
|
||||
<li><a href="tornado/index.html">Tornado</a> (templating language)</li>
|
||||
<li><a href="turtle/index.html">Turtle</a></li>
|
||||
<li><a href="vb/index.html">VB.NET</a></li>
|
||||
<li><a href="vbscript/index.html">VBScript</a></li>
|
||||
<li><a href="velocity/index.html">Velocity</a></li>
|
||||
<li><a href="verilog/index.html">Verilog/SystemVerilog</a></li>
|
||||
<li><a href="xml/index.html">XML/HTML</a></li>
|
||||
<li><a href="xquery/index.html">XQuery</a></li>
|
||||
<li><a href="yaml/index.html">YAML</a></li>
|
||||
<li><a href="z80/index.html">Z80</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</article>
|
|
@ -0,0 +1,144 @@
|
|||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
CodeMirror.modeInfo = [
|
||||
{name: "APL", mime: "text/apl", mode: "apl", ext: ["dyalog", "apl"]},
|
||||
{name: "Asterisk", mime: "text/x-asterisk", mode: "asterisk"},
|
||||
{name: "C", mime: "text/x-csrc", mode: "clike", ext: ["c", "h"]},
|
||||
{name: "C++", mime: "text/x-c++src", mode: "clike", ext: ["cpp", "c++", "hpp", "h++"]},
|
||||
{name: "Cobol", mime: "text/x-cobol", mode: "cobol", ext: ["cob", "cpy"]},
|
||||
{name: "C#", mime: "text/x-csharp", mode: "clike", ext: ["cs"]},
|
||||
{name: "Clojure", mime: "text/x-clojure", mode: "clojure", ext: ["clj"]},
|
||||
{name: "CoffeeScript", mime: "text/x-coffeescript", mode: "coffeescript", ext: ["coffee"]},
|
||||
{name: "Common Lisp", mime: "text/x-common-lisp", mode: "commonlisp", ext: ["cl", "lisp", "el"]},
|
||||
{name: "Cypher", mime: "application/x-cypher-query", mode: "cypher"},
|
||||
{name: "Cython", mime: "text/x-cython", mode: "python", ext: ["pyx", "pxd", "pxi"]},
|
||||
{name: "CSS", mime: "text/css", mode: "css", ext: ["css"]},
|
||||
{name: "CQL", mime: "text/x-cassandra", mode: "sql", ext: ["cql"]},
|
||||
{name: "D", mime: "text/x-d", mode: "d", ext: ["d"]},
|
||||
{name: "diff", mime: "text/x-diff", mode: "diff", ext: ["diff", "patch"]},
|
||||
{name: "DTD", mime: "application/xml-dtd", mode: "dtd", ext: ["dtd"]},
|
||||
{name: "Dylan", mime: "text/x-dylan", mode: "dylan", ext: ["dylan", "dyl", "intr"]},
|
||||
{name: "ECL", mime: "text/x-ecl", mode: "ecl", ext: ["ecl"]},
|
||||
{name: "Eiffel", mime: "text/x-eiffel", mode: "eiffel", ext: ["e"]},
|
||||
{name: "Embedded Javascript", mime: "application/x-ejs", mode: "htmlembedded", ext: ["ejs"]},
|
||||
{name: "Erlang", mime: "text/x-erlang", mode: "erlang", ext: ["erl"]},
|
||||
{name: "Fortran", mime: "text/x-fortran", mode: "fortran", ext: ["f", "for", "f77", "f90"]},
|
||||
{name: "F#", mime: "text/x-fsharp", mode: "mllike", ext: ["fs"]},
|
||||
{name: "Gas", mime: "text/x-gas", mode: "gas", ext: ["s"]},
|
||||
{name: "Gherkin", mime: "text/x-feature", mode: "gherkin", ext: ["feature"]},
|
||||
{name: "GitHub Flavored Markdown", mime: "text/x-gfm", mode: "gfm"},
|
||||
{name: "Go", mime: "text/x-go", mode: "go", ext: ["go"]},
|
||||
{name: "Groovy", mime: "text/x-groovy", mode: "groovy", ext: ["groovy"]},
|
||||
{name: "HAML", mime: "text/x-haml", mode: "haml", ext: ["haml"]},
|
||||
{name: "Haskell", mime: "text/x-haskell", mode: "haskell", ext: ["hs"]},
|
||||
{name: "Haxe", mime: "text/x-haxe", mode: "haxe", ext: ["hx"]},
|
||||
{name: "HXML", mime: "text/x-hxml", mode: "haxe", ext: ["hxml"]},
|
||||
{name: "ASP.NET", mime: "application/x-aspx", mode: "htmlembedded", ext: ["aspx"]},
|
||||
{name: "HTML", mime: "text/html", mode: "htmlmixed", ext: ["html", "htm"]},
|
||||
{name: "HTTP", mime: "message/http", mode: "http"},
|
||||
{name: "Jade", mime: "text/x-jade", mode: "jade", ext: ["jade"]},
|
||||
{name: "Java", mime: "text/x-java", mode: "clike", ext: ["java"]},
|
||||
{name: "Java Server Pages", mime: "application/x-jsp", mode: "htmlembedded", ext: ["jsp"]},
|
||||
{name: "JavaScript", mimes: ["text/javascript", "text/ecmascript", "application/javascript", "application/x-javascript", "application/ecmascript"],
|
||||
mode: "javascript", ext: ["js"]},
|
||||
{name: "JSON", mimes: ["application/json", "application/x-json"], mode: "javascript", ext: ["json", "map"]},
|
||||
{name: "JSON-LD", mime: "application/ld+json", mode: "javascript"},
|
||||
{name: "Jinja2", mime: "null", mode: "jinja2"},
|
||||
{name: "Julia", mime: "text/x-julia", mode: "julia", ext: ["jl"]},
|
||||
{name: "Kotlin", mime: "text/x-kotlin", mode: "kotlin", ext: ["kt"]},
|
||||
{name: "LESS", mime: "text/x-less", mode: "css", ext: ["less"]},
|
||||
{name: "LiveScript", mime: "text/x-livescript", mode: "livescript", ext: ["ls"]},
|
||||
{name: "Lua", mime: "text/x-lua", mode: "lua", ext: ["lua"]},
|
||||
{name: "Markdown (GitHub-flavour)", mime: "text/x-markdown", mode: "markdown", ext: ["markdown", "md", "mkd"]},
|
||||
{name: "mIRC", mime: "text/mirc", mode: "mirc"},
|
||||
{name: "MariaDB SQL", mime: "text/x-mariadb", mode: "sql"},
|
||||
{name: "Modelica", mime: "text/x-modelica", mode: "modelica", ext: ["mo"]},
|
||||
{name: "MS SQL", mime: "text/x-mssql", mode: "sql"},
|
||||
{name: "MySQL", mime: "text/x-mysql", mode: "sql"},
|
||||
{name: "Nginx", mime: "text/x-nginx-conf", mode: "nginx"},
|
||||
{name: "NTriples", mime: "text/n-triples", mode: "ntriples", ext: ["nt"]},
|
||||
{name: "OCaml", mime: "text/x-ocaml", mode: "mllike", ext: ["ml", "mli", "mll", "mly"]},
|
||||
{name: "Octave", mime: "text/x-octave", mode: "octave", ext: ["m"]},
|
||||
{name: "Pascal", mime: "text/x-pascal", mode: "pascal", ext: ["p", "pas"]},
|
||||
{name: "PEG.js", mime: "null", mode: "pegjs"},
|
||||
{name: "Perl", mime: "text/x-perl", mode: "perl", ext: ["pl", "pm"]},
|
||||
{name: "PHP", mime: "application/x-httpd-php", mode: "php", ext: ["php", "php3", "php4", "php5", "phtml"]},
|
||||
{name: "Pig", mime: "text/x-pig", mode: "pig"},
|
||||
{name: "Plain Text", mime: "text/plain", mode: "null", ext: ["txt", "text", "conf", "def", "list", "log"]},
|
||||
{name: "PLSQL", mime: "text/x-plsql", mode: "sql"},
|
||||
{name: "Properties files", mime: "text/x-properties", mode: "properties", ext: ["properties", "ini", "in"]},
|
||||
{name: "Python", mime: "text/x-python", mode: "python", ext: ["py", "pyw"]},
|
||||
{name: "Puppet", mime: "text/x-puppet", mode: "puppet", ext: ["pp"]},
|
||||
{name: "Q", mime: "text/x-q", mode: "q", ext: ["q"]},
|
||||
{name: "R", mime: "text/x-rsrc", mode: "r", ext: ["r"]},
|
||||
{name: "reStructuredText", mime: "text/x-rst", mode: "rst", ext: ["rst"]},
|
||||
{name: "Ruby", mime: "text/x-ruby", mode: "ruby", ext: ["rb"]},
|
||||
{name: "Rust", mime: "text/x-rustsrc", mode: "rust", ext: ["rs"]},
|
||||
{name: "Sass", mime: "text/x-sass", mode: "sass", ext: ["sass"]},
|
||||
{name: "Scala", mime: "text/x-scala", mode: "clike", ext: ["scala"]},
|
||||
{name: "Scheme", mime: "text/x-scheme", mode: "scheme", ext: ["scm", "ss"]},
|
||||
{name: "SCSS", mime: "text/x-scss", mode: "css", ext: ["scss"]},
|
||||
{name: "Shell", mime: "text/x-sh", mode: "shell", ext: ["sh", "ksh", "bash"]},
|
||||
{name: "Sieve", mime: "application/sieve", mode: "sieve"},
|
||||
{name: "Slim", mimes: ["text/x-slim", "application/x-slim"], mode: "slim"},
|
||||
{name: "Smalltalk", mime: "text/x-stsrc", mode: "smalltalk", ext: ["st"]},
|
||||
{name: "Smarty", mime: "text/x-smarty", mode: "smarty", ext: ["tpl"]},
|
||||
{name: "SmartyMixed", mime: "text/x-smarty", mode: "smartymixed"},
|
||||
{name: "Solr", mime: "text/x-solr", mode: "solr"},
|
||||
{name: "SPARQL", mime: "application/x-sparql-query", mode: "sparql", ext: ["sparql"]},
|
||||
{name: "SQL", mime: "text/x-sql", mode: "sql", ext: ["sql"]},
|
||||
{name: "MariaDB", mime: "text/x-mariadb", mode: "sql"},
|
||||
{name: "sTeX", mime: "text/x-stex", mode: "stex"},
|
||||
{name: "LaTeX", mime: "text/x-latex", mode: "stex", ext: ["text", "ltx"]},
|
||||
{name: "SystemVerilog", mime: "text/x-systemverilog", mode: "verilog", ext: ["v"]},
|
||||
{name: "Tcl", mime: "text/x-tcl", mode: "tcl", ext: ["tcl"]},
|
||||
{name: "Textile", mime: "text/x-textile", mode: "textile"},
|
||||
{name: "TiddlyWiki ", mime: "text/x-tiddlywiki", mode: "tiddlywiki"},
|
||||
{name: "Tiki wiki", mime: "text/tiki", mode: "tiki"},
|
||||
{name: "TOML", mime: "text/x-toml", mode: "toml"},
|
||||
{name: "Tornado", mime: "text/x-tornado", mode: "tornado"},
|
||||
{name: "Turtle", mime: "text/turtle", mode: "turtle", ext: ["ttl"]},
|
||||
{name: "TypeScript", mime: "application/typescript", mode: "javascript", ext: ["ts"]},
|
||||
{name: "VB.NET", mime: "text/x-vb", mode: "vb", ext: ["vb"]},
|
||||
{name: "VBScript", mime: "text/vbscript", mode: "vbscript"},
|
||||
{name: "Velocity", mime: "text/velocity", mode: "velocity", ext: ["vtl"]},
|
||||
{name: "Verilog", mime: "text/x-verilog", mode: "verilog", ext: ["v"]},
|
||||
{name: "XML", mimes: ["application/xml", "text/xml"], mode: "xml", ext: ["xml", "xsl", "xsd"]},
|
||||
{name: "XQuery", mime: "application/xquery", mode: "xquery", ext: ["xy", "xquery"]},
|
||||
{name: "YAML", mime: "text/x-yaml", mode: "yaml", ext: ["yaml"]},
|
||||
{name: "Z80", mime: "text/x-z80", mode: "z80", ext: ["z80"]}
|
||||
];
|
||||
// Ensure all modes have a mime property for backwards compatibility
|
||||
for (var i = 0; i < CodeMirror.modeInfo.length; i++) {
|
||||
var info = CodeMirror.modeInfo[i];
|
||||
if (info.mimes) info.mime = info.mimes[0];
|
||||
}
|
||||
|
||||
CodeMirror.findModeByMIME = function(mime) {
|
||||
for (var i = 0; i < CodeMirror.modeInfo.length; i++) {
|
||||
var info = CodeMirror.modeInfo[i];
|
||||
if (info.mime == mime) return info;
|
||||
if (info.mimes) for (var j = 0; j < info.mimes.length; j++)
|
||||
if (info.mimes[j] == mime) return info;
|
||||
}
|
||||
};
|
||||
|
||||
CodeMirror.findModeByExtension = function(ext) {
|
||||
for (var i = 0; i < CodeMirror.modeInfo.length; i++) {
|
||||
var info = CodeMirror.modeInfo[i];
|
||||
if (info.ext) for (var j = 0; j < info.ext.length; j++)
|
||||
if (info.ext[j] == ext) return info;
|
||||
}
|
||||
};
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
<!doctype html>
|
||||
|
||||
<title>CodeMirror: Shell mode</title>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel=stylesheet href="../../doc/docs.css">
|
||||
|
||||
<link rel=stylesheet href=../../lib/codemirror.css>
|
||||
<script src=../../lib/codemirror.js></script>
|
||||
<script src="../../addon/edit/matchbrackets.js"></script>
|
||||
<script src=shell.js></script>
|
||||
<style type=text/css>
|
||||
.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;}
|
||||
</style>
|
||||
<div id=nav>
|
||||
<a href="http://codemirror.net"><h1>CodeMirror</h1><img id=logo src="../../doc/logo.png"></a>
|
||||
|
||||
<ul>
|
||||
<li><a href="../../index.html">Home</a>
|
||||
<li><a href="../../doc/manual.html">Manual</a>
|
||||
<li><a href="https://github.com/codemirror/codemirror">Code</a>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a href="../index.html">Language modes</a>
|
||||
<li><a class=active href="#">Shell</a>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<article>
|
||||
<h2>Shell mode</h2>
|
||||
|
||||
|
||||
<textarea id=code>
|
||||
#!/bin/bash
|
||||
|
||||
# clone the repository
|
||||
git clone http://github.com/garden/tree
|
||||
|
||||
# generate HTTPS credentials
|
||||
cd tree
|
||||
openssl genrsa -aes256 -out https.key 1024
|
||||
openssl req -new -nodes -key https.key -out https.csr
|
||||
openssl x509 -req -days 365 -in https.csr -signkey https.key -out https.crt
|
||||
cp https.key{,.orig}
|
||||
openssl rsa -in https.key.orig -out https.key
|
||||
|
||||
# start the server in HTTPS mode
|
||||
cd web
|
||||
sudo node ../server.js 443 'yes' >> ../node.log &
|
||||
|
||||
# here is how to stop the server
|
||||
for pid in `ps aux | grep 'node ../server.js' | awk '{print $2}'` ; do
|
||||
sudo kill -9 $pid 2> /dev/null
|
||||
done
|
||||
|
||||
exit 0</textarea>
|
||||
|
||||
<script>
|
||||
var editor = CodeMirror.fromTextArea(document.getElementById('code'), {
|
||||
mode: 'shell',
|
||||
lineNumbers: true,
|
||||
matchBrackets: true
|
||||
});
|
||||
</script>
|
||||
|
||||
<p><strong>MIME types defined:</strong> <code>text/x-sh</code>.</p>
|
||||
</article>
|
|
@ -0,0 +1,138 @@
|
|||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
CodeMirror.defineMode('shell', function() {
|
||||
|
||||
var words = {};
|
||||
function define(style, string) {
|
||||
var split = string.split(' ');
|
||||
for(var i = 0; i < split.length; i++) {
|
||||
words[split[i]] = style;
|
||||
}
|
||||
};
|
||||
|
||||
// Atoms
|
||||
define('atom', 'true false');
|
||||
|
||||
// Keywords
|
||||
define('keyword', 'if then do else elif while until for in esac fi fin ' +
|
||||
'fil done exit set unset export function');
|
||||
|
||||
// Commands
|
||||
define('builtin', 'ab awk bash beep cat cc cd chown chmod chroot clear cp ' +
|
||||
'curl cut diff echo find gawk gcc get git grep kill killall ln ls make ' +
|
||||
'mkdir openssl mv nc node npm ping ps restart rm rmdir sed service sh ' +
|
||||
'shopt shred source sort sleep ssh start stop su sudo tee telnet top ' +
|
||||
'touch vi vim wall wc wget who write yes zsh');
|
||||
|
||||
function tokenBase(stream, state) {
|
||||
if (stream.eatSpace()) return null;
|
||||
|
||||
var sol = stream.sol();
|
||||
var ch = stream.next();
|
||||
|
||||
if (ch === '\\') {
|
||||
stream.next();
|
||||
return null;
|
||||
}
|
||||
if (ch === '\'' || ch === '"' || ch === '`') {
|
||||
state.tokens.unshift(tokenString(ch));
|
||||
return tokenize(stream, state);
|
||||
}
|
||||
if (ch === '#') {
|
||||
if (sol && stream.eat('!')) {
|
||||
stream.skipToEnd();
|
||||
return 'meta'; // 'comment'?
|
||||
}
|
||||
stream.skipToEnd();
|
||||
return 'comment';
|
||||
}
|
||||
if (ch === '$') {
|
||||
state.tokens.unshift(tokenDollar);
|
||||
return tokenize(stream, state);
|
||||
}
|
||||
if (ch === '+' || ch === '=') {
|
||||
return 'operator';
|
||||
}
|
||||
if (ch === '-') {
|
||||
stream.eat('-');
|
||||
stream.eatWhile(/\w/);
|
||||
return 'attribute';
|
||||
}
|
||||
if (/\d/.test(ch)) {
|
||||
stream.eatWhile(/\d/);
|
||||
if(stream.eol() || !/\w/.test(stream.peek())) {
|
||||
return 'number';
|
||||
}
|
||||
}
|
||||
stream.eatWhile(/[\w-]/);
|
||||
var cur = stream.current();
|
||||
if (stream.peek() === '=' && /\w+/.test(cur)) return 'def';
|
||||
return words.hasOwnProperty(cur) ? words[cur] : null;
|
||||
}
|
||||
|
||||
function tokenString(quote) {
|
||||
return function(stream, state) {
|
||||
var next, end = false, escaped = false;
|
||||
while ((next = stream.next()) != null) {
|
||||
if (next === quote && !escaped) {
|
||||
end = true;
|
||||
break;
|
||||
}
|
||||
if (next === '$' && !escaped && quote !== '\'') {
|
||||
escaped = true;
|
||||
stream.backUp(1);
|
||||
state.tokens.unshift(tokenDollar);
|
||||
break;
|
||||
}
|
||||
escaped = !escaped && next === '\\';
|
||||
}
|
||||
if (end || !escaped) {
|
||||
state.tokens.shift();
|
||||
}
|
||||
return (quote === '`' || quote === ')' ? 'quote' : 'string');
|
||||
};
|
||||
};
|
||||
|
||||
var tokenDollar = function(stream, state) {
|
||||
if (state.tokens.length > 1) stream.eat('$');
|
||||
var ch = stream.next(), hungry = /\w/;
|
||||
if (ch === '{') hungry = /[^}]/;
|
||||
if (ch === '(') {
|
||||
state.tokens[0] = tokenString(')');
|
||||
return tokenize(stream, state);
|
||||
}
|
||||
if (!/\d/.test(ch)) {
|
||||
stream.eatWhile(hungry);
|
||||
stream.eat('}');
|
||||
}
|
||||
state.tokens.shift();
|
||||
return 'def';
|
||||
};
|
||||
|
||||
function tokenize(stream, state) {
|
||||
return (state.tokens[0] || tokenBase) (stream, state);
|
||||
};
|
||||
|
||||
return {
|
||||
startState: function() {return {tokens:[]};},
|
||||
token: function(stream, state) {
|
||||
return tokenize(stream, state);
|
||||
},
|
||||
lineComment: '#'
|
||||
};
|
||||
});
|
||||
|
||||
CodeMirror.defineMIME('text/x-sh', 'shell');
|
||||
|
||||
});
|
|
@ -0,0 +1,58 @@
|
|||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
(function() {
|
||||
var mode = CodeMirror.getMode({}, "shell");
|
||||
function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); }
|
||||
|
||||
MT("var",
|
||||
"text [def $var] text");
|
||||
MT("varBraces",
|
||||
"text[def ${var}]text");
|
||||
MT("varVar",
|
||||
"text [def $a$b] text");
|
||||
MT("varBracesVarBraces",
|
||||
"text[def ${a}${b}]text");
|
||||
|
||||
MT("singleQuotedVar",
|
||||
"[string 'text $var text']");
|
||||
MT("singleQuotedVarBraces",
|
||||
"[string 'text ${var} text']");
|
||||
|
||||
MT("doubleQuotedVar",
|
||||
'[string "text ][def $var][string text"]');
|
||||
MT("doubleQuotedVarBraces",
|
||||
'[string "text][def ${var}][string text"]');
|
||||
MT("doubleQuotedVarPunct",
|
||||
'[string "text ][def $@][string text"]');
|
||||
MT("doubleQuotedVarVar",
|
||||
'[string "][def $a$b][string "]');
|
||||
MT("doubleQuotedVarBracesVarBraces",
|
||||
'[string "][def ${a}${b}][string "]');
|
||||
|
||||
MT("notAString",
|
||||
"text\\'text");
|
||||
MT("escapes",
|
||||
"outside\\'\\\"\\`\\\\[string \"inside\\`\\'\\\"\\\\`\\$notAVar\"]outside\\$\\(notASubShell\\)");
|
||||
|
||||
MT("subshell",
|
||||
"[builtin echo] [quote $(whoami)] s log, stardate [quote `date`].");
|
||||
MT("doubleQuotedSubshell",
|
||||
"[builtin echo] [string \"][quote $(whoami)][string 's log, stardate `date`.\"]");
|
||||
|
||||
MT("hashbang",
|
||||
"[meta #!/bin/bash]");
|
||||
MT("comment",
|
||||
"text [comment # Blurb]");
|
||||
|
||||
MT("numbers",
|
||||
"[number 0] [number 1] [number 2]");
|
||||
MT("keywords",
|
||||
"[keyword while] [atom true]; [keyword do]",
|
||||
" [builtin sleep] [number 3]",
|
||||
"[keyword done]");
|
||||
MT("options",
|
||||
"[builtin ls] [attribute -l] [attribute --human-readable]");
|
||||
MT("operator",
|
||||
"[def var][operator =]value");
|
||||
})();
|
|
@ -0,0 +1,105 @@
|
|||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.attach = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
function attach(term, socket, bidirectional, buffered) {
|
||||
var addonTerminal = term;
|
||||
bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional;
|
||||
addonTerminal.__socket = socket;
|
||||
addonTerminal.__flushBuffer = function () {
|
||||
addonTerminal.write(addonTerminal.__attachSocketBuffer);
|
||||
addonTerminal.__attachSocketBuffer = null;
|
||||
};
|
||||
addonTerminal.__pushToBuffer = function (data) {
|
||||
if (addonTerminal.__attachSocketBuffer) {
|
||||
addonTerminal.__attachSocketBuffer += data;
|
||||
}
|
||||
else {
|
||||
addonTerminal.__attachSocketBuffer = data;
|
||||
setTimeout(addonTerminal.__flushBuffer, 10);
|
||||
}
|
||||
};
|
||||
var myTextDecoder;
|
||||
addonTerminal.__getMessage = function (ev) {
|
||||
var _this = this;
|
||||
var str;
|
||||
if (typeof ev.data === 'object') {
|
||||
if (!myTextDecoder) {
|
||||
myTextDecoder = new TextDecoder();
|
||||
}
|
||||
if (ev.data instanceof ArrayBuffer) {
|
||||
str = myTextDecoder.decode(ev.data);
|
||||
displayData(str);
|
||||
}
|
||||
else {
|
||||
var fileReader = new FileReader();
|
||||
fileReader.addEventListener('load', function () {
|
||||
str = myTextDecoder.decode(_this.result);
|
||||
displayData(str);
|
||||
});
|
||||
fileReader.readAsArrayBuffer(ev.data);
|
||||
}
|
||||
}
|
||||
else if (typeof ev.data === 'string') {
|
||||
displayData(ev.data);
|
||||
}
|
||||
else {
|
||||
throw Error("Cannot handle \"" + typeof ev.data + "\" websocket message.");
|
||||
}
|
||||
};
|
||||
function displayData(str, data) {
|
||||
if (buffered) {
|
||||
addonTerminal.__pushToBuffer(str || data);
|
||||
}
|
||||
else {
|
||||
addonTerminal.write(str || data);
|
||||
}
|
||||
}
|
||||
addonTerminal.__sendData = function (data) {
|
||||
if (socket.readyState !== 1) {
|
||||
return;
|
||||
}
|
||||
socket.send(data);
|
||||
};
|
||||
addonTerminal._core.register(addSocketListener(socket, 'message', addonTerminal.__getMessage));
|
||||
if (bidirectional) {
|
||||
addonTerminal._core.register(addonTerminal.addDisposableListener('data', addonTerminal.__sendData));
|
||||
}
|
||||
addonTerminal._core.register(addSocketListener(socket, 'close', function () { return detach(addonTerminal, socket); }));
|
||||
addonTerminal._core.register(addSocketListener(socket, 'error', function () { return detach(addonTerminal, socket); }));
|
||||
}
|
||||
exports.attach = attach;
|
||||
function addSocketListener(socket, type, handler) {
|
||||
socket.addEventListener(type, handler);
|
||||
return {
|
||||
dispose: function () {
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
socket.removeEventListener(type, handler);
|
||||
handler = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
function detach(term, socket) {
|
||||
var addonTerminal = term;
|
||||
addonTerminal.off('data', addonTerminal.__sendData);
|
||||
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;
|
||||
if (socket) {
|
||||
socket.removeEventListener('message', addonTerminal.__getMessage);
|
||||
}
|
||||
delete addonTerminal.__socket;
|
||||
}
|
||||
exports.detach = detach;
|
||||
function apply(terminalConstructor) {
|
||||
terminalConstructor.prototype.attach = function (socket, bidirectional, buffered) {
|
||||
attach(this, socket, bidirectional, buffered);
|
||||
};
|
||||
terminalConstructor.prototype.detach = function (socket) {
|
||||
detach(this, socket);
|
||||
};
|
||||
}
|
||||
exports.apply = apply;
|
||||
|
||||
},{}]},{},[1])(1)
|
||||
});
|
||||
//# sourceMappingURL=attach.js.map
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,51 @@
|
|||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.fit = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
function proposeGeometry(term) {
|
||||
if (!term.element.parentElement) {
|
||||
return null;
|
||||
}
|
||||
var parentElementStyle = window.getComputedStyle(term.element.parentElement);
|
||||
var parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
||||
var parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
|
||||
var elementStyle = window.getComputedStyle(term.element);
|
||||
var elementPadding = {
|
||||
top: parseInt(elementStyle.getPropertyValue('padding-top')),
|
||||
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
|
||||
right: parseInt(elementStyle.getPropertyValue('padding-right')),
|
||||
left: parseInt(elementStyle.getPropertyValue('padding-left'))
|
||||
};
|
||||
var elementPaddingVer = elementPadding.top + elementPadding.bottom;
|
||||
var elementPaddingHor = elementPadding.right + elementPadding.left;
|
||||
var availableHeight = parentElementHeight - elementPaddingVer;
|
||||
var availableWidth = parentElementWidth - elementPaddingHor - term._core.viewport.scrollBarWidth;
|
||||
var geometry = {
|
||||
cols: Math.floor(availableWidth / term._core.renderer.dimensions.actualCellWidth),
|
||||
rows: Math.floor(availableHeight / term._core.renderer.dimensions.actualCellHeight)
|
||||
};
|
||||
return geometry;
|
||||
}
|
||||
exports.proposeGeometry = proposeGeometry;
|
||||
function fit(term) {
|
||||
var geometry = proposeGeometry(term);
|
||||
if (geometry) {
|
||||
if (term.rows !== geometry.rows || term.cols !== geometry.cols) {
|
||||
term._core.renderer.clear();
|
||||
term.resize(geometry.cols, geometry.rows);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.fit = fit;
|
||||
function apply(terminalConstructor) {
|
||||
terminalConstructor.prototype.proposeGeometry = function () {
|
||||
return proposeGeometry(this);
|
||||
};
|
||||
terminalConstructor.prototype.fit = function () {
|
||||
fit(this);
|
||||
};
|
||||
}
|
||||
exports.apply = apply;
|
||||
|
||||
},{}]},{},[1])(1)
|
||||
});
|
||||
//# sourceMappingURL=fit.js.map
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"fit.js","sources":["../../../src/addons/fit/fit.ts","../../../node_modules/browser-pack/_prelude.js"],"sourcesContent":["/**\n * Copyright (c) 2014 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Fit terminal columns and rows to the dimensions of its DOM element.\n *\n * ## Approach\n *\n * Rows: Truncate the division of the terminal parent element height by the\n * terminal row height.\n * Columns: Truncate the division of the terminal parent element width by the\n * terminal character width (apply display: inline at the terminal\n * row and truncate its width with the current number of columns).\n */\n\nimport { Terminal } from 'xterm';\n\nexport interface IGeometry {\n rows: number;\n cols: number;\n}\n\nexport function proposeGeometry(term: Terminal): IGeometry {\n if (!term.element.parentElement) {\n return null;\n }\n const parentElementStyle = window.getComputedStyle(term.element.parentElement);\n const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));\n const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));\n const elementStyle = window.getComputedStyle(term.element);\n const elementPadding = {\n top: parseInt(elementStyle.getPropertyValue('padding-top')),\n bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),\n right: parseInt(elementStyle.getPropertyValue('padding-right')),\n left: parseInt(elementStyle.getPropertyValue('padding-left'))\n };\n const elementPaddingVer = elementPadding.top + elementPadding.bottom;\n const elementPaddingHor = elementPadding.right + elementPadding.left;\n const availableHeight = parentElementHeight - elementPaddingVer;\n const availableWidth = parentElementWidth - elementPaddingHor - (<any>term)._core.viewport.scrollBarWidth;\n const geometry = {\n cols: Math.floor(availableWidth / (<any>term)._core.renderer.dimensions.actualCellWidth),\n rows: Math.floor(availableHeight / (<any>term)._core.renderer.dimensions.actualCellHeight)\n };\n return geometry;\n}\n\nexport function fit(term: Terminal): void {\n const geometry = proposeGeometry(term);\n if (geometry) {\n // Force a full render\n if (term.rows !== geometry.rows || term.cols !== geometry.cols) {\n (<any>term)._core.renderer.clear();\n term.resize(geometry.cols, geometry.rows);\n }\n }\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n (<any>terminalConstructor.prototype).proposeGeometry = function (): IGeometry {\n return proposeGeometry(this);\n };\n\n (<any>terminalConstructor.prototype).fit = function (): void {\n fit(this);\n };\n}\n",null],"names":[],"mappings":"ACAA;;;ADsBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAvBA;AAyBA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AATA;AAWA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AARA;"}
|
|
@ -0,0 +1,10 @@
|
|||
.xterm.fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: auto;
|
||||
height: auto;
|
||||
z-index: 255;
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.fullscreen = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
function toggleFullScreen(term, fullscreen) {
|
||||
var fn;
|
||||
if (typeof fullscreen === 'undefined') {
|
||||
fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add';
|
||||
}
|
||||
else if (!fullscreen) {
|
||||
fn = 'remove';
|
||||
}
|
||||
else {
|
||||
fn = 'add';
|
||||
}
|
||||
term.element.classList[fn]('fullscreen');
|
||||
}
|
||||
exports.toggleFullScreen = toggleFullScreen;
|
||||
function apply(terminalConstructor) {
|
||||
terminalConstructor.prototype.toggleFullScreen = function (fullscreen) {
|
||||
toggleFullScreen(this, fullscreen);
|
||||
};
|
||||
}
|
||||
exports.apply = apply;
|
||||
|
||||
},{}]},{},[1])(1)
|
||||
});
|
||||
//# sourceMappingURL=fullscreen.js.map
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"fullscreen.js","sources":["../../../src/addons/fullscreen/fullscreen.ts","../../../node_modules/browser-pack/_prelude.js"],"sourcesContent":["/**\n * Copyright (c) 2014 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Terminal } from 'xterm';\n\n/**\n * Toggle the given terminal's fullscreen mode.\n * @param term The terminal to toggle full screen mode\n * @param fullscreen Toggle fullscreen on (true) or off (false)\n */\nexport function toggleFullScreen(term: Terminal, fullscreen: boolean): void {\n let fn: string;\n\n if (typeof fullscreen === 'undefined') {\n fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add';\n } else if (!fullscreen) {\n fn = 'remove';\n } else {\n fn = 'add';\n }\n\n term.element.classList[fn]('fullscreen');\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n (<any>terminalConstructor.prototype).toggleFullScreen = function (fullscreen: boolean): void {\n toggleFullScreen(this, fullscreen);\n };\n}\n",null],"names":[],"mappings":"ACAA;;;ADYA;AACA;AAEA;AACA;AACA;AAAA;AACA;AACA;AAAA;AACA;AACA;AAEA;AACA;AAZA;AAcA;AACA;AACA;AACA;AACA;AAJA;"}
|
|
@ -0,0 +1,126 @@
|
|||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.search = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var SearchHelper = (function () {
|
||||
function SearchHelper(_terminal) {
|
||||
this._terminal = _terminal;
|
||||
}
|
||||
SearchHelper.prototype.findNext = function (term) {
|
||||
if (!term || term.length === 0) {
|
||||
return false;
|
||||
}
|
||||
var result;
|
||||
var startRow = this._terminal._core.buffer.ydisp;
|
||||
if (this._terminal._core.selectionManager.selectionEnd) {
|
||||
startRow = this._terminal._core.selectionManager.selectionEnd[1];
|
||||
}
|
||||
for (var y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
|
||||
result = this._findInLine(term, y);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!result) {
|
||||
for (var y = 0; y < startRow; y++) {
|
||||
result = this._findInLine(term, y);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this._selectResult(result);
|
||||
};
|
||||
SearchHelper.prototype.findPrevious = function (term) {
|
||||
if (!term || term.length === 0) {
|
||||
return false;
|
||||
}
|
||||
var result;
|
||||
var startRow = this._terminal._core.buffer.ydisp;
|
||||
if (this._terminal._core.selectionManager.selectionStart) {
|
||||
startRow = this._terminal._core.selectionManager.selectionStart[1];
|
||||
}
|
||||
for (var y = startRow - 1; y >= 0; y--) {
|
||||
result = this._findInLine(term, y);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!result) {
|
||||
for (var y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
|
||||
result = this._findInLine(term, y);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this._selectResult(result);
|
||||
};
|
||||
SearchHelper.prototype._findInLine = function (term, y) {
|
||||
var lowerStringLine = this._terminal._core.buffer.translateBufferLineToString(y, true).toLowerCase();
|
||||
var lowerTerm = term.toLowerCase();
|
||||
var searchIndex = lowerStringLine.indexOf(lowerTerm);
|
||||
if (searchIndex >= 0) {
|
||||
var line = this._terminal._core.buffer.lines.get(y);
|
||||
for (var i = 0; i < searchIndex; i++) {
|
||||
var charData = line[i];
|
||||
var char = charData[1];
|
||||
if (char.length > 1) {
|
||||
searchIndex -= char.length - 1;
|
||||
}
|
||||
var charWidth = charData[2];
|
||||
if (charWidth === 0) {
|
||||
searchIndex++;
|
||||
}
|
||||
}
|
||||
return {
|
||||
term: term,
|
||||
col: searchIndex,
|
||||
row: y
|
||||
};
|
||||
}
|
||||
};
|
||||
SearchHelper.prototype._selectResult = function (result) {
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
this._terminal._core.selectionManager.setSelection(result.col, result.row, result.term.length);
|
||||
this._terminal.scrollLines(result.row - this._terminal._core.buffer.ydisp);
|
||||
return true;
|
||||
};
|
||||
return SearchHelper;
|
||||
}());
|
||||
exports.SearchHelper = SearchHelper;
|
||||
|
||||
},{}],2:[function(require,module,exports){
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var SearchHelper_1 = require("./SearchHelper");
|
||||
function findNext(terminal, term) {
|
||||
var addonTerminal = terminal;
|
||||
if (!addonTerminal.__searchHelper) {
|
||||
addonTerminal.__searchHelper = new SearchHelper_1.SearchHelper(addonTerminal);
|
||||
}
|
||||
return addonTerminal.__searchHelper.findNext(term);
|
||||
}
|
||||
exports.findNext = findNext;
|
||||
function findPrevious(terminal, term) {
|
||||
var addonTerminal = terminal;
|
||||
if (!addonTerminal.__searchHelper) {
|
||||
addonTerminal.__searchHelper = new SearchHelper_1.SearchHelper(addonTerminal);
|
||||
}
|
||||
return addonTerminal.__searchHelper.findPrevious(term);
|
||||
}
|
||||
exports.findPrevious = findPrevious;
|
||||
function apply(terminalConstructor) {
|
||||
terminalConstructor.prototype.findNext = function (term) {
|
||||
return findNext(this, term);
|
||||
};
|
||||
terminalConstructor.prototype.findPrevious = function (term) {
|
||||
return findPrevious(this, term);
|
||||
};
|
||||
}
|
||||
exports.apply = apply;
|
||||
|
||||
},{"./SearchHelper":1}]},{},[2])(2)
|
||||
});
|
||||
//# sourceMappingURL=search.js.map
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,69 @@
|
|||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.terminado = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
function terminadoAttach(term, socket, bidirectional, buffered) {
|
||||
var addonTerminal = term;
|
||||
bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional;
|
||||
addonTerminal.__socket = socket;
|
||||
addonTerminal.__flushBuffer = function () {
|
||||
addonTerminal.write(addonTerminal.__attachSocketBuffer);
|
||||
addonTerminal.__attachSocketBuffer = null;
|
||||
};
|
||||
addonTerminal.__pushToBuffer = function (data) {
|
||||
if (addonTerminal.__attachSocketBuffer) {
|
||||
addonTerminal.__attachSocketBuffer += data;
|
||||
}
|
||||
else {
|
||||
addonTerminal.__attachSocketBuffer = data;
|
||||
setTimeout(addonTerminal.__flushBuffer, 10);
|
||||
}
|
||||
};
|
||||
addonTerminal.__getMessage = function (ev) {
|
||||
var data = JSON.parse(ev.data);
|
||||
if (data[0] === 'stdout') {
|
||||
if (buffered) {
|
||||
addonTerminal.__pushToBuffer(data[1]);
|
||||
}
|
||||
else {
|
||||
addonTerminal.write(data[1]);
|
||||
}
|
||||
}
|
||||
};
|
||||
addonTerminal.__sendData = function (data) {
|
||||
socket.send(JSON.stringify(['stdin', data]));
|
||||
};
|
||||
addonTerminal.__setSize = function (size) {
|
||||
socket.send(JSON.stringify(['set_size', size.rows, size.cols]));
|
||||
};
|
||||
socket.addEventListener('message', addonTerminal.__getMessage);
|
||||
if (bidirectional) {
|
||||
addonTerminal.on('data', addonTerminal.__sendData);
|
||||
}
|
||||
addonTerminal.on('resize', addonTerminal.__setSize);
|
||||
socket.addEventListener('close', function () { return terminadoDetach(addonTerminal, socket); });
|
||||
socket.addEventListener('error', function () { return terminadoDetach(addonTerminal, socket); });
|
||||
}
|
||||
exports.terminadoAttach = terminadoAttach;
|
||||
function terminadoDetach(term, socket) {
|
||||
var addonTerminal = term;
|
||||
addonTerminal.off('data', addonTerminal.__sendData);
|
||||
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;
|
||||
if (socket) {
|
||||
socket.removeEventListener('message', addonTerminal.__getMessage);
|
||||
}
|
||||
delete addonTerminal.__socket;
|
||||
}
|
||||
exports.terminadoDetach = terminadoDetach;
|
||||
function apply(terminalConstructor) {
|
||||
terminalConstructor.prototype.terminadoAttach = function (socket, bidirectional, buffered) {
|
||||
return terminadoAttach(this, socket, bidirectional, buffered);
|
||||
};
|
||||
terminalConstructor.prototype.terminadoDetach = function (socket) {
|
||||
return terminadoDetach(this, socket);
|
||||
};
|
||||
}
|
||||
exports.apply = apply;
|
||||
|
||||
},{}]},{},[1])(1)
|
||||
});
|
||||
//# sourceMappingURL=terminado.js.map
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"terminado.js","sources":["../../../src/addons/terminado/terminado.ts","../../../node_modules/browser-pack/_prelude.js"],"sourcesContent":["/**\n * Copyright (c) 2016 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * This module provides methods for attaching a terminal to a terminado\n * WebSocket stream.\n */\n\nimport { Terminal } from 'xterm';\nimport { ITerminadoAddonTerminal } from './Interfaces';\n\n/**\n * Attaches the given terminal to the given socket.\n *\n * @param term The terminal to be attached to the given socket.\n * @param socket The socket to attach the current terminal.\n * @param bidirectional Whether the terminal should send data to the socket as well.\n * @param buffered Whether the rendering of incoming data should happen instantly or at a maximum\n * frequency of 1 rendering per 10ms.\n */\nexport function terminadoAttach(term: Terminal, socket: WebSocket, bidirectional: boolean, buffered: boolean): void {\n const addonTerminal = <ITerminadoAddonTerminal>term;\n bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional;\n addonTerminal.__socket = socket;\n\n addonTerminal.__flushBuffer = () => {\n addonTerminal.write(addonTerminal.__attachSocketBuffer);\n addonTerminal.__attachSocketBuffer = null;\n };\n\n addonTerminal.__pushToBuffer = (data: string) => {\n if (addonTerminal.__attachSocketBuffer) {\n addonTerminal.__attachSocketBuffer += data;\n } else {\n addonTerminal.__attachSocketBuffer = data;\n setTimeout(addonTerminal.__flushBuffer, 10);\n }\n };\n\n addonTerminal.__getMessage = (ev: MessageEvent) => {\n const data = JSON.parse(ev.data);\n if (data[0] === 'stdout') {\n if (buffered) {\n addonTerminal.__pushToBuffer(data[1]);\n } else {\n addonTerminal.write(data[1]);\n }\n }\n };\n\n addonTerminal.__sendData = (data: string) => {\n socket.send(JSON.stringify(['stdin', data]));\n };\n\n addonTerminal.__setSize = (size: {rows: number, cols: number}) => {\n socket.send(JSON.stringify(['set_size', size.rows, size.cols]));\n };\n\n socket.addEventListener('message', addonTerminal.__getMessage);\n\n if (bidirectional) {\n addonTerminal.on('data', addonTerminal.__sendData);\n }\n addonTerminal.on('resize', addonTerminal.__setSize);\n\n socket.addEventListener('close', () => terminadoDetach(addonTerminal, socket));\n socket.addEventListener('error', () => terminadoDetach(addonTerminal, socket));\n}\n\n/**\n * Detaches the given terminal from the given socket\n *\n * @param term The terminal to be detached from the given socket.\n * @param socket The socket from which to detach the current terminal.\n */\nexport function terminadoDetach(term: Terminal, socket: WebSocket): void {\n const addonTerminal = <ITerminadoAddonTerminal>term;\n addonTerminal.off('data', addonTerminal.__sendData);\n\n socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;\n\n if (socket) {\n socket.removeEventListener('message', addonTerminal.__getMessage);\n }\n\n delete addonTerminal.__socket;\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n /**\n * Attaches the current terminal to the given socket\n *\n * @param socket - The socket to attach the current terminal.\n * @param bidirectional - Whether the terminal should send data to the socket as well.\n * @param buffered - Whether the rendering of incoming data should happen instantly or at a\n * maximum frequency of 1 rendering per 10ms.\n */\n (<any>terminalConstructor.prototype).terminadoAttach = function (socket: WebSocket, bidirectional: boolean, buffered: boolean): void {\n return terminadoAttach(this, socket, bidirectional, buffered);\n };\n\n /**\n * Detaches the current terminal from the given socket.\n *\n * @param socket The socket from which to detach the current terminal.\n */\n (<any>terminalConstructor.prototype).terminadoDetach = function (socket: WebSocket): void {\n return terminadoDetach(this, socket);\n };\n}\n",null],"names":[],"mappings":"ACAA;;;ADoBA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AACA;AACA;AAEA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AA/CA;AAuDA;AACA;AACA;AAEA;AAEA;AACA;AACA;AAEA;AACA;AAXA;AAaA;AASA;AACA;AACA;AAOA;AACA;AACA;AACA;AArBA;"}
|
|
@ -0,0 +1,41 @@
|
|||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.webLinks = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var protocolClause = '(https?:\\/\\/)';
|
||||
var domainCharacterSet = '[\\da-z\\.-]+';
|
||||
var negatedDomainCharacterSet = '[^\\da-z\\.-]+';
|
||||
var domainBodyClause = '(' + domainCharacterSet + ')';
|
||||
var tldClause = '([a-z\\.]{2,6})';
|
||||
var ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
|
||||
var localHostClause = '(localhost)';
|
||||
var portClause = '(:\\d{1,5})';
|
||||
var hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
|
||||
var pathClause = '(\\/[\\/\\w\\.\\-%~]*)*';
|
||||
var queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
|
||||
var queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
|
||||
var hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
|
||||
var negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
|
||||
var bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;
|
||||
var start = '(?:^|' + negatedDomainCharacterSet + ')(';
|
||||
var end = ')($|' + negatedPathCharacterSet + ')';
|
||||
var strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
|
||||
function handleLink(event, uri) {
|
||||
window.open(uri, '_blank');
|
||||
}
|
||||
function webLinksInit(term, handler, options) {
|
||||
if (handler === void 0) { handler = handleLink; }
|
||||
if (options === void 0) { options = {}; }
|
||||
options.matchIndex = 1;
|
||||
term.registerLinkMatcher(strictUrlRegex, handler, options);
|
||||
}
|
||||
exports.webLinksInit = webLinksInit;
|
||||
function apply(terminalConstructor) {
|
||||
terminalConstructor.prototype.webLinksInit = function (handler, options) {
|
||||
webLinksInit(this, handler, options);
|
||||
};
|
||||
}
|
||||
exports.apply = apply;
|
||||
|
||||
},{}]},{},[1])(1)
|
||||
});
|
||||
//# sourceMappingURL=webLinks.js.map
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"webLinks.js","sources":["../../../src/addons/webLinks/webLinks.ts","../../../node_modules/browser-pack/_prelude.js"],"sourcesContent":["/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Terminal, ILinkMatcherOptions } from 'xterm';\n\nconst protocolClause = '(https?:\\\\/\\\\/)';\nconst domainCharacterSet = '[\\\\da-z\\\\.-]+';\nconst negatedDomainCharacterSet = '[^\\\\da-z\\\\.-]+';\nconst domainBodyClause = '(' + domainCharacterSet + ')';\nconst tldClause = '([a-z\\\\.]{2,6})';\nconst ipClause = '((\\\\d{1,3}\\\\.){3}\\\\d{1,3})';\nconst localHostClause = '(localhost)';\nconst portClause = '(:\\\\d{1,5})';\nconst hostClause = '((' + domainBodyClause + '\\\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';\nconst pathClause = '(\\\\/[\\\\/\\\\w\\\\.\\\\-%~]*)*';\nconst queryStringHashFragmentCharacterSet = '[0-9\\\\w\\\\[\\\\]\\\\(\\\\)\\\\/\\\\?\\\\!#@$%&\\'*+,:;~\\\\=\\\\.\\\\-]*';\nconst queryStringClause = '(\\\\?' + queryStringHashFragmentCharacterSet + ')?';\nconst hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';\nconst negatedPathCharacterSet = '[^\\\\/\\\\w\\\\.\\\\-%]+';\nconst bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;\nconst start = '(?:^|' + negatedDomainCharacterSet + ')(';\nconst end = ')($|' + negatedPathCharacterSet + ')';\nconst strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);\n\nfunction handleLink(event: MouseEvent, uri: string): void {\n window.open(uri, '_blank');\n}\n\n/**\n * Initialize the web links addon, registering the link matcher.\n * @param term The terminal to use web links within.\n * @param handler A custom handler to use.\n * @param options Custom options to use, matchIndex will always be ignored.\n */\nexport function webLinksInit(term: Terminal, handler: (event: MouseEvent, uri: string) => void = handleLink, options: ILinkMatcherOptions = {}): void {\n options.matchIndex = 1;\n term.registerLinkMatcher(strictUrlRegex, handler, options);\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n (<any>terminalConstructor.prototype).webLinksInit = function (handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): void {\n webLinksInit(this, handler, options);\n };\n}\n",null],"names":[],"mappings":"ACAA;;;ADOA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAQA;AAAA;AAAA;AACA;AACA;AACA;AAHA;AAKA;AACA;AACA;AACA;AACA;AAJA;"}
|
|
@ -0,0 +1,29 @@
|
|||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.winptyCompat = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
function winptyCompatInit(terminal) {
|
||||
var addonTerminal = terminal;
|
||||
var isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
|
||||
if (!isWindows) {
|
||||
return;
|
||||
}
|
||||
addonTerminal.on('linefeed', function () {
|
||||
var line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1);
|
||||
var lastChar = line[addonTerminal.cols - 1];
|
||||
if (lastChar[3] !== 32) {
|
||||
var nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y);
|
||||
nextLine.isWrapped = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
exports.winptyCompatInit = winptyCompatInit;
|
||||
function apply(terminalConstructor) {
|
||||
terminalConstructor.prototype.winptyCompatInit = function () {
|
||||
winptyCompatInit(this);
|
||||
};
|
||||
}
|
||||
exports.apply = apply;
|
||||
|
||||
},{}]},{},[1])(1)
|
||||
});
|
||||
//# sourceMappingURL=winptyCompat.js.map
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"winptyCompat.js","sources":["../../../src/addons/winptyCompat/winptyCompat.ts","../../../node_modules/browser-pack/_prelude.js"],"sourcesContent":["/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Terminal } from 'xterm';\nimport { IWinptyCompatAddonTerminal } from './Interfaces';\n\nexport function winptyCompatInit(terminal: Terminal): void {\n const addonTerminal = <IWinptyCompatAddonTerminal>terminal;\n\n // Don't do anything when the platform is not Windows\n const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;\n if (!isWindows) {\n return;\n }\n\n // Winpty does not support wraparound mode which means that lines will never\n // be marked as wrapped. This causes issues for things like copying a line\n // retaining the wrapped new line characters or if consumers are listening\n // in on the data stream.\n //\n // The workaround for this is to listen to every incoming line feed and mark\n // the line as wrapped if the last character in the previous line is not a\n // space. This is certainly not without its problems, but generally on\n // Windows when text reaches the end of the terminal it's likely going to be\n // wrapped.\n addonTerminal.on('linefeed', () => {\n const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1);\n const lastChar = line[addonTerminal.cols - 1];\n\n if (lastChar[3] !== 32 /* ' ' */) {\n const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y);\n (<any>nextLine).isWrapped = true;\n }\n });\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n (<any>terminalConstructor.prototype).winptyCompatInit = function (): void {\n winptyCompatInit(this);\n };\n}\n",null],"names":[],"mappings":"ACAA;;;ADQA;AACA;AAGA;AACA;AACA;AACA;AAYA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AA5BA;AA8BA;AACA;AACA;AACA;AACA;AAJA;"}
|
|
@ -0,0 +1,45 @@
|
|||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.zmodem = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var zmodem;
|
||||
function zmodemAttach(ws, opts) {
|
||||
if (opts === void 0) { opts = {}; }
|
||||
var term = this;
|
||||
var senderFunc = function (octets) { return ws.send(new Uint8Array(octets)); };
|
||||
var zsentry;
|
||||
function shouldWrite() {
|
||||
return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession;
|
||||
}
|
||||
zsentry = new zmodem.Sentry({
|
||||
to_terminal: function (octets) {
|
||||
if (shouldWrite()) {
|
||||
term.write(String.fromCharCode.apply(String, octets));
|
||||
}
|
||||
},
|
||||
sender: senderFunc,
|
||||
on_retract: function () { return term.emit('zmodemRetract'); },
|
||||
on_detect: function (detection) { return term.emit('zmodemDetect', detection); }
|
||||
});
|
||||
function handleWSMessage(evt) {
|
||||
if (typeof evt.data === 'string') {
|
||||
if (shouldWrite()) {
|
||||
term.write(evt.data);
|
||||
}
|
||||
}
|
||||
else {
|
||||
zsentry.consume(evt.data);
|
||||
}
|
||||
}
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.addEventListener('message', handleWSMessage);
|
||||
}
|
||||
function apply(terminalConstructor) {
|
||||
zmodem = (typeof window === 'object') ? window.Zmodem : { Browser: null };
|
||||
terminalConstructor.prototype.zmodemAttach = zmodemAttach;
|
||||
terminalConstructor.prototype.zmodemBrowser = zmodem.Browser;
|
||||
}
|
||||
exports.apply = apply;
|
||||
|
||||
},{}]},{},[1])(1)
|
||||
});
|
||||
//# sourceMappingURL=zmodem.js.map
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"zmodem.js","sources":["../../../src/addons/zmodem/zmodem.ts","../../../node_modules/browser-pack/_prelude.js"],"sourcesContent":["/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Terminal } from 'xterm';\n\n/**\n *\n * Allow xterm.js to handle ZMODEM uploads and downloads.\n *\n * This addon is a wrapper around zmodem.js. It adds the following to the\n * Terminal class:\n *\n * - function `zmodemAttach(<WebSocket>, <Object>)` - creates a Zmodem.Sentry\n * on the passed WebSocket object. The Object passed is optional and\n * can contain:\n * - noTerminalWriteOutsideSession: Suppress writes from the Sentry\n * object to the Terminal while there is no active Session. This\n * is necessary for compatibility with, for example, the\n * `attach.js` addon.\n *\n * - event `zmodemDetect` - fired on Zmodem.Sentry’s `on_detect` callback.\n * Passes the zmodem.js Detection object.\n *\n * - event `zmodemRetract` - fired on Zmodem.Sentry’s `on_retract` callback.\n *\n * You’ll need to provide logic to handle uploads and downloads.\n * See zmodem.js’s documentation for more details.\n *\n * **IMPORTANT:** After you confirm() a zmodem.js Detection, if you have\n * used the `attach` or `terminado` addons, you’ll need to suspend their\n * operation for the duration of the ZMODEM session. (The demo does this\n * via `detach()` and a re-`attach()`.)\n */\n\nlet zmodem;\n\nexport interface IZmodemOptions {\n noTerminalWriteOutsideSession?: boolean;\n}\n\nfunction zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void {\n const term = this;\n const senderFunc = (octets: ArrayLike<number>) => ws.send(new Uint8Array(octets));\n\n let zsentry;\n\n function shouldWrite(): boolean {\n return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession;\n }\n\n zsentry = new zmodem.Sentry({\n to_terminal: (octets: ArrayLike<number>) => {\n if (shouldWrite()) {\n term.write(\n String.fromCharCode.apply(String, octets)\n );\n }\n },\n sender: senderFunc,\n on_retract: () => (<any>term).emit('zmodemRetract'),\n on_detect: (detection: any) => (<any>term).emit('zmodemDetect', detection)\n });\n\n function handleWSMessage(evt: MessageEvent): void {\n\n // In testing with xterm.js’s demo the first message was\n // always text even if the rest were binary. While that\n // may be specific to xterm.js’s demo, ultimately we\n // should reject anything that isn’t binary.\n if (typeof evt.data === 'string') {\n if (shouldWrite()) {\n term.write(evt.data);\n }\n }\n else {\n zsentry.consume(evt.data);\n }\n }\n\n ws.binaryType = 'arraybuffer';\n ws.addEventListener('message', handleWSMessage);\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n zmodem = (typeof window === 'object') ? (<any>window).Zmodem : {Browser: null}; // Nullify browser for tests\n\n (<any>terminalConstructor.prototype).zmodemAttach = zmodemAttach;\n (<any>terminalConstructor.prototype).zmodemBrowser = zmodem.Browser;\n}\n",null],"names":[],"mappings":"ACAA;;;ADoCA;AAMA;AAAA;AACA;AACA;AAEA;AAEA;AACA;AACA;AAEA;AACA;AACA;AACA;AAGA;AACA;AACA;AACA;AACA;AACA;AAEA;AAMA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AALA;"}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* JQuery zTree exHideNodes v3.5.33
|
||||
* http://treejs.cn/
|
||||
*
|
||||
* Copyright (c) 2010 Hunter.z
|
||||
*
|
||||
* Licensed same as jquery - MIT License
|
||||
* http://www.opensource.org/licenses/mit-license.php
|
||||
*
|
||||
* email: hunter.z@263.net
|
||||
* Date: 2018-01-30
|
||||
*/
|
||||
(function(j){j.extend(!0,j.fn.zTree._z,{view:{clearOldFirstNode:function(c,a){for(var b=a.getNextNode();b;){if(b.isFirstNode){b.isFirstNode=!1;e.setNodeLineIcos(c,b);break}if(b.isLastNode)break;b=b.getNextNode()}},clearOldLastNode:function(c,a,b){for(a=a.getPreNode();a;){if(a.isLastNode){a.isLastNode=!1;b&&e.setNodeLineIcos(c,a);break}if(a.isFirstNode)break;a=a.getPreNode()}},makeDOMNodeMainBefore:function(c,a,b){a=d.isHidden(a,b);c.push("<li ",a?"style='display:none;' ":"","id='",b.tId,"' class='",
|
||||
l.className.LEVEL,b.level,"' tabindex='0' hidefocus='true' treenode>")},showNode:function(c,a){d.isHidden(c,a,!1);d.initShowForExCheck(c,a);k(a,c).show()},showNodes:function(c,a,b){if(a&&a.length!=0){var f={},g,i;for(g=0,i=a.length;g<i;g++){var h=a[g];if(!f[h.parentTId]){var u=h.getParentNode();f[h.parentTId]=u===null?d.getRoot(c):h.getParentNode()}e.showNode(c,h,b)}for(var j in f)a=d.nodeChildren(c,f[j]),e.setFirstNodeForShow(c,a),e.setLastNodeForShow(c,a)}},hideNode:function(c,a){d.isHidden(c,a,
|
||||
!0);a.isFirstNode=!1;a.isLastNode=!1;d.initHideForExCheck(c,a);e.cancelPreSelectedNode(c,a);k(a,c).hide()},hideNodes:function(c,a,b){if(a&&a.length!=0){var f={},g,i;for(g=0,i=a.length;g<i;g++){var h=a[g];if((h.isFirstNode||h.isLastNode)&&!f[h.parentTId]){var j=h.getParentNode();f[h.parentTId]=j===null?d.getRoot(c):h.getParentNode()}e.hideNode(c,h,b)}for(var k in f)a=d.nodeChildren(c,f[k]),e.setFirstNodeForHide(c,a),e.setLastNodeForHide(c,a)}},setFirstNode:function(c,a){var b=d.nodeChildren(c,a),f=
|
||||
d.isHidden(c,b[0],!1);b.length>0&&!f?b[0].isFirstNode=!0:b.length>0&&e.setFirstNodeForHide(c,b)},setLastNode:function(c,a){var b=d.nodeChildren(c,a),f=d.isHidden(c,b[0]);b.length>0&&!f?b[b.length-1].isLastNode=!0:b.length>0&&e.setLastNodeForHide(c,b)},setFirstNodeForHide:function(c,a){var b,f,g;for(f=0,g=a.length;f<g;f++){b=a[f];if(b.isFirstNode)break;if(!d.isHidden(c,b)&&!b.isFirstNode){b.isFirstNode=!0;e.setNodeLineIcos(c,b);break}else b=null}return b},setFirstNodeForShow:function(c,a){var b,f,
|
||||
g,i,h;for(f=0,g=a.length;f<g;f++){b=a[f];var j=d.isHidden(c,b);if(!i&&!j&&b.isFirstNode){i=b;break}else if(!i&&!j&&!b.isFirstNode)b.isFirstNode=!0,i=b,e.setNodeLineIcos(c,b);else if(i&&b.isFirstNode){b.isFirstNode=!1;h=b;e.setNodeLineIcos(c,b);break}}return{"new":i,old:h}},setLastNodeForHide:function(c,a){var b,f;for(f=a.length-1;f>=0;f--){b=a[f];if(b.isLastNode)break;if(!d.isHidden(c,b)&&!b.isLastNode){b.isLastNode=!0;e.setNodeLineIcos(c,b);break}else b=null}return b},setLastNodeForShow:function(c,
|
||||
a){var b,f,g,i;for(f=a.length-1;f>=0;f--){b=a[f];var h=d.isHidden(c,b);if(!g&&!h&&b.isLastNode){g=b;break}else if(!g&&!h&&!b.isLastNode)b.isLastNode=!0,g=b,e.setNodeLineIcos(c,b);else if(g&&b.isLastNode){b.isLastNode=!1;i=b;e.setNodeLineIcos(c,b);break}}return{"new":g,old:i}}},data:{initHideForExCheck:function(c,a){if(d.isHidden(c,a)&&c.check&&c.check.enable){if(typeof a._nocheck=="undefined")a._nocheck=!!a.nocheck,a.nocheck=!0;a.check_Child_State=-1;e.repairParentChkClassWithSelf&&e.repairParentChkClassWithSelf(c,
|
||||
a)}},initShowForExCheck:function(c,a){if(!d.isHidden(c,a)&&c.check&&c.check.enable){if(typeof a._nocheck!="undefined")a.nocheck=a._nocheck,delete a._nocheck;if(e.setChkClass){var b=k(a,l.id.CHECK,c);e.setChkClass(c,b,a)}e.repairParentChkClassWithSelf&&e.repairParentChkClassWithSelf(c,a)}}}});var j=j.fn.zTree,m=j._z.tools,l=j.consts,e=j._z.view,d=j._z.data,k=m.$;d.isHidden=function(c,a,b){if(!a)return!1;c=c.data.key.isHidden;typeof b!=="undefined"&&(typeof b==="string"&&(b=m.eqs(checked,"true")),a[c]=
|
||||
!!b);return a[c]};d.exSetting({data:{key:{isHidden:"isHidden"}}});d.addInitNode(function(c,a,b){a=d.isHidden(c,b);d.isHidden(c,b,a);d.initHideForExCheck(c,b)});d.addBeforeA(function(){});d.addZTreeTools(function(c,a){a.showNodes=function(a,b){e.showNodes(c,a,b)};a.showNode=function(a,b){a&&e.showNodes(c,[a],b)};a.hideNodes=function(a,b){e.hideNodes(c,a,b)};a.hideNode=function(a,b){a&&e.hideNodes(c,[a],b)};var b=a.checkNode;if(b)a.checkNode=function(f,e,i,h){(!f||!d.isHidden(c,f))&&b.apply(a,arguments)}});
|
||||
var n=d.initNode;d.initNode=function(c,a,b,f,g,i,h){var j=(f?f:d.getRoot(c))[c.data.key.children];d.tmpHideFirstNode=e.setFirstNodeForHide(c,j);d.tmpHideLastNode=e.setLastNodeForHide(c,j);h&&(e.setNodeLineIcos(c,d.tmpHideFirstNode),e.setNodeLineIcos(c,d.tmpHideLastNode));g=d.tmpHideFirstNode===b;i=d.tmpHideLastNode===b;n&&n.apply(d,arguments);h&&i&&e.clearOldLastNode(c,b,h)};var o=d.makeChkFlag;if(o)d.makeChkFlag=function(c,a){(!a||!d.isHidden(c,a))&&o.apply(d,arguments)};var p=d.getTreeCheckedNodes;
|
||||
if(p)d.getTreeCheckedNodes=function(c,a,b,f){if(a&&a.length>0){var e=a[0].getParentNode();if(e&&d.isHidden(c,e))return[]}return p.apply(d,arguments)};var q=d.getTreeChangeCheckedNodes;if(q)d.getTreeChangeCheckedNodes=function(c,a,b){if(a&&a.length>0){var e=a[0].getParentNode();if(e&&d.isHidden(c,e))return[]}return q.apply(d,arguments)};var r=e.expandCollapseSonNode;if(r)e.expandCollapseSonNode=function(c,a,b,f,g){(!a||!d.isHidden(c,a))&&r.apply(e,arguments)};var s=e.setSonNodeCheckBox;if(s)e.setSonNodeCheckBox=
|
||||
function(c,a,b,f){(!a||!d.isHidden(c,a))&&s.apply(e,arguments)};var t=e.repairParentChkClassWithSelf;if(t)e.repairParentChkClassWithSelf=function(c,a){(!a||!d.isHidden(c,a))&&t.apply(e,arguments)}})(jQuery);
|
|
@ -58,7 +58,6 @@
|
|||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
{% if request.user.is_superuser %}
|
||||
<li id="ops">
|
||||
<a>
|
||||
<i class="fa fa-coffee" style="width: 14px"></i> <span class="nav-label">{% trans 'Job Center' %}</span><span class="fa arrow"></span>
|
||||
|
@ -67,7 +66,6 @@
|
|||
<li id="task"><a href="{% url 'ops:task-list' %}">{% trans 'Task list' %}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li id="audits">
|
||||
<a>
|
||||
<i class="fa fa-history" style="width: 14px"></i> <span class="nav-label">{% trans 'Audits' %}</span><span class="fa arrow"></span>
|
||||
|
@ -77,17 +75,9 @@
|
|||
<li id="ftp-log"><a href="{% url 'audits:ftp-log-list' %}">{% trans 'FTP log' %}</a></li>
|
||||
<li id="operate-log"><a href="{% url 'audits:operate-log-list' %}">{% trans 'Operate log' %}</a></li>
|
||||
<li id="password-change-log"><a href="{% url 'audits:password-change-log-list' %}">{% trans 'Password change log' %}</a></li>
|
||||
<li id="command-execution-log"><a href="{% url 'audits:command-execution-log-list' %}">{% trans 'Command execution' %}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{#<li id="">#}
|
||||
{# <a href="#">#}
|
||||
{# <i class="fa fa-download"></i> <span class="nav-label">{% trans 'File' %}</span><span class="fa arrow"></span>#}
|
||||
{# </a>#}
|
||||
{# <ul class="nav nav-second-level">#}
|
||||
{# <li id="upload"><a href="">{% trans 'File upload' %}</a></li>#}
|
||||
{# <li id="download"><a href="">{% trans 'File download' %}</a></li>#}
|
||||
{# </ul>#}
|
||||
{#</li>#}
|
||||
{% if XPACK_PLUGINS %}
|
||||
<li id="xpack">
|
||||
<a>
|
||||
|
|
|
@ -4,6 +4,11 @@
|
|||
<i class="fa fa-files-o" style="width: 14px"></i><span class="nav-label">{% trans 'My assets' %}</span><span class="label label-info pull-right"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li id="ops">
|
||||
<a href="{% url 'ops:command-execution-start' %}">
|
||||
<i class="fa fa-terminal" style="width: 14px"></i> <span class="nav-label">{% trans 'Command execution' %}</span><span class="label label-info pull-right"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li id="users">
|
||||
<a href="{% url 'users:user-profile' %}">
|
||||
<i class="fa fa-user" style="width: 14px"></i> <span class="nav-label">{% trans 'Profile' %}</span><span class="label label-info pull-right"></span>
|
||||
|
|
|
@ -57,7 +57,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
|
@ -108,7 +107,8 @@ function initTable() {
|
|||
|
||||
function onSelected(event, treeNode) {
|
||||
url = '{% url "api-perms:user-node-assets" pk=object.id node_id=DEFAULT_PK %}';
|
||||
url = url.replace("{{ DEFAULT_PK }}", treeNode.node_id);
|
||||
var node_id = treeNode.meta.node.id;
|
||||
url = url.replace("{{ DEFAULT_PK }}", node_id);
|
||||
asset_table.ajax.url(url);
|
||||
asset_table.ajax.reload();
|
||||
}
|
||||
|
@ -129,23 +129,9 @@ function initTree() {
|
|||
}
|
||||
};
|
||||
|
||||
var zNodes = [];
|
||||
$.get("{% url 'api-perms:user-nodes' pk=object.id %}", function(data, status) {
|
||||
$.each(data, function (index, value) {
|
||||
value["node_id"] = value["id"];
|
||||
value["id"] = value["tree_id"];
|
||||
if (value["tree_id"] !== value["tree_parent"]) {
|
||||
value["pId"] = value["tree_parent"];
|
||||
}
|
||||
value["isParent"] = value["is_node"];
|
||||
value['name'] = value['value'];
|
||||
value["iconSkin"] = value["is_node"] ? null : 'file';
|
||||
});
|
||||
zNodes = data;
|
||||
$.fn.zTree.init($("#assetTree"), setting, zNodes);
|
||||
$.get("{% url 'api-perms:user-nodes-assets-as-tree' pk=object.id %}?show_assets=0", function(data, status) {
|
||||
$.fn.zTree.init($("#assetTree"), setting, data);
|
||||
zTree = $.fn.zTree.getZTreeObj("assetTree");
|
||||
var root = zTree.getNodes()[0];
|
||||
zTree.expandNode(root);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ jmespath==0.9.3
|
|||
kombu==4.0.2
|
||||
ldap3==2.4
|
||||
MarkupSafe==1.0
|
||||
mysqlclient==1.3.12
|
||||
mysqlclient==1.3.14
|
||||
olefile==0.44
|
||||
openapi-codec==1.3.2
|
||||
paramiko==2.4.1
|
||||
|
|
Loading…
Reference in New Issue