perf: 优化部署 host

pull/9008/head
ibuler 2022-11-01 11:52:51 +08:00
parent 8df15cb564
commit cf81f08b7a
24 changed files with 186 additions and 108 deletions

View File

@ -8,7 +8,6 @@ from collections import defaultdict
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.db.models import Model
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from common.utils import get_logger from common.utils import get_logger
@ -115,9 +114,9 @@ class BasePlaybookManager:
method_attr = '{}_method'.format(self.__class__.method_type()) method_attr = '{}_method'.format(self.__class__.method_type())
method_enabled = automation and \ method_enabled = automation and \
getattr(automation, enabled_attr) and \ getattr(automation, enabled_attr) and \
getattr(automation, method_attr) and \ getattr(automation, method_attr) and \
getattr(automation, method_attr) in self.method_id_meta_mapper getattr(automation, method_attr) in self.method_id_meta_mapper
if not method_enabled: if not method_enabled:
host['error'] = _('{} disabled'.format(self.__class__.method_type())) host['error'] = _('{} disabled'.format(self.__class__.method_type()))
@ -132,6 +131,7 @@ class BasePlaybookManager:
def generate_private_key_path(secret, path_dir): def generate_private_key_path(secret, path_dir):
key_name = '.' + md5(secret.encode('utf-8')).hexdigest() key_name = '.' + md5(secret.encode('utf-8')).hexdigest()
key_path = os.path.join(path_dir, key_name) key_path = os.path.join(path_dir, key_name)
if not os.path.exists(key_path): if not os.path.exists(key_path):
ssh_key_string_to_obj(secret, password=None).write_private_key_file(key_path) ssh_key_string_to_obj(secret, password=None).write_private_key_file(key_path)
os.chmod(key_path, 0o400) os.chmod(key_path, 0o400)

View File

@ -154,7 +154,7 @@ class ChangeSecretManager(BasePlaybookManager):
recorder = self.name_recorder_mapper.get(host) recorder = self.name_recorder_mapper.get(host)
if not recorder: if not recorder:
return return
recorder.status = 'succeed' recorder.status = 'success'
recorder.date_finished = timezone.now() recorder.date_finished = timezone.now()
recorder.save() recorder.save()

View File

@ -106,7 +106,7 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
return '{0.name}({0.address})'.format(self) return '{0.name}({0.address})'.format(self)
@property @property
def category_property(self): def specific(self):
if not hasattr(self, self.category): if not hasattr(self, self.category):
return {} return {}
instance = getattr(self, self.category) instance = getattr(self, self.category)

View File

@ -15,7 +15,7 @@ class Database(Asset):
return self.address return self.address
@property @property
def category_property(self): def specific(self):
return { return {
'db_name': self.db_name, 'db_name': self.db_name,
} }

View File

@ -3,7 +3,7 @@ from celery import current_task
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.const.choices import Trigger from common.const.choices import Trigger, Status
from common.mixins.models import CommonModelMixin from common.mixins.models import CommonModelMixin
from common.db.fields import EncryptJsonDictTextField from common.db.fields import EncryptJsonDictTextField
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin

View File

@ -77,7 +77,7 @@ class AssetSerializer(OrgResourceSerializerMixin, WritableNestedModelSerializer)
'nodes', 'labels', 'accounts', 'protocols', 'nodes_display', 'nodes', 'labels', 'accounts', 'protocols', 'nodes_display',
] ]
read_only_fields = [ read_only_fields = [
'category', 'type', 'category_property', 'category', 'type', 'specific',
'connectivity', 'date_verified', 'connectivity', 'date_verified',
'created_by', 'date_created', 'created_by', 'date_created',
] ]
@ -90,7 +90,7 @@ class AssetSerializer(OrgResourceSerializerMixin, WritableNestedModelSerializer)
def get_field_names(self, declared_fields, info): def get_field_names(self, declared_fields, info):
names = super().get_field_names(declared_fields, info) names = super().get_field_names(declared_fields, info)
if self.__class__.__name__ != 'AssetSerializer': if self.__class__.__name__ != 'AssetSerializer':
names.remove('category_property') names.remove('specific')
return names return names
@classmethod @classmethod

View File

@ -9,3 +9,12 @@ AUDITOR = 'Auditor'
class Trigger(models.TextChoices): class Trigger(models.TextChoices):
manual = 'manual', _('Manual trigger') manual = 'manual', _('Manual trigger')
timing = 'timing', _('Timing trigger') timing = 'timing', _('Timing trigger')
class Status(models.TextChoices):
pending = 'pending', _("Pending")
running = 'running', _("Running")
success = 'success', _("Success")
failed = 'failed', _("Failed")
error = 'error', _("Error")
canceled = 'canceled', _("Canceled")

View File

@ -2,6 +2,14 @@ from collections import defaultdict
class DefaultCallback: class DefaultCallback:
STATUS_MAPPER = {
'successful': 'success',
'failure': 'failed',
'running': 'running',
'pending': 'pending',
'unknown': 'unknown'
}
def __init__(self): def __init__(self):
self.result = dict( self.result = dict(
ok=defaultdict(dict), ok=defaultdict(dict),
@ -27,7 +35,7 @@ class DefaultCallback:
return results return results
def is_success(self): def is_success(self):
return self.status != 'successful' return self.status != 'success'
def event_handler(self, data, **kwargs): def event_handler(self, data, **kwargs):
event = data.get('event', None) event = data.get('event', None)
@ -131,4 +139,5 @@ class DefaultCallback:
pass pass
def status_handler(self, data, **kwargs): def status_handler(self, data, **kwargs):
self.status = data.get('status', 'unknown') status = data.get('status', '')
self.status = self.STATUS_MAPPER.get(status, 'unknown')

View File

@ -9,13 +9,12 @@ __all__ = ['JMSInventory']
class JMSInventory: class JMSInventory:
def __init__(self, assets, account_policy='smart', def __init__(self, assets, account_policy='privileged_first',
account_prefer='root,administrator', account_prefer='root,Administrator', host_callback=None):
host_callback=None):
""" """
:param assets: :param assets:
:param account_prefer: account username name if not set use account_policy :param account_prefer: account username name if not set use account_policy
:param account_policy: smart, privileged_must, privileged_first :param account_policy: privileged_only, privileged_first, skip
""" """
self.assets = self.clean_assets(assets) self.assets = self.clean_assets(assets)
self.account_prefer = account_prefer self.account_prefer = account_prefer
@ -105,7 +104,7 @@ class JMSInventory:
'id': str(asset.id), 'name': asset.name, 'address': asset.address, 'id': str(asset.id), 'name': asset.name, 'address': asset.address,
'type': asset.type, 'category': asset.category, 'type': asset.type, 'category': asset.category,
'protocol': asset.protocol, 'port': asset.port, 'protocol': asset.protocol, 'port': asset.port,
'category_property': asset.category_property, 'specific': asset.specific,
'protocols': [{'name': p.name, 'port': p.port} for p in protocols], 'protocols': [{'name': p.name, 'port': p.port} for p in protocols],
}, },
'jms_account': { 'jms_account': {
@ -137,24 +136,27 @@ class JMSInventory:
def select_account(self, asset): def select_account(self, asset):
accounts = list(asset.accounts.all()) accounts = list(asset.accounts.all())
account_selected = None account_selected = None
account_username = self.account_prefer account_usernames = self.account_prefer
if isinstance(self.account_prefer, str): if isinstance(self.account_prefer, str):
account_username = self.account_prefer.split(',') account_usernames = self.account_prefer.split(',')
if account_username: # 优先使用提供的名称
for username in account_username: if account_usernames:
account_matched = list(filter(lambda account: account.username == username, accounts)) account_matched = list(filter(lambda account: account.username in account_usernames, accounts))
if account_matched: account_selected = account_matched[0] if account_matched else None
account_selected = account_matched[0]
break
if not account_selected: if account_selected or self.account_policy == 'skip':
if self.account_policy in ['privileged_must', 'privileged_first']: return account_selected
account_matched = list(filter(lambda account: account.privileged, accounts))
account_selected = account_matched[0] if account_matched else None
if not account_selected and self.account_policy == 'privileged_first': if self.account_policy in ['privileged_only', 'privileged_first']:
account_matched = list(filter(lambda account: account.privileged, accounts))
account_selected = account_matched[0] if account_matched else None
if account_selected:
return account_selected
if self.account_policy == 'privileged_first':
account_selected = accounts[0] if accounts else None account_selected = accounts[0] if accounts else None
return account_selected return account_selected

View File

@ -5,7 +5,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.utils import get_logger from common.utils import get_logger
from .base import BaseAnsibleTask, BaseAnsibleExecution from .base import BaseAnsibleJob, BaseAnsibleExecution
from ..ansible import AdHocRunner from ..ansible import AdHocRunner
__all__ = ["AdHoc", "AdHocExecution"] __all__ = ["AdHoc", "AdHocExecution"]
@ -14,7 +14,7 @@ __all__ = ["AdHoc", "AdHocExecution"]
logger = get_logger(__file__) logger = get_logger(__file__)
class AdHoc(BaseAnsibleTask): class AdHoc(BaseAnsibleJob):
pattern = models.CharField(max_length=1024, verbose_name=_("Pattern"), default='all') pattern = models.CharField(max_length=1024, verbose_name=_("Pattern"), default='all')
module = models.CharField(max_length=128, default='shell', verbose_name=_('Module')) module = models.CharField(max_length=128, default='shell', verbose_name=_('Module'))
args = models.CharField(max_length=1024, default='', verbose_name=_('Args')) args = models.CharField(max_length=1024, default='', verbose_name=_('Args'))

View File

@ -12,7 +12,7 @@ from ..ansible.inventory import JMSInventory
from ..mixin import PeriodTaskModelMixin from ..mixin import PeriodTaskModelMixin
class BaseAnsibleTask(PeriodTaskModelMixin, JMSOrgBaseModel): class BaseAnsibleJob(PeriodTaskModelMixin, JMSOrgBaseModel):
owner = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True) owner = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True)
assets = models.ManyToManyField('assets.Asset', verbose_name=_("Assets")) assets = models.ManyToManyField('assets.Asset', verbose_name=_("Assets"))
account = models.CharField(max_length=128, default='root', verbose_name=_('Account')) account = models.CharField(max_length=128, default='root', verbose_name=_('Account'))
@ -46,7 +46,7 @@ class BaseAnsibleTask(PeriodTaskModelMixin, JMSOrgBaseModel):
class BaseAnsibleExecution(models.Model): class BaseAnsibleExecution(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4) id = models.UUIDField(primary_key=True, default=uuid.uuid4)
status = models.CharField(max_length=16, verbose_name=_('Status'), default='running') status = models.CharField(max_length=16, verbose_name=_('Status'), default='running')
task = models.ForeignKey(BaseAnsibleTask, on_delete=models.CASCADE, related_name='executions', null=True) task = models.ForeignKey(BaseAnsibleJob, on_delete=models.CASCADE, related_name='executions', null=True)
result = models.JSONField(blank=True, null=True, verbose_name=_('Result')) result = models.JSONField(blank=True, null=True, verbose_name=_('Result'))
summary = models.JSONField(default=dict, verbose_name=_('Summary')) summary = models.JSONField(default=dict, verbose_name=_('Summary'))
creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True) creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True)
@ -86,7 +86,7 @@ class BaseAnsibleExecution(models.Model):
def set_result(self, cb): def set_result(self, cb):
status_mapper = { status_mapper = {
'successful': 'succeeded', 'successful': 'success',
} }
this = self.__class__.objects.get(id=self.id) this = self.__class__.objects.get(id=self.id)
this.status = status_mapper.get(cb.status, cb.status) this.status = status_mapper.get(cb.status, cb.status)
@ -112,11 +112,11 @@ class BaseAnsibleExecution(models.Model):
@property @property
def is_finished(self): def is_finished(self):
return self.status in ['succeeded', 'failed'] return self.status in ['success', 'failed']
@property @property
def is_success(self): def is_success(self):
return self.status == 'succeeded' return self.status == 'success'
@property @property
def time_cost(self): def time_cost(self):

View File

@ -23,6 +23,7 @@ class CeleryTask(models.Model):
"comment": getattr(task, 'comment', None), "comment": getattr(task, 'comment', None),
"queue": getattr(task, 'queue', 'default') "queue": getattr(task, 'queue', 'default')
} }
@property @property
def state(self): def state(self):
last_five_executions = CeleryTaskExecution.objects.filter(name=self.name).order_by('-date_published')[:5] last_five_executions = CeleryTaskExecution.objects.filter(name=self.name).order_by('-date_published')[:5]

View File

@ -2,7 +2,7 @@ from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from orgs.mixins.models import JMSOrgBaseModel from orgs.mixins.models import JMSOrgBaseModel
from .base import BaseAnsibleExecution, BaseAnsibleTask from .base import BaseAnsibleExecution, BaseAnsibleJob
class PlaybookTemplate(JMSOrgBaseModel): class PlaybookTemplate(JMSOrgBaseModel):
@ -19,7 +19,7 @@ class PlaybookTemplate(JMSOrgBaseModel):
unique_together = [('org_id', 'name')] unique_together = [('org_id', 'name')]
class Playbook(BaseAnsibleTask): class Playbook(BaseAnsibleJob):
path = models.FilePathField(max_length=1024, verbose_name=_("Playbook")) path = models.FilePathField(max_length=1024, verbose_name=_("Playbook"))
owner = models.ForeignKey('users.User', verbose_name=_("Owner"), on_delete=models.SET_NULL, null=True) owner = models.ForeignKey('users.User', verbose_name=_("Owner"), on_delete=models.SET_NULL, null=True)
comment = models.TextField(blank=True, verbose_name=_("Comment")) comment = models.TextField(blank=True, verbose_name=_("Comment"))

View File

@ -20,16 +20,15 @@ __all__ = [
class OrgManager(models.Manager): class OrgManager(models.Manager):
def all_group_by_org(self): def all_group_by_org(self):
from ..models import Organization from ..models import Organization
orgs = list(Organization.objects.all()) orgs = list(Organization.objects.all())
querysets = {} org_queryset = {}
for org in orgs: for org in orgs:
org_id = org.id org_id = org.id
queryset = super(OrgManager, self).get_queryset().filter(org_id=org_id) queryset = super(OrgManager, self).get_queryset().filter(org_id=org_id)
querysets[org] = queryset org_queryset[org] = queryset
return querysets return org_queryset
def get_queryset(self): def get_queryset(self):
queryset = super(OrgManager, self).get_queryset() queryset = super(OrgManager, self).get_queryset()
@ -46,7 +45,7 @@ class OrgManager(models.Manager):
for obj in objs: for obj in objs:
if org.is_root(): if org.is_root():
if not obj.org_id: if not obj.org_id:
raise ValidationError('Please save in a organization') raise ValidationError('Please save in a org')
else: else:
obj.org_id = org.id obj.org_id = org.id
return super().bulk_create(objs, batch_size, ignore_conflicts) return super().bulk_create(objs, batch_size, ignore_conflicts)
@ -54,20 +53,24 @@ class OrgManager(models.Manager):
class OrgModelMixin(models.Model): class OrgModelMixin(models.Model):
org_id = models.CharField( org_id = models.CharField(
max_length=36, blank=True, default='', verbose_name=_("Organization"), db_index=True max_length=36, blank=True, default='',
verbose_name=_("Organization"), db_index=True
) )
objects = OrgManager() objects = OrgManager()
sep = '@' sep = '@'
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
org = get_current_org() locking_org = getattr(self, 'locking_org', None)
if locking_org:
org = Organization.get_instance(locking_org)
else:
org = get_current_org()
# 这里不可以优化成, 因为 root 组织下可以设置组织 id 来保存 # 这里不可以优化成, 因为 root 组织下可以设置组织 id 来保存
# if org.is_root() and not self.org_id: # if org.is_root() and not self.org_id:
# raise ... # raise ...
if org.is_root(): if org.is_root():
if not self.org_id: if not self.org_id:
raise ValidationError('Please save in a organization') raise ValidationError('Please save in a org')
else: else:
self.org_id = org.id self.org_id = org.id
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
@ -87,8 +90,6 @@ class OrgModelMixin(models.Model):
name = getattr(self, attr) name = getattr(self, attr)
elif hasattr(self, 'name'): elif hasattr(self, 'name'):
name = self.name name = self.name
elif hasattr(self, 'name'):
name = self.hostname
return name + self.sep + self.org_name return name + self.sep + self.org_name
def validate_unique(self, exclude=None): def validate_unique(self, exclude=None):

View File

@ -103,22 +103,20 @@ def tmp_to_builtin_org(system=0, default=0):
set_current_org(ori_org) set_current_org(ori_org)
def get_org_filters():
kwargs = {}
_current_org = get_current_org()
if _current_org is None:
return kwargs
if _current_org.is_root():
return kwargs
kwargs['org_id'] = _current_org.id
return kwargs
def filter_org_queryset(queryset): def filter_org_queryset(queryset):
kwargs = get_org_filters() locking_org = getattr(queryset.model, 'LOCKING_ORG', None)
if locking_org:
org = Organization.get_instance(locking_org)
else:
org = get_current_org()
if org is None:
kwargs = {}
elif org.is_root():
kwargs = {}
else:
kwargs = {'org_id': org.id}
#
# lines = traceback.format_stack() # lines = traceback.format_stack()
# print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>") # print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>")
# for line in lines[-10:-1]: # for line in lines[-10:-1]:

View File

@ -2,29 +2,17 @@ from rest_framework import viewsets
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from orgs.utils import tmp_to_builtin_org
from terminal import serializers from terminal import serializers
from terminal.models import AppletHost, Applet from terminal.models import AppletHost, Applet, AppletHostDeployment
from terminal.tasks import run_applet_host_deployment from terminal.tasks import run_applet_host_deployment
__all__ = ['AppletHostViewSet']
__all__ = ['AppletHostViewSet', 'AppletHostDeploymentViewSet']
class AppletHostViewSet(viewsets.ModelViewSet): class AppletHostViewSet(viewsets.ModelViewSet):
serializer_class = serializers.AppletHostSerializer serializer_class = serializers.AppletHostSerializer
queryset = AppletHost.objects.all()
def get_queryset(self):
return AppletHost.objects.all()
def dispatch(self, request, *args, **kwargs):
with tmp_to_builtin_org(system=1):
return super().dispatch(request, *args, **kwargs)
@action(methods=['post'], detail=True)
def deploy(self, request):
from terminal.automations.deploy_applet_host.manager import DeployAppletHostManager
manager = DeployAppletHostManager(self)
manager.run()
@action(methods=['get'], detail=True, url_path='') @action(methods=['get'], detail=True, url_path='')
def not_published_applets(self, request, *args, **kwargs): def not_published_applets(self, request, *args, **kwargs):
@ -33,3 +21,15 @@ class AppletHostViewSet(viewsets.ModelViewSet):
serializer = serializers.AppletSerializer(applets, many=True) serializer = serializers.AppletSerializer(applets, many=True)
return Response(serializer.data) return Response(serializer.data)
class AppletHostDeploymentViewSet(viewsets.ModelViewSet):
serializer_class = serializers.AppletHostDeploymentSerializer
queryset = AppletHostDeployment.objects.all()
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
instance = serializer.save()
task = run_applet_host_deployment.delay(instance.id)
return Response({'task': str(task.id)}, status=201)

View File

@ -1,16 +1,21 @@
import os import os
import datetime import datetime
import shutil import shutil
from django.utils import timezone
from django.conf import settings from django.conf import settings
from common.utils import get_logger
from common.db.utils import safe_db_connection
from ops.ansible import PlaybookRunner, JMSInventory from ops.ansible import PlaybookRunner, JMSInventory
logger = get_logger(__name__)
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
class DeployAppletHostManager: class DeployAppletHostManager:
def __init__(self, applet_host): def __init__(self, deployment):
self.applet_host = applet_host self.deployment = deployment
self.run_dir = self.get_run_dir() self.run_dir = self.get_run_dir()
@staticmethod @staticmethod
@ -28,16 +33,32 @@ class DeployAppletHostManager:
return playbook_dst return playbook_dst
def generate_inventory(self): def generate_inventory(self):
inventory = JMSInventory([self.applet_host], account_policy='privileged_only') inventory = JMSInventory([self.deployment.host], account_policy='privileged_only')
inventory_dir = os.path.join(self.run_dir, 'inventory') inventory_dir = os.path.join(self.run_dir, 'inventory')
inventory_path = os.path.join(inventory_dir, 'hosts.yml') inventory_path = os.path.join(inventory_dir, 'hosts.yml')
inventory.write_to_file(inventory_path) inventory.write_to_file(inventory_path)
return inventory_path return inventory_path
def run(self, **kwargs): def _run(self, **kwargs):
inventory = self.generate_inventory() inventory = self.generate_inventory()
playbook = self.generate_playbook() playbook = self.generate_playbook()
runner = PlaybookRunner( runner = PlaybookRunner(
inventory=inventory, playbook=playbook, project_dir=self.run_dir inventory=inventory, playbook=playbook, project_dir=self.run_dir
) )
return runner.run(**kwargs) return runner.run(**kwargs)
def run(self, **kwargs):
try:
self.deployment.date_start = timezone.now()
cb = self._run(**kwargs)
self.deployment.status = cb.status
except Exception as e:
logger.error("Error: {}".format(e))
self.deployment.status = 'error'
finally:
self.deployment.date_finished = timezone.now()
with safe_db_connection():
self.deployment.save()

View File

@ -73,7 +73,7 @@ class Migration(migrations.Migration):
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('status', models.CharField(max_length=16, verbose_name='Status')), ('status', models.CharField(max_length=16, default='', verbose_name='Status')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')), ('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='terminal.applethost', verbose_name='Hosting')), ('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='terminal.applethost', verbose_name='Hosting')),
], ],

View File

@ -1,4 +1,4 @@
# Generated by Django 3.2.14 on 2022-10-28 07:44 # Generated by Django 3.2.14 on 2022-10-31 10:48
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -19,12 +19,22 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='applethost', model_name='applethost',
name='date_inited', name='date_inited',
field=models.DateTimeField(blank=True, null=True, verbose_name='Date initialized'), field=models.DateTimeField(blank=True, null=True, verbose_name='Date inited'),
), ),
migrations.AddField( migrations.AddField(
model_name='applethost', model_name='applethost',
name='initialized', name='inited',
field=models.BooleanField(default=False, verbose_name='Initialized'), field=models.BooleanField(default=False, verbose_name='Inited'),
),
migrations.AddField(
model_name='applethostdeployment',
name='date_finished',
field=models.DateTimeField(null=True, verbose_name='Date finished'),
),
migrations.AddField(
model_name='applethostdeployment',
name='date_start',
field=models.DateTimeField(db_index=True, null=True, verbose_name='Date start'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='appletpublication', model_name='appletpublication',
@ -36,7 +46,4 @@ class Migration(migrations.Migration):
name='host', name='host',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='publications', to='terminal.applethost', verbose_name='Host'), field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='publications', to='terminal.applethost', verbose_name='Host'),
), ),
migrations.DeleteModel(
name='AppletHostDeployment',
),
] ]

View File

@ -55,6 +55,7 @@ class AppletPublication(JMSBaseModel):
applet = models.ForeignKey('Applet', on_delete=models.PROTECT, related_name='publications', verbose_name=_('Applet')) applet = models.ForeignKey('Applet', on_delete=models.PROTECT, related_name='publications', verbose_name=_('Applet'))
host = models.ForeignKey('AppletHost', on_delete=models.PROTECT, related_name='publications', verbose_name=_('Host')) host = models.ForeignKey('AppletHost', on_delete=models.PROTECT, related_name='publications', verbose_name=_('Host'))
status = models.CharField(max_length=16, verbose_name=_('Status')) status = models.CharField(max_length=16, verbose_name=_('Status'))
published = models.BooleanField(default=False, verbose_name=_('Published'))
comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
class Meta: class Meta:

View File

@ -1,17 +1,19 @@
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from common.db.models import JMSBaseModel
from assets.models import Host from assets.models import Host
from ops.ansible import PlaybookRunner, JMSInventory
__all__ = ['AppletHost'] __all__ = ['AppletHost', 'AppletHostDeployment']
class AppletHost(Host): class AppletHost(Host):
LOCKING_ORG = 'SYSTEM'
account_automation = models.BooleanField(default=False, verbose_name=_('Account automation')) account_automation = models.BooleanField(default=False, verbose_name=_('Account automation'))
initialized = models.BooleanField(default=False, verbose_name=_('Initialized')) inited = models.BooleanField(default=False, verbose_name=_('Inited'))
date_inited = models.DateTimeField(null=True, blank=True, verbose_name=_('Date initialized')) date_inited = models.DateTimeField(null=True, blank=True, verbose_name=_('Date inited'))
date_synced = models.DateTimeField(null=True, blank=True, verbose_name=_('Date synced')) date_synced = models.DateTimeField(null=True, blank=True, verbose_name=_('Date synced'))
status = models.CharField(max_length=16, verbose_name=_('Status')) status = models.CharField(max_length=16, verbose_name=_('Status'))
applets = models.ManyToManyField( applets = models.ManyToManyField(
@ -19,11 +21,18 @@ class AppletHost(Host):
through='AppletPublication', through_fields=('host', 'applet'), through='AppletPublication', through_fields=('host', 'applet'),
) )
def deploy(self):
inventory = JMSInventory([self])
playbook = PlaybookRunner(inventory, 'applets.yml')
playbook.run()
def __str__(self): def __str__(self):
return self.name return self.name
class AppletHostDeployment(JMSBaseModel):
host = models.ForeignKey('AppletHost', on_delete=models.CASCADE, verbose_name=_('Hosting'))
status = models.CharField(max_length=16, default='', verbose_name=_('Status'))
date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True)
date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished"))
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
def start(self, **kwargs):
from ...automations.deploy_applet_host import DeployAppletHostManager
manager = DeployAppletHostManager(self)
manager.run(**kwargs)

View File

@ -5,13 +5,13 @@ from common.drf.fields import ObjectRelatedField, LabeledChoiceField
from common.validators import ProjectUniqueValidator from common.validators import ProjectUniqueValidator
from assets.models import Platform from assets.models import Platform
from assets.serializers import HostSerializer from assets.serializers import HostSerializer
from ..models import Applet, AppletPublication, AppletHost from ..models import Applet, AppletPublication, AppletHost, AppletHostDeployment
__all__ = [ __all__ = [
'AppletSerializer', 'AppletPublicationSerializer', 'AppletSerializer', 'AppletPublicationSerializer',
'AppletHostSerializer', 'AppletHostSerializer', 'AppletHostDeploymentSerializer',
'AppletUploadSerializer' 'AppletUploadSerializer',
] ]
@ -85,3 +85,13 @@ class AppletHostSerializer(HostSerializer):
validators.append(uniq_validator) validators.append(uniq_validator)
return validators return validators
class AppletHostDeploymentSerializer(serializers.ModelSerializer):
class Meta:
model = AppletHostDeployment
fields_mini = ['id', 'host', 'status']
read_only_fields = [
'status', 'date_created', 'date_updated',
'date_start', 'date_finished'
]
fields = fields_mini + ['comment'] + read_only_fields

View File

@ -12,9 +12,14 @@ from django.core.files.storage import default_storage
from common.utils import get_log_keep_day from common.utils import get_log_keep_day
from ops.celery.decorator import ( from ops.celery.decorator import (
register_as_period_task, after_app_ready_start, after_app_shutdown_clean_periodic register_as_period_task, after_app_ready_start,
after_app_shutdown_clean_periodic
) )
from .models import Status, Session, Command, Task, AppletHost from .models import (
Status, Session, Command, Task, AppletHost,
AppletHostDeployment
)
from orgs.utils import tmp_to_builtin_org
from .backends import server_replay_storage from .backends import server_replay_storage
from .utils import find_session_replay_local from .utils import find_session_replay_local
@ -84,16 +89,19 @@ def upload_session_replay_to_external_storage(session_id):
if not session: if not session:
logger.error(f'Session db item not found: {session_id}') logger.error(f'Session db item not found: {session_id}')
return return
local_path, foobar = find_session_replay_local(session) local_path, foobar = find_session_replay_local(session)
if not local_path: if not local_path:
logger.error(f'Session replay not found, may be upload error: {local_path}') logger.error(f'Session replay not found, may be upload error: {local_path}')
return return
abs_path = default_storage.path(local_path) abs_path = default_storage.path(local_path)
remote_path = session.get_relative_path_by_local_path(abs_path) remote_path = session.get_relative_path_by_local_path(abs_path)
ok, err = server_replay_storage.upload(abs_path, remote_path) ok, err = server_replay_storage.upload(abs_path, remote_path)
if not ok: if not ok:
logger.error(f'Session replay upload to external error: {err}') logger.error(f'Session replay upload to external error: {err}')
return return
try: try:
default_storage.delete(local_path) default_storage.delete(local_path)
except: except:
@ -103,5 +111,6 @@ def upload_session_replay_to_external_storage(session_id):
@shared_task @shared_task
def run_applet_host_deployment(did): def run_applet_host_deployment(did):
host = AppletHost.objects.get(id=did) with tmp_to_builtin_org(system=1):
host.deploy() deployment = AppletHostDeployment.objects.get(id=did)
deployment.start()

View File

@ -27,6 +27,7 @@ router.register(r'endpoint-rules', api.EndpointRuleViewSet, 'endpoint-rule')
router.register(r'applets', api.AppletViewSet, 'applet') router.register(r'applets', api.AppletViewSet, 'applet')
router.register(r'applet-hosts', api.AppletHostViewSet, 'applet-host') router.register(r'applet-hosts', api.AppletHostViewSet, 'applet-host')
router.register(r'applet-publications', api.AppletPublicationViewSet, 'applet-publication') router.register(r'applet-publications', api.AppletPublicationViewSet, 'applet-publication')
router.register(r'applet-host-deployments', api.AppletHostDeploymentViewSet, 'applet-host-deployment')
urlpatterns = [ urlpatterns = [