models.py 11.7 KB
Newer Older
baltery's avatar
baltery 已提交
1 2
# ~*~ coding: utf-8 ~*~

baltery's avatar
baltery 已提交
3
import json
baltery's avatar
baltery 已提交
4
import uuid
baltery's avatar
baltery 已提交
5

baltery's avatar
baltery 已提交
6
import time
baltery's avatar
baltery 已提交
7
from django.db import models
baltery's avatar
baltery 已提交
8
from django.utils import timezone
baltery's avatar
baltery 已提交
9
from django.utils.translation import ugettext_lazy as _
baltery's avatar
baltery 已提交
10
from django_celery_beat.models import CrontabSchedule, IntervalSchedule, PeriodicTask
baltery's avatar
baltery 已提交
11

baltery's avatar
baltery 已提交
12 13 14
from common.utils import get_signer, get_logger
from common.celery import delete_celery_periodic_task, create_or_update_celery_periodic_tasks, \
     disable_celery_periodic_task
baltery's avatar
baltery 已提交
15
from .ansible import AdHocRunner, AnsibleError
baltery's avatar
baltery 已提交
16
from .inventory import JMSInventory
baltery's avatar
baltery 已提交
17

baltery's avatar
baltery 已提交
18
__all__ = ["Task", "AdHoc", "AdHocRunHistory"]
baltery's avatar
baltery 已提交
19 20


baltery's avatar
baltery 已提交
21
logger = get_logger(__file__)
baltery's avatar
baltery 已提交
22
signer = get_signer()
baltery's avatar
baltery 已提交
23 24


baltery's avatar
baltery 已提交
25 26 27 28 29
class Task(models.Model):
    """
    This task is different ansible task, Task like 'push system user', 'get asset info' ..
    One task can have some versions of adhoc, run a task only run the latest version adhoc
    """
baltery's avatar
baltery 已提交
30
    id = models.UUIDField(default=uuid.uuid4, primary_key=True)
baltery's avatar
baltery 已提交
31
    name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'))
baltery's avatar
baltery 已提交
32 33
    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 * * * *"))
baltery's avatar
baltery 已提交
34
    is_periodic = models.BooleanField(default=False)
baltery's avatar
baltery 已提交
35
    callback = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Callback"))  # Callback must be a registered celery task
baltery's avatar
baltery 已提交
36
    is_deleted = models.BooleanField(default=False)
baltery's avatar
baltery 已提交
37
    comment = models.TextField(blank=True, verbose_name=_("Comment"))
baltery's avatar
baltery 已提交
38
    created_by = models.CharField(max_length=128, blank=True, null=True, default='')
baltery's avatar
baltery 已提交
39
    date_created = models.DateTimeField(auto_now_add=True)
baltery's avatar
baltery 已提交
40
    __latest_adhoc = None
baltery's avatar
baltery 已提交
41 42 43 44

    @property
    def short_id(self):
        return str(self.id).split('-')[-1]
baltery's avatar
baltery 已提交
45

baltery's avatar
baltery 已提交
46 47 48 49 50
    @property
    def latest_adhoc(self):
        if not self.__latest_adhoc:
            self.__latest_adhoc = self.get_latest_adhoc()
        return self.__latest_adhoc
baltery's avatar
baltery 已提交
51

baltery's avatar
baltery 已提交
52 53 54
    @latest_adhoc.setter
    def latest_adhoc(self, item):
        self.__latest_adhoc = item
baltery's avatar
baltery 已提交
55

baltery's avatar
baltery 已提交
56 57 58 59 60 61
    @property
    def latest_history(self):
        try:
            return self.history.all().latest()
        except AdHocRunHistory.DoesNotExist:
            return None
baltery's avatar
baltery 已提交
62

baltery's avatar
baltery 已提交
63 64 65 66 67
    def get_latest_adhoc(self):
        try:
            return self.adhoc.all().latest()
        except AdHoc.DoesNotExist:
            return None
baltery's avatar
baltery 已提交
68

baltery's avatar
baltery 已提交
69 70 71 72 73 74
    @property
    def history_summary(self):
        history = self.get_run_history()
        total = len(history)
        success = len([history for history in history if history.is_success])
        failed = len([history for history in history if not history.is_success])
baltery's avatar
baltery 已提交
75
        return {'total': total, 'success': success, 'failed': failed}
baltery's avatar
baltery 已提交
76

baltery's avatar
baltery 已提交
77 78 79
    def get_run_history(self):
        return self.history.all()

baltery's avatar
baltery 已提交
80
    def run(self, record=True):
baltery's avatar
baltery 已提交
81
        if self.latest_adhoc:
baltery's avatar
baltery 已提交
82
            return self.latest_adhoc.run(record=record)
baltery's avatar
baltery 已提交
83 84 85
        else:
            return {'error': 'No adhoc'}

baltery's avatar
baltery 已提交
86 87
    def save(self, force_insert=False, force_update=False, using=None,
             update_fields=None):
baltery's avatar
baltery 已提交
88 89
        from .tasks import run_ansible_task
        super().save(
baltery's avatar
baltery 已提交
90 91 92 93
            force_insert=force_insert,  force_update=force_update,
            using=using, update_fields=update_fields,
        )

baltery's avatar
baltery 已提交
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
        if self.is_periodic:
            interval = None
            crontab = None

            if self.interval:
                interval = self.interval
            elif self.crontab:
                crontab = self.crontab

            tasks = {
                self.name: {
                    "task": run_ansible_task.name,
                    "interval": interval,
                    "crontab": crontab,
                    "args": (str(self.id),),
                    "kwargs": {"callback": self.callback},
                    "enabled": True,
                }
            }
            create_or_update_celery_periodic_tasks(tasks)
baltery's avatar
baltery 已提交
114
        else:
baltery's avatar
baltery 已提交
115
            disable_celery_periodic_task(self.name)
baltery's avatar
baltery 已提交
116

baltery's avatar
baltery 已提交
117 118 119 120 121 122 123 124 125 126
    def delete(self, using=None, keep_parents=False):
        super().delete(using=using, keep_parents=keep_parents)
        delete_celery_periodic_task(self.name)

    @property
    def schedule(self):
        try:
            return PeriodicTask.objects.get(name=self.name)
        except PeriodicTask.DoesNotExist:
            return None
baltery's avatar
baltery 已提交
127

baltery's avatar
baltery 已提交
128 129 130
    def __str__(self):
        return self.name

baltery's avatar
baltery 已提交
131 132
    class Meta:
        db_table = 'ops_task'
baltery's avatar
baltery 已提交
133
        get_latest_by = 'date_created'
baltery's avatar
baltery 已提交
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149


class AdHoc(models.Model):
    """
    task: A task reference
    _tasks: [{'name': 'task_name', 'action': {'module': '', 'args': ''}, 'other..': ''}, ]
    _options: ansible options, more see ops.ansible.runner.Options
    _hosts: ["hostname1", "hostname2"], hostname must be unique key of cmdb
    run_as_admin: if true, then need get every host admin user run it, because every host may be have different admin user, so we choise host level
    run_as: if not run as admin, it run it as a system/common user from cmdb
    _become: May be using become [sudo, su] options. {method: "sudo", user: "user", pass: "pass"]
    pattern: Even if we set _hosts, We only use that to make inventory, We also can set `patter` to run task on match hosts
    """
    id = models.UUIDField(default=uuid.uuid4, primary_key=True)
    task = models.ForeignKey(Task, related_name='adhoc', on_delete=models.CASCADE)
    _tasks = models.TextField(verbose_name=_('Tasks'))
baltery's avatar
baltery 已提交
150
    pattern = models.CharField(max_length=64, default='{}', verbose_name=_('Pattern'))
baltery's avatar
baltery 已提交
151
    _options = models.CharField(max_length=1024, default='', verbose_name=_('Options'))
baltery's avatar
baltery 已提交
152 153
    _hosts = models.TextField(blank=True, verbose_name=_('Hosts'))  # ['hostname1', 'hostname2']
    run_as_admin = models.BooleanField(default=False, verbose_name=_('Run as admin'))
baltery's avatar
baltery 已提交
154 155
    run_as = models.CharField(max_length=128, default='', verbose_name=_("Run as"))
    _become = models.CharField(max_length=1024, default='', verbose_name=_("Become"))
baltery's avatar
baltery 已提交
156
    created_by = models.CharField(max_length=64, default='', null=True, verbose_name=_('Create by'))
157
    date_created = models.DateTimeField(auto_now_add=True)
baltery's avatar
baltery 已提交
158

baltery's avatar
baltery 已提交
159
    @property
baltery's avatar
baltery 已提交
160 161 162 163 164
    def tasks(self):
        return json.loads(self._tasks)

    @tasks.setter
    def tasks(self, item):
baltery's avatar
baltery 已提交
165 166 167
        if item and isinstance(item, list):
            self._tasks = json.dumps(item)
        else:
baltery's avatar
baltery 已提交
168
            raise SyntaxError('Tasks should be a list: {}'.format(item))
baltery's avatar
baltery 已提交
169 170

    @property
baltery's avatar
baltery 已提交
171 172
    def hosts(self):
        return json.loads(self._hosts)
baltery's avatar
baltery 已提交
173

baltery's avatar
baltery 已提交
174 175 176
    @hosts.setter
    def hosts(self, item):
        self._hosts = json.dumps(item)
baltery's avatar
baltery 已提交
177

baltery's avatar
baltery 已提交
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
    @property
    def inventory(self):
        if self.become:
            become_info = {
                'become': {
                    self.become
                }
            }
        else:
            become_info = None

        inventory = JMSInventory(
            self.hosts, run_as_admin=self.run_as_admin,
            run_as=self.run_as, become_info=become_info
        )
        return inventory

baltery's avatar
baltery 已提交
195
    @property
baltery's avatar
baltery 已提交
196 197 198 199 200 201
    def become(self):
        if self._become:
            return json.loads(signer.unsign(self._become))
        else:
            return {}

baltery's avatar
baltery 已提交
202 203 204 205 206 207 208 209 210 211
    def run(self, record=True):
        if record:
            return self._run_and_record()
        else:
            return self._run_only()

    def _run_and_record(self):
        history = AdHocRunHistory(adhoc=self, task=self.task)
        time_start = time.time()
        try:
baltery's avatar
baltery 已提交
212
            raw, summary = self._run_only()
baltery's avatar
baltery 已提交
213
            history.is_finished = True
baltery's avatar
baltery 已提交
214
            if summary.get('dark'):
baltery's avatar
baltery 已提交
215 216 217
                history.is_success = False
            else:
                history.is_success = True
baltery's avatar
baltery 已提交
218 219 220
            history.result = raw
            history.summary = summary
            return raw, summary
baltery's avatar
baltery 已提交
221 222
        except Exception as e:
            return {}, {"dark": {"all": str(e)}, "contacted": []}
baltery's avatar
baltery 已提交
223 224 225 226 227 228
        finally:
            history.date_finished = timezone.now()
            history.timedelta = time.time() - time_start
            history.save()

    def _run_only(self):
baltery's avatar
baltery 已提交
229
        runner = AdHocRunner(self.inventory)
baltery's avatar
baltery 已提交
230 231 232 233 234
        for k, v in self.options.items():
            runner.set_option(k, v)

        try:
            result = runner.run(self.tasks, self.pattern, self.task.name)
baltery's avatar
baltery 已提交
235
            return result.results_raw, result.results_summary
baltery's avatar
baltery 已提交
236 237
        except AnsibleError as e:
            logger.error("Failed run adhoc {}, {}".format(self.task.name, e))
baltery's avatar
baltery 已提交
238
            pass
baltery's avatar
baltery 已提交
239

baltery's avatar
baltery 已提交
240 241 242 243 244 245 246 247 248 249
    @become.setter
    def become(self, item):
        """
        :param item:  {
            method: "sudo",
            user: "user",
            pass: "pass",
        }
        :return:
        """
baltery's avatar
baltery 已提交
250
        self._become = signer.sign(json.dumps(item)).decode('utf-8')
baltery's avatar
baltery 已提交
251 252 253 254

    @property
    def options(self):
        if self._options:
baltery's avatar
baltery 已提交
255 256 257 258
            _options = json.loads(self._options)
            if isinstance(_options, dict):
                return _options
        return {}
baltery's avatar
baltery 已提交
259

baltery's avatar
baltery 已提交
260 261 262
    @options.setter
    def options(self, item):
        self._options = json.dumps(item)
baltery's avatar
baltery 已提交
263

baltery's avatar
baltery 已提交
264
    @property
baltery's avatar
baltery 已提交
265 266
    def short_id(self):
        return str(self.id).split('-')[-1]
baltery's avatar
baltery 已提交
267

baltery's avatar
baltery 已提交
268 269 270 271 272 273
    @property
    def latest_history(self):
        try:
            return self.history.all().latest()
        except AdHocRunHistory.DoesNotExist:
            return None
baltery's avatar
baltery 已提交
274

baltery's avatar
baltery 已提交
275 276 277 278 279
    def save(self, force_insert=False, force_update=False, using=None,
             update_fields=None):
        super().save(force_insert=force_insert, force_update=force_update,
                     using=using, update_fields=update_fields)

baltery's avatar
baltery 已提交
280
    def __str__(self):
baltery's avatar
baltery 已提交
281
        return "{} of {}".format(self.task.name, self.short_id)
baltery's avatar
baltery 已提交
282

baltery's avatar
baltery 已提交
283 284 285 286 287 288 289 290 291 292 293 294
    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            return False
        fields_check = []
        for field in self.__class__._meta.fields:
            if field.name not in ['id', 'date_created']:
                fields_check.append(field)
        for field in fields_check:
            if getattr(self, field.name) != getattr(other, field.name):
                return False
        return True

baltery's avatar
baltery 已提交
295
    class Meta:
baltery's avatar
baltery 已提交
296
        db_table = "ops_adhoc"
baltery's avatar
baltery 已提交
297
        get_latest_by = 'date_created'
baltery's avatar
baltery 已提交
298

baltery's avatar
baltery 已提交
299

baltery's avatar
baltery 已提交
300
class AdHocRunHistory(models.Model):
baltery's avatar
baltery 已提交
301 302 303 304
    """
    AdHoc running history.
    """
    id = models.UUIDField(default=uuid.uuid4, primary_key=True)
baltery's avatar
baltery 已提交
305 306
    task = models.ForeignKey(Task, related_name='history', on_delete=models.SET_NULL, null=True)
    adhoc = models.ForeignKey(AdHoc, related_name='history', on_delete=models.SET_NULL, null=True)
baltery's avatar
baltery 已提交
307 308 309 310 311
    date_start = models.DateTimeField(auto_now_add=True, verbose_name=_('Start time'))
    date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('End time'))
    timedelta = models.FloatField(default=0.0, verbose_name=_('Time'), null=True)
    is_finished = models.BooleanField(default=False, verbose_name=_('Is finished'))
    is_success = models.BooleanField(default=False, verbose_name=_('Is success'))
baltery's avatar
baltery 已提交
312 313
    _result = models.TextField(blank=True, null=True, verbose_name=_('Adhoc raw result'))
    _summary = models.TextField(blank=True, null=True, verbose_name=_('Adhoc result summary'))
baltery's avatar
baltery 已提交
314 315 316 317 318

    @property
    def short_id(self):
        return str(self.id).split('-')[-1]

baltery's avatar
baltery 已提交
319 320
    @property
    def result(self):
baltery's avatar
baltery 已提交
321 322 323 324
        if self._result:
            return json.loads(self._result)
        else:
            return {}
baltery's avatar
baltery 已提交
325 326 327 328 329 330 331

    @result.setter
    def result(self, item):
        self._result = json.dumps(item)

    @property
    def summary(self):
baltery's avatar
baltery 已提交
332 333 334 335
        if self._summary:
            return json.loads(self._summary)
        else:
            return {"ok": {}, "dark": {}}
baltery's avatar
baltery 已提交
336 337 338 339 340

    @summary.setter
    def summary(self, item):
        self._summary = json.dumps(item)

baltery's avatar
baltery 已提交
341 342 343 344 345 346 347 348
    @property
    def success_hosts(self):
        return self.summary.get('contacted', [])

    @property
    def failed_hosts(self):
        return self.summary.get('dark', {})

baltery's avatar
baltery 已提交
349 350
    def __str__(self):
        return self.short_id
baltery's avatar
baltery 已提交
351 352 353

    class Meta:
        db_table = "ops_adhoc_history"
baltery's avatar
baltery 已提交
354
        get_latest_by = 'date_start'