From 01e50d592e72d6c4bb15df3f8faca2dfbc127774 Mon Sep 17 00:00:00 2001 From: ibuler Date: Sun, 5 Mar 2017 11:38:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90AdHoc=20JMSHost=20JMSInventor?= =?UTF-8?q?y?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/models/asset.py | 12 + apps/assets/models/user.py | 21 +- apps/ops/api/serializers.py | 61 ----- apps/ops/api/views.py | 72 ----- apps/ops/models/__init__.py | 2 - apps/ops/models/cron.py | 61 ----- apps/ops/models/sudo.py | 321 ---------------------- apps/ops/models/utils.py | 5 +- apps/ops/urls/api_urls.py | 16 +- apps/ops/urls/view_urls.py | 12 - apps/ops/utils/ansible_api.py | 503 ++++++++++++++++++---------------- apps/ops/views.py | 47 +--- 12 files changed, 300 insertions(+), 833 deletions(-) delete mode 100644 apps/ops/models/cron.py delete mode 100644 apps/ops/models/sudo.py diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index da1ee37d6..4f9162de4 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -87,6 +87,18 @@ class Asset(models.Model): def to_json(self): pass + def _to_secret_json(self): + """Ansible use it create inventory""" + return { + 'hostname': self.hostname, + 'ip': self.ip, + 'port': self.port, + 'groups': [group.name for group in self.groups.all()], + 'username': self.admin_user.username, + 'password': self.admin_user.password, + 'private_key': self.admin_user.private_key, + } + class Meta: unique_together = ('ip', 'port') diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 5440d3910..94230ad9d 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -9,7 +9,7 @@ import logging from django.utils.translation import ugettext_lazy as _ from django.core.exceptions import ValidationError -from common.utils import signer, validate_ssh_private_key +from common.utils import signer, validate_ssh_private_key, ssh_key_string_to_obj __all__ = ['AdminUser', 'SystemUser', 'private_key_validator'] logger = logging.getLogger(__name__) @@ -24,12 +24,20 @@ def private_key_validator(value): class AdminUser(models.Model): + BECOME_METHOD_CHOICES = ( + ('sudo', 'sudo'), + ('su', 'su'), + ) name = models.CharField(max_length=128, unique=True, verbose_name=_('Name')) username = models.CharField(max_length=16, verbose_name=_('Username')) _password = models.CharField(max_length=256, blank=True, null=True, verbose_name=_('Password')) _private_key = models.CharField(max_length=4096, blank=True, null=True, verbose_name=_('SSH private key'), validators=[private_key_validator,]) _public_key = models.CharField(max_length=4096, blank=True, verbose_name=_('SSH public key')) + become = models.BooleanField(default=True) + become_method = models.CharField(choices=BECOME_METHOD_CHOICES, default='sudo', max_length=4) + become_user = models.CharField(default='root', max_length=64) + become_password = models.CharField(default='', max_length=128) comment = models.TextField(blank=True, verbose_name=_('Comment')) date_created = models.DateTimeField(auto_now_add=True, null=True) created_by = models.CharField(max_length=32, null=True, verbose_name=_('Created by')) @@ -41,7 +49,10 @@ class AdminUser(models.Model): @property def password(self): - return signer.unsign(self._password) + if self._password: + return signer.unsign(self._password) + else: + return '' @password.setter def password(self, password_raw): @@ -49,7 +60,11 @@ class AdminUser(models.Model): @property def private_key(self): - return signer.unsign(self._private_key) + if self._private_key: + key_str = signer.unsign(self._private_key) + return ssh_key_string_to_obj(key_str) + else: + return None @private_key.setter def private_key(self, private_key_raw): diff --git a/apps/ops/api/serializers.py b/apps/ops/api/serializers.py index 24abe7e2a..af472b3cb 100644 --- a/apps/ops/api/serializers.py +++ b/apps/ops/api/serializers.py @@ -1,65 +1,4 @@ # ~*~ coding: utf-8 ~*~ from __future__ import unicode_literals -from ops.models import * -from rest_framework import serializers - -class HostAliaSerializer(serializers.ModelSerializer): - - class Meta: - model = HostAlia - - -class CmdAliaSerializer(serializers.ModelSerializer): - - class Meta: - model = CmdAlia - - -class UserAliaSerializer(serializers.ModelSerializer): - - class Meta: - model = UserAlia - - -class RunasAliaSerializer(serializers.ModelSerializer): - - class Meta: - model = RunasAlia - - -class ExtraconfSerializer(serializers.ModelSerializer): - - class Meta: - model = Extra_conf - - -class PrivilegeSerializer(serializers.ModelSerializer): - - class Meta: - model = Privilege - - -class SudoSerializer(serializers.ModelSerializer): - - class Meta: - model = Sudo - - -class CronTableSerializer(serializers.ModelSerializer): - - class Meta: - model = CronTable - -class TaskSerializer(serializers.ModelSerializer): - sub_tasks = serializers.PrimaryKeyRelatedField(many=True, read_only=True) - - class Meta: - model = Task - read_only_fields = ('record',) - -class SubTaskSerializer(serializers.ModelSerializer): - - class Meta: - model = SubTask diff --git a/apps/ops/api/views.py b/apps/ops/api/views.py index eaacb2b76..affa9cdc7 100644 --- a/apps/ops/api/views.py +++ b/apps/ops/api/views.py @@ -5,75 +5,3 @@ from rest_framework import viewsets from serializers import * from permissions import * -__all__ = ["HostAliaViewSet", - "CmdAliaViewSet", - "UserAliaViewSet", - "RunasAliaViewSet", - "ExtraconfViewSet", - "PrivilegeViewSet", - "SudoViewSet", - "CronTableViewSet", - "TaskViewSet", - "SubTaskViewSet", - ] - - -class HostAliaViewSet(viewsets.ModelViewSet): - queryset = HostAlia.objects.all() - serializer_class = HostAliaSerializer - permission_classes = (AdminUserRequired,) - - -class CmdAliaViewSet(viewsets.ModelViewSet): - queryset = CmdAlia.objects.all() - serializer_class = CmdAliaSerializer - permission_classes = (AdminUserRequired,) - - -class UserAliaViewSet(viewsets.ModelViewSet): - queryset = UserAlia.objects.all() - serializer_class = UserAliaSerializer - permission_classes = (AdminUserRequired,) - - -class RunasAliaViewSet(viewsets.ModelViewSet): - queryset = RunasAlia.objects.all() - serializer_class = RunasAliaSerializer - permission_classes = (AdminUserRequired,) - - -class ExtraconfViewSet(viewsets.ModelViewSet): - queryset = Extra_conf.objects.all() - serializer_class = ExtraconfSerializer - permission_classes = (AdminUserRequired,) - - -class PrivilegeViewSet(viewsets.ModelViewSet): - queryset = Privilege.objects.all() - serializer_class = PrivilegeSerializer - permission_classes = (AdminUserRequired,) - - -class SudoViewSet(viewsets.ModelViewSet): - queryset = Sudo.objects.all() - serializer_class = SudoSerializer - permission_classes = (AdminUserRequired,) - - -class CronTableViewSet(viewsets.ModelViewSet): - queryset = CronTable.objects.all() - serializer_class = CronTableSerializer - permission_classes = (AdminUserRequired,) - -class TaskViewSet(viewsets.ModelViewSet): - queryset = Task.objects.all() - serializer_class = TaskSerializer - permission_classes = (AdminUserRequired,) - -class SubTaskViewSet(viewsets.ModelViewSet): - queryset = SubTask.objects.all() - serializer_class = SubTaskSerializer - permission_classes = (AdminUserRequired,) - - - diff --git a/apps/ops/models/__init__.py b/apps/ops/models/__init__.py index b7bfa1e0d..dbd842bab 100644 --- a/apps/ops/models/__init__.py +++ b/apps/ops/models/__init__.py @@ -1,6 +1,4 @@ from ansible import * -from cron import * -from sudo import * from utils import * from task import * diff --git a/apps/ops/models/cron.py b/apps/ops/models/cron.py deleted file mode 100644 index 766c46ece..000000000 --- a/apps/ops/models/cron.py +++ /dev/null @@ -1,61 +0,0 @@ -# ~*~ coding: utf-8 ~*~ -from __future__ import unicode_literals, absolute_import - -import logging - -from django.db import models -from assets.models import Asset -from django.utils.translation import ugettext_lazy as _ - -logger = logging.getLogger(__name__) - -__all__ = ["CronTable"] - - -class CronTable(models.Model): - name = models.CharField(max_length=128, blank=True, null=True, unique=True, verbose_name=_('Name'), - help_text=_("Description of a crontab entry")) - month = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Month'), - help_text=_("Month of the year the job should run ( 1-12, *, */2, etc )")) - weekday = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('WeekDay'), - help_text=_("Day of the week that the job should run" - " ( 0-6 for Sunday-Saturday, *, etc )")) - day = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Day'), - help_text=_("Day of the month the job should run ( 1-31, *, */2, etc )")) - hour = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Hour'), - help_text=_("Hour when the job should run ( 0-23, *, */2, etc )")) - minute = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Minute'), - help_text=_("Minute when the job should run ( 0-59, *, */2, etc )")) - job = models.CharField(max_length=4096, blank=True, null=True, verbose_name=_('Job'), - help_text=_("The command to execute or, if env is set, the value of " - "environment variable. Required if state=present.")) - user = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('User'), - help_text=_("The specific user whose crontab should be modified.")) - asset = models.ForeignKey(Asset, null=True, blank=True, related_name='crontables') - - @property - def describe(self): - return "http://docs.ansible.com/ansible/cron_module.html" - - @classmethod - def generate_fake(cls, count=20): - from random import seed, choice - import forgery_py - - seed() - for i in range(count): - cron = cls(name=forgery_py.name.full_name(), - month=str(choice(range(1,13))), - weekday=str(choice(range(0,7))), - day=str(choice(range(1,32))), - hour=str(choice(range(0,24))), - minute=str(choice(range(0,60))), - job=forgery_py.lorem_ipsum.sentence(), - user=forgery_py.name.first_name(), - ) - try: - cron.save() - logger.debug('Generate fake cron: %s' % cron.name) - except Exception as e: - print('Error: %s, continue...' % e.message) - continue \ No newline at end of file diff --git a/apps/ops/models/sudo.py b/apps/ops/models/sudo.py deleted file mode 100644 index 13713064c..000000000 --- a/apps/ops/models/sudo.py +++ /dev/null @@ -1,321 +0,0 @@ -# ~*~ coding: utf-8 ~*~ -from __future__ import unicode_literals, absolute_import - -import logging - -from jinja2 import Template -from django.db import models -from django.utils.timezone import now -from assets.models import Asset, AssetGroup -from django.utils.translation import ugettext_lazy as _ - -logger = logging.getLogger(__name__) - -__all__ = ["HostAlia", "UserAlia", "CmdAlia", "RunasAlia", "Privilege", "Extra_conf", "Sudo"] - - -class HostAlia(models.Model): - name = models.CharField(max_length=128, blank=True, null=True, unique=True, verbose_name=_('Host_Alias')) - host_items = models.TextField(blank=True, null=True, verbose_name=_('Host_Items')) - - def __unicode__(self): - return self.name - - @classmethod - def generate_fake(cls, count=20): - from random import seed - import forgery_py - - seed() - for i in range(count): - hostA = cls(name=forgery_py.name.full_name(), - host_items=forgery_py.lorem_ipsum.sentence(), - ) - try: - hostA.save() - logger.debug('Generate fake host alia: %s' % hostA.name) - except Exception as e: - print('Error: %s, continue...' % e.message) - continue - - -class UserAlia(models.Model): - name = models.CharField(max_length=128, blank=True, null=True, unique=True, verbose_name=_('User_Alias')) - user_items = models.TextField(blank=True, null=True, verbose_name=_('Host_Items')) - - def __unicode__(self): - return self.name - - @classmethod - def generate_fake(cls, count=20): - from random import seed - import forgery_py - - seed() - for i in range(count): - userA = cls(name=forgery_py.name.full_name(), - user_items=forgery_py.lorem_ipsum.sentence(), - ) - try: - userA.save() - logger.debug('Generate fake host alia: %s' % userA.name) - except Exception as e: - print('Error: %s, continue...' % e.message) - continue - - -class CmdAlia(models.Model): - name = models.CharField(max_length=128, blank=True, null=True, unique=True, verbose_name=_('Command_Alias')) - cmd_items = models.TextField(blank=True, null=True, verbose_name=_('Host_Items')) - - def __unicode__(self): - return self.name - - @classmethod - def generate_fake(cls, count=20): - from random import seed - import forgery_py - - seed() - for i in range(count): - cmdA = cls(name=forgery_py.name.full_name(), - cmd_items=forgery_py.lorem_ipsum.sentence(), - ) - try: - cmdA.save() - logger.debug('Generate fake command alia: %s' % cmdA.name) - except Exception as e: - print('Error: %s, continue...' % e.message) - continue - - -class RunasAlia(models.Model): - name = models.CharField(max_length=128, blank=True, null=True, unique=True, verbose_name=_('Runas_Alias')) - runas_items = models.TextField(blank=True, null=True, verbose_name=_('Host_Items')) - - def __unicode__(self): - return self.name - - @classmethod - def generate_fake(cls, count=20): - from random import seed - import forgery_py - - seed() - for i in range(count): - runas = cls(name=forgery_py.name.full_name(), - runas_items=forgery_py.lorem_ipsum.sentence(), - ) - try: - runas.save() - logger.debug('Generate fake RunAs alia: %s' % runas.name) - except Exception as e: - print('Error: %s, continue...' % e.message) - continue - - -class Privilege(models.Model): - name = models.CharField(max_length=128, unique=True, verbose_name=_('Name')) - user = models.ForeignKey(UserAlia, blank=True, null=True, related_name='privileges') - host = models.ForeignKey(HostAlia, blank=True, null=True, related_name='privileges') - runas = models.ForeignKey(RunasAlia, blank=True, null=True, related_name='privileges') - command = models.ForeignKey(CmdAlia, blank=True, null=True, related_name='privileges') - nopassword = models.BooleanField(default=True, verbose_name=_('Is_NoPassword')) - comment = models.TextField(blank=True, null=True, verbose_name=_('Comment')) - - def __unicode__(self): - return "[%s %s %s %s %s]" % (self.user.name, - self.host.name, - self.runas.name, - self.command.name, - self.nopassword) - - def to_tuple(self): - return self.user.name, self.host.name, self.runas.name, self.command.name, self.nopassword - - @classmethod - def generate_fake(cls, count=20): - from random import seed, choice - import forgery_py - - seed() - for i in range(count): - pri = cls(name=forgery_py.name.full_name(), - comment=forgery_py.lorem_ipsum.sentence(), - ) - try: - pri.user = choice(UserAlia.objects.all()) - pri.host = choice(HostAlia.objects.all()) - pri.runas = choice(RunasAlia.objects.all()) - pri.command = choice(CmdAlia.objects.all()) - pri.save() - logger.debug('Generate fake privileges: %s' % pri.name) - except Exception as e: - print('Error: %s, continue...' % e.message) - continue - - -class Extra_conf(models.Model): - line = models.TextField(blank=True, null=True, verbose_name=_('Extra_Item'), - help_text=_('The extra sudo config line.')) - - def __unicode__(self): - return self.line - - -class Sudo(models.Model): - """ - Sudo配置文件对象, 用于配置sudo的配置文件 - - :param extra_lines: [, ,...] - :param privileges: [(user, host, runas, command, nopassword),] - """ - - name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'), - help_text=_('Name for this sudo')) - created_time = models.DateTimeField(verbose_name=_('Created Time'), auto_created=True, - help_text=_('The create time of this sudo')) - modify_time = models.DateTimeField(auto_now=True, verbose_name=_('Modify Time'), - help_text=_('The recent modify time of this sudo')) - assets = models.ManyToManyField(Asset, blank=True, related_name='sudos') - asset_groups = models.ManyToManyField(AssetGroup, blank=True, related_name='sudos') - extra_lines = models.ManyToManyField(Extra_conf, related_name='sudos', blank=True) - privilege_items = models.ManyToManyField(Privilege, related_name='sudos', blank=True) - - @property - def all_assets(self): - assets = list(self.assets.all()) - for group in self.asset_groups.all(): - for asset in group.assets.all(): - if asset not in assets: - assets.append(asset) - return assets - - @property - def users(self): - return {privilege.user.name: privilege.user.user_items.split(',') for privilege in self.privilege_items.all()} - - @property - def commands(self): - return {privilege.command.name: privilege.command.cmd_items.split(',') for privilege in self.privilege_items.all()} - - @property - def hosts(self): - return {privilege.host.name: privilege.host.host_items.split(',') for privilege in self.privilege_items.all()} - - @property - def runas(self): - return {privilege.runas.name: privilege.runas.runas_items.split(',') for privilege in self.privilege_items.all()} - - @property - def extras(self): - return [extra.line for extra in self.extra_lines.all()] - - @property - def privileges(self): - return [privilege.to_tuple() for privilege in self.privilege_items.all()] - - @property - def content(self): - template = Template(self.__sudoers_jinja2_tmp__) - context = {"User_Alias": self.users, - "Cmnd_Alias": self.commands, - "Host_Alias": self.hosts, - "Runas_Alias": self.runas, - "Extra_Lines": self.extras, - "Privileges": self.privileges} - return template.render(context) - - @property - def __sudoers_jinja2_tmp__(self): - return """# management by JumpServer -# This file MUST be edited with the 'visudo' command as root. -# -# Please consider adding local content in /etc/sudoers.d/ instead of -# directly modifying this file. -# -# See the man page for details on how to write a sudoers file. -# -Defaults env_reset -Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" - -# JumpServer Generate Other Configure is here -{% if Extra_Lines -%} -{% for line in Extra_Lines -%} -{{ line }} -{% endfor %} -{%- endif %} - -# Host alias specification -{% if Host_Alias -%} -{% for flag, items in Host_Alias.iteritems() -%} -Host_Alias {{ flag }} = {{ items|join(', ') }} -{% endfor %} -{%- endif %} - -# User alias specification -{% if User_Alias -%} -{% for flag, items in User_Alias.iteritems() -%} -User_Alias {{ flag }} = {{ items|join(', ') }} -{% endfor %} -{%- endif %} - - -# Cmnd alias specification -{% if Cmnd_Alias -%} -{% for flag, items in Cmnd_Alias.iteritems() -%} -Cmnd_Alias {{ flag }} = {{ items|join(', ') }} -{% endfor %} -{%- endif %} - -# Run as alias specification -{% if Runas_Alias -%} -{% for flag, items in Runas_Alias.iteritems() -%} -Runas_Alias {{ flag }} = {{ items|join(', ') }} -{% endfor %} -{%- endif %} - -# User privilege specification -root ALL=(ALL:ALL) ALL - -# JumpServer Generate User privilege is here. -# Note privileges is a tuple list like [(user, host, runas, command, nopassword),] -{% if Privileges -%} -{% for User_Flag, Host_Flag, Runas_Flag, Command_Flag, NopassWord in Privileges -%} -{% if NopassWord -%} -{{ User_Flag }} {{ Host_Flag }}=({{ Runas_Flag }}) NOPASSWD: {{ Command_Flag }} -{%- else -%} -{{ User_Flag }} {{ Host_Flag }}=({{ Runas_Flag }}) {{ Command_Flag }} -{%- endif %} -{% endfor %} -{%- endif %} - -# Members of the admin group may gain root privileges -%admin ALL=(ALL) ALL - -# Allow members of group sudo to execute any command -%sudo ALL=(ALL:ALL) ALL - -# See sudoers(5) for more information on "#include" directives: - -#includedir /etc/sudoers.d -""" - - @classmethod - def generate_fake(cls, count=20): - from random import seed, choice - import forgery_py - - seed() - for i in range(count): - sudo = cls(name=forgery_py.name.full_name(), - created_time=now() - ) - try: - sudo.save() - sudo.privilege_items = [choice(Privilege.objects.all())] - sudo.save() - logger.debug('Generate fake cron: %s' % sudo.name) - except Exception as e: - print('Error: %s, continue...' % e.message) - continue \ No newline at end of file diff --git a/apps/ops/models/utils.py b/apps/ops/models/utils.py index 17c7cbab1..7d9f13e3d 100644 --- a/apps/ops/models/utils.py +++ b/apps/ops/models/utils.py @@ -2,13 +2,10 @@ from __future__ import unicode_literals from ansible import * -from cron import * -from sudo import * __all__ = ["generate_fake"] def generate_fake(): - for cls in (TaskRecord, AnsiblePlay, AnsibleTask, AnsibleHostResult, CronTable, - HostAlia, UserAlia, CmdAlia, RunasAlia, Privilege, Sudo): + for cls in (TaskRecord, AnsiblePlay, AnsibleTask, AnsibleHostResult): cls.generate_fake() \ No newline at end of file diff --git a/apps/ops/urls/api_urls.py b/apps/ops/urls/api_urls.py index 77141ed35..d97d28a4a 100644 --- a/apps/ops/urls/api_urls.py +++ b/apps/ops/urls/api_urls.py @@ -2,20 +2,6 @@ from __future__ import unicode_literals from rest_framework.routers import DefaultRouter -from ops import api as v1_api -__all__ = ["urlpatterns"] -api_router = DefaultRouter() -api_router.register(r'v1/host_alia', v1_api.HostAliaViewSet) -api_router.register(r'v1/user_alia', v1_api.UserAliaViewSet) -api_router.register(r'v1/cmd_alia', v1_api.CmdAliaViewSet) -api_router.register(r'v1/runas_alia', v1_api.RunasAliaViewSet) -api_router.register(r'v1/extra_conf', v1_api.ExtraconfViewSet) -api_router.register(r'v1/privilege', v1_api.PrivilegeViewSet) -api_router.register(r'v1/sudo', v1_api.SudoViewSet) -api_router.register(r'v1/cron', v1_api.CronTableViewSet) -api_router.register(r'v1/task', v1_api.TaskViewSet) -api_router.register(r'v1/subtask', v1_api.SubTaskViewSet) - -urlpatterns = api_router.urls \ No newline at end of file +urlpatterns = [] \ No newline at end of file diff --git a/apps/ops/urls/view_urls.py b/apps/ops/urls/view_urls.py index 9e9c68253..4b19c3f1c 100644 --- a/apps/ops/urls/view_urls.py +++ b/apps/ops/urls/view_urls.py @@ -8,18 +8,6 @@ from ops import views as page_view __all__ = ["urlpatterns"] urlpatterns = [ - # Resource Sudo url - url(r'^sudo/list$', page_view.SudoListView.as_view(), name='page-sudo-list'), - url(r'^sudo/create$', page_view.SudoCreateView.as_view(), name='page-sudo-create'), - url(r'^sudo/(?P[0-9]+)/detail$', page_view.SudoDetailView.as_view(), name='page-sudo-detail'), - url(r'^sudo/(?P[0-9]+)/update$', page_view.SudoUpdateView.as_view(), name='page-sudo-update'), - - # Resource Cron url - url(r'^cron/list$', page_view.CronListView.as_view(), name='page-cron-list'), - url(r'^cron/create$', page_view.CronCreateView.as_view(), name='page-cron-create'), - url(r'^cron/(?P[0-9]+)/detail$', page_view.CronDetailView.as_view(), name='page-cron-detail'), - url(r'^cron/(?P[0-9]+)/update$', page_view.CronUpdateView.as_view(), name='page-cron-update'), - # TResource Task url url(r'^task/list$', page_view.TaskListView.as_view(), name='page-task-list'), url(r'^task/create$', page_view.TaskCreateView.as_view(), name='page-task-create'), diff --git a/apps/ops/utils/ansible_api.py b/apps/ops/utils/ansible_api.py index 09af3a37c..38ab320b4 100644 --- a/apps/ops/utils/ansible_api.py +++ b/apps/ops/utils/ansible_api.py @@ -1,11 +1,12 @@ # ~*~ coding: utf-8 ~*~ -from __future__ import unicode_literals, print_function +# from __future__ import unicode_literals, print_function import os import json import logging import traceback import ansible.constants as default_config +from collections import namedtuple from uuid import uuid4 from django.utils import timezone @@ -17,11 +18,15 @@ from ansible.executor import playbook_executor from ansible.utils.display import Display from ansible.playbook.play import Play from ansible.plugins.callback import CallbackBase +import ansible.constants as C +from ansible.utils.vars import load_extra_vars +from ansible.utils.vars import load_options_vars -from ops.models import TaskRecord, AnsiblePlay, AnsibleTask, AnsibleHostResult +from ..models import TaskRecord, AnsiblePlay, AnsibleTask, AnsibleHostResult __all__ = ["ADHocRunner", "Options"] +C.HOST_KEY_CHECKING = False logger = logging.getLogger(__name__) @@ -30,149 +35,187 @@ class AnsibleError(StandardError): pass -class Options(object): - """Ansible运行时配置类, 用于初始化Ansible的一些默认配置. - """ - def __init__(self, verbosity=None, inventory=None, listhosts=None, subset=None, module_paths=None, extra_vars=None, - forks=10, ask_vault_pass=False, vault_password_files=None, new_vault_password_file=None, - output_file=None, tags=None, skip_tags=None, one_line=None, tree=None, ask_sudo_pass=False, ask_su_pass=False, - sudo=None, sudo_user=None, become=None, become_method=None, become_user=None, become_ask_pass=False, - ask_pass=False, private_key_file=None, remote_user=None, connection="smart", timeout=10, ssh_common_args=None, - sftp_extra_args=None, scp_extra_args=None, ssh_extra_args=None, poll_interval=None, seconds=None, check=False, - syntax=None, diff=None, force_handlers=None, flush_cache=None, listtasks=None, listtags=None, module_path=None): - self.verbosity = verbosity - self.inventory = inventory - self.listhosts = listhosts - self.subset = subset - self.module_paths = module_paths - self.extra_vars = extra_vars - self.forks = forks - self.ask_vault_pass = ask_vault_pass - self.vault_password_files = vault_password_files - self.new_vault_password_file = new_vault_password_file - self.output_file = output_file - self.tags = tags - self.skip_tags = skip_tags - self.one_line = one_line - self.tree = tree - self.ask_sudo_pass = ask_sudo_pass - self.ask_su_pass = ask_su_pass - self.sudo = sudo - self.sudo_user = sudo_user - self.become = become - self.become_method = become_method - self.become_user = become_user - self.become_ask_pass = become_ask_pass - self.ask_pass = ask_pass - self.private_key_file = private_key_file - self.remote_user = remote_user - self.connection = connection - self.timeout = timeout - self.ssh_common_args = ssh_common_args - self.sftp_extra_args = sftp_extra_args - self.scp_extra_args = scp_extra_args - self.ssh_extra_args = ssh_extra_args - self.poll_interval = poll_interval - self.seconds = seconds - self.check = check - self.syntax = syntax - self.diff = diff - self.force_handlers = force_handlers - self.flush_cache = flush_cache - self.listtasks = listtasks - self.listtags = listtags - self.module_path = module_path - self.__overwrite_default() - - def __overwrite_default(self): - """上面并不能包含Ansible所有的配置, 如果有其他的配置, - 可以通过替换default_config模块里面的变量进行重载,  - 比如 default_config.DEFAULT_ASK_PASS = False. - """ - default_config.HOST_KEY_CHECKING = False - - -class InventoryMixin(object): +# class Options(object): +# """Ansible运行时配置类, 用于初始化Ansible的一些默认配置. +# """ +# def __init__(self, verbosity=None, inventory=None, listhosts=None, subset=None, module_paths=None, extra_vars=None, +# forks=10, ask_vault_pass=False, vault_password_files=None, new_vault_password_file=None, +# output_file=None, tags=None, skip_tags=None, one_line=None, tree=None, ask_sudo_pass=False, ask_su_pass=False, +# sudo=None, sudo_user=None, become=None, become_method=None, become_user=None, become_ask_pass=False, +# ask_pass=False, private_key_file=None, remote_user=None, connection="smart", timeout=10, ssh_common_args=None, +# sftp_extra_args=None, scp_extra_args=None, ssh_extra_args=None, poll_interval=None, seconds=None, check=False, +# syntax=None, diff=None, force_handlers=None, flush_cache=None, listtasks=None, listtags=None, module_path=None): +# self.verbosity = verbosity +# self.inventory = inventory +# self.listhosts = listhosts +# self.subset = subset +# self.module_paths = module_paths +# self.extra_vars = extra_vars +# self.forks = forks +# self.ask_vault_pass = ask_vault_pass +# self.vault_password_files = vault_password_files +# self.new_vault_password_file = new_vault_password_file +# self.output_file = output_file +# self.tags = tags +# self.skip_tags = skip_tags +# self.one_line = one_line +# self.tree = tree +# self.ask_sudo_pass = ask_sudo_pass +# self.ask_su_pass = ask_su_pass +# self.sudo = sudo +# self.sudo_user = sudo_user +# self.become = become +# self.become_method = become_method +# self.become_user = become_user +# self.become_ask_pass = become_ask_pass +# self.ask_pass = ask_pass +# self.private_key_file = private_key_file +# self.remote_user = remote_user +# self.connection = connection +# self.timeout = timeout +# self.ssh_common_args = ssh_common_args +# self.sftp_extra_args = sftp_extra_args +# self.scp_extra_args = scp_extra_args +# self.ssh_extra_args = ssh_extra_args +# self.poll_interval = poll_interval +# self.seconds = seconds +# self.check = check +# self.syntax = syntax +# self.diff = diff +# self.force_handlers = force_handlers +# self.flush_cache = flush_cache +# self.listtasks = listtasks +# self.listtags = listtags +# self.module_path = module_path +# self.__overwrite_default() +# +# def __overwrite_default(self): +# """上面并不能包含Ansible所有的配置, 如果有其他的配置, +# 可以通过替换default_config模块里面的变量进行重载,  +# 比如 default_config.DEFAULT_ASK_PASS = False. +# """ +# default_config.HOST_KEY_CHECKING = False +Options = namedtuple("Options", [ + 'connection', 'module_path', 'private_key_file', "remote_user", "timeout", + 'forks', 'become', 'become_method', 'become_user', 'check', "extra_vars", + ] +) + + +class JMSHost(Host): + def __init__(self, asset): + self.asset = asset + self.name = name = asset.get('hostname') or asset.get('ip') + self.port = port = asset.get('port') or 22 + super(JMSHost, self).__init__(name, port) + self.set_all_variable() + + def set_all_variable(self): + asset = self.asset + self.set_variable('ansible_host', asset['ip']) + self.set_variable('ansible_port', asset['port']) + self.set_variable('ansible_user', asset['username']) + + # 添加密码和秘钥 + if asset.get('password'): + self.set_variable('ansible_ssh_pass', asset['password']) + if asset.get('key'): + self.set_variable('ansible_ssh_private_key_file', asset['private_key']) + + # 添加become支持 + become = asset.get("become", None) + if become is not None: + self.set_variable("ansible_become", True) + self.set_variable("ansible_become_method", become.get('method')) + self.set_variable("ansible_become_user", become.get('user')) + self.set_variable("ansible_become_pass", become.get('pass')) + else: + self.set_variable("ansible_become", False) + + +class JMSInventory(Inventory): """ 提供生成Ansible inventory对象的方法 """ - def gen_inventory(self): + def __init__(self, host_list=None): + if host_list is None: + host_list = [] + assert isinstance(host_list, list) + self.host_list = host_list + self.loader = DataLoader() + self.variable_manager = VariableManager() + super(JMSInventory, self).__init__(self.loader, self.variable_manager, + host_list=host_list) + + def parse_inventory(self, host_list): """用于生成动态构建Ansible Inventory. - self.hosts: [ - {"host": , - "port": , - "user": , - "pass": , - "key": , - "group": - "other_host_var": }, - {...}, - ] - self.group_vars: { - "groupName1": {"var1": , "var2": , ...}, - "groupName2": {"var1": , "var2": , ...}, - } + self.host_list: [ + {"name": "asset_name", + "ip": , + "port": , + "user": , + "pass": , + "key": , + "groups": ['group1', 'group2'], + "other_host_var": }, + {...}, + ] :return: 返回一个Ansible的inventory对象 """ # TODO: 验证输入 - # 创建Ansible Group,如果没有则创建default组 - for asset in self.hosts: - g_name = asset.get('group', 'default') - if g_name not in [g.name for g in self.groups]: - group = Group(name=g_name) - self.groups.append(group) - - # 添加组变量到相应的组上 - for group_name, variables in self.group_vars.iteritems(): - for g in self.groups: - if g.name == group_name: - for v_name, v_value in variables.iteritems(): - g.set_variable(v_name, v_value) - - # 往组里面添加Host - for asset in self.hosts: - # 添加Host链接的常用变量(host,port,user,pass,key) - host = Host(name=asset['name'], port=asset['port']) - host.set_variable('ansible_host', asset['ip']) - host.set_variable('ansible_port', asset['port']) - host.set_variable('ansible_user', asset['username']) - - # 添加密码和秘钥 - if asset.get('password'): - host.set_variable('ansible_ssh_pass', asset['password']) - if asset.get('key'): - host.set_variable('ansible_ssh_private_key_file', asset['key']) - - # 添加become支持 - become = asset.get("become", None) - if become is not None: - host.set_variable("ansible_become", True) - host.set_variable("ansible_become_method", become.get('method')) - host.set_variable("ansible_become_user", become.get('user')) - host.set_variable("ansible_become_pass", become.get('pass')) + ungrouped = Group('ungrouped') + all = Group('all') + all.add_child_group(ungrouped) + self.groups = dict(all=all, ungrouped=ungrouped) + + for asset in host_list: + host = JMSHost(asset=asset) + asset_groups = asset.get('groups') + if asset_groups: + for group_name in asset_groups: + if group_name not in self.groups: + group = Group(group_name) + self.groups[group_name] = group + else: + group = self.groups[group_name] + group.add_host(host) else: - host.set_variable("ansible_become", False) + ungrouped.add_host(host) + all.add_host(host) - # 添加其他Host的额外变量 - for key, value in asset.iteritems(): - if key not in ["name", "port", "ip", "username", "password", "key"]: - host.set_variable(key, value) - # 将host添加到组里面 - for g in self.groups: - if g.name == asset.get('group', 'default'): - g.add_host(host) +class BasicResultCallback(CallbackBase): + """ + Custom Callback + """ + def __init__(self, display=None): + self.result_q = dict(contacted={}, dark={}) + super(BasicResultCallback, self).__init__(display) + + def gather_result(self, n, res): + self.result_q[n].update({res._host.name: res._result}) - # 将组添加到Inventory里面,生成真正的inventory对象 - inventory = Inventory(loader=self.loader, variable_manager=self.variable_manager, host_list=[]) - for g in self.groups: - inventory.add_group(g) - self.variable_manager.set_inventory(inventory) - return inventory + def v2_runner_on_ok(self, result): + self.gather_result("contacted", result) + + def v2_runner_on_failed(self, result, ignore_errors=False): + self.gather_result("dark", result) + + def v2_runner_on_unreachable(self, result): + self.gather_result("dark", result) + + def v2_runner_on_skipped(self, result): + self.gather_result("dark", result) + + def v2_playbook_on_task_start(self, task, is_conditional): + pass + + def v2_playbook_on_play_start(self, play): + pass class CallbackModule(CallbackBase): @@ -310,7 +353,7 @@ class CallbackModule(CallbackBase): print("summary: %s", summary) -class PlayBookRunner(InventoryMixin): +class PlayBookRunner(object): """用于执行AnsiblePlaybook的接口.简化Playbook对象的使用. """ @@ -393,133 +436,121 @@ class PlayBookRunner(InventoryMixin): return stats -class ADHocRunner(InventoryMixin): +class ADHocRunner(object): """ ADHoc接口 """ - def __init__(self, play_data, config=None, *hosts, **group_vars): - """ - :param hosts: 见PlaybookRunner参数 - :param group_vars: 见PlaybookRunner参数 - :param config: Config实例 + def __init__(self, + hosts=C.DEFAULT_HOST_LIST, + module_name=C.DEFAULT_MODULE_NAME, # * command + module_args=C.DEFAULT_MODULE_ARGS, # * 'cmd args' + forks=C.DEFAULT_FORKS, # 5 + timeout=C.DEFAULT_TIMEOUT, # SSH timeout = 10s + pattern="all", # all + remote_user=C.DEFAULT_REMOTE_USER, # root + module_path=None, # dirs of custome modules + connection_type="smart", + become=None, + become_method=None, + become_user=None, + check=False, + passwords=None, + extra_vars=None, + private_key_file=None, + gather_facts='no'): + + self.pattern = pattern + self.variable_manager = VariableManager() + self.loader = DataLoader() + self.module_name = module_name + self.module_args = module_args + self.check_module_args() + self.gather_facts = gather_facts + self.results_callback = BasicResultCallback() + self.options = Options( + connection=connection_type, + timeout=timeout, + module_path=module_path, + forks=forks, + become=become, + become_method=become_method, + become_user=become_user, + check=check, + remote_user=remote_user, + extra_vars=extra_vars or [], + private_key_file=private_key_file, + ) - :param play_data: - play_data = dict( - name="Ansible Ad-Hoc", - hosts=pattern, - gather_facts=True, - tasks=[dict(action=dict(module='service', args={'name': 'vsftpd', 'state': 'restarted'}), async=async, poll=poll)] + self.variable_manager.extra_vars = load_extra_vars(self.loader, options=self.options) + self.variable_manager.options_vars = load_options_vars(self.options) + self.passwords = passwords or {} + self.inventory = JMSInventory(hosts) + self.variable_manager.set_inventory(self.inventory) + + self.play_source = dict( + name='Ansible Ad-hoc', + hosts=self.pattern, + gather_facts=self.gather_facts, + tasks=[dict(action=dict( + module=self.module_name, + args=self.module_args + ))] ) - """ - self.options = config if config != None else Options() - # 设置verbosity级别, 及命令行的--verbose选项 - self.display = Display() - self.display.verbosity = self.options.verbosity + self.play = Play().load( + self.play_source, + variable_manager=self.variable_manager, + loader=self.loader, + ) - # sudo的配置移到了Host级别去了,因此这里不再需要处理 - self.passwords = None + self.runner = TaskQueueManager( + inventory=self.inventory, + variable_manager=self.variable_manager, + loader=self.loader, + options=self.options, + passwords=self.passwords, + stdout_callback=self.results_callback, + ) - # 生成Ansible inventory, 这些变量Mixin都会用到 - self.hosts = hosts - self.group_vars = group_vars - self.loader = DataLoader() - self.variable_manager = VariableManager() - self.groups = [] - self.inventory = self.gen_inventory() + def check_module_args(self): + if self.module_name in C.MODULE_REQUIRE_ARGS and not self.module_args: + err = "No argument passed to '%s' module." % self.module_name + raise AnsibleError(err) - self.play = Play().load(play_data, variable_manager=self.variable_manager, loader=self.loader) + def run(self): + if not self.inventory.list_hosts("all"): + raise AnsibleError("Inventory is empty.") - @staticmethod - def update_db_tasker(tasker_id, ext_code): - try: - tasker = TaskRecord.objects.get(uuid=tasker_id) - tasker.end = timezone.now() - tasker.completed = True - tasker.exit_code = ext_code - tasker.save() - except Exception as e: - logger.error("Update Tasker Status into database error!, %s" % e.message) + if not self.inventory.list_hosts(self.pattern): + raise AnsibleError( + "pattern: %s dose not match any hosts." % self.pattern) - def create_db_tasker(self, name, uuid): try: - hosts = [host.get('name') for host in self.hosts] - tasker = TaskRecord(name=name, uuid=uuid, hosts=','.join(hosts), start=timezone.now()) - tasker.save() + self.runner.run(self.play) except Exception as e: - logger.error("Save Tasker to database error!, %s" % e.message) - - def run(self, tasker_name, tasker_uuid): - """执行ADHoc, 执行完后, 修改AnsiblePlay的状态为完成状态. - - :param tasker_uuid 用于标示此次task - """ - # 初始化callback插件,以及Tasker - - self.create_db_tasker(tasker_name, tasker_uuid) - self.results_callback = CallbackModule(tasker_uuid) - - tqm = None - # TODO:日志和结果分析 - try: - tqm = TaskQueueManager( - inventory=self.inventory, - variable_manager=self.variable_manager, - loader=self.loader, - stdout_callback=self.results_callback, - options=self.options, - passwords=self.passwords - ) - ext_code = tqm.run(self.play) - result = self.results_callback.results - - # 任务运行结束, 标示任务完成 - self.update_db_tasker(tasker_uuid, ext_code) - - ret = json.dumps(result) - return ext_code, ret - + pass + else: + return self.results_callback.result_q finally: - if tqm: - tqm.cleanup() + if self.runner: + self.runner.cleanup() + if self.loader: + self.loader.cleanup_all_tmp_files() def test_run(): - conf = Options() assets = [ { - "name": "192.168.1.119", - "ip": "192.168.1.119", - "port": "22", + "hostname": "192.168.152.129", + "ip": "192.168.152.129", + "port": 22, "username": "root", - "password": "tongfang_test", - "key": "asset_private_key", + "password": "redhat", }, - { - "name": "192.168.232.135", - "ip": "192.168.232.135", - "port": "22", - "username": "yumaojun", - "password": "xxx", - "key": "asset_private_key", - "become": {"method": "sudo", "user": "root", "pass": "xxx"} - }, ] - # 初始化Play - play_source = { - "name": "Ansible Play", - "hosts": "default", - "gather_facts": "no", - "tasks": [ - dict(action=dict(module='ping')), - ] - } - hoc = ADHocRunner(conf, play_source, *assets) - uuid = "tasker-" + uuid4().hex - ext_code, result = hoc.run("test_task", uuid) - print(ext_code) - print(result) - + hoc = ADHocRunner(module_name='shell', module_args='ls', hosts=assets) + ret = hoc.run() + print(ret) if __name__ == "__main__": test_run() diff --git a/apps/ops/views.py b/apps/ops/views.py index cf07aee59..c897cde79 100644 --- a/apps/ops/views.py +++ b/apps/ops/views.py @@ -8,54 +8,9 @@ from django.views.generic.detail import DetailView, SingleObjectMixin from users.utils import AdminUserRequiredMixin from ops.utils.mixins import CreateSudoPrivilegesMixin, ListSudoPrivilegesMixin -from ops.models import * +from .models import Task -class SudoListView(AdminUserRequiredMixin, ListSudoPrivilegesMixin, ListView): - paginate_by = settings.CONFIG.DISPLAY_PER_PAGE - model = Sudo - context_object_name = 'sudos' - template_name = 'sudo/list.html' - - -class SudoCreateView(AdminUserRequiredMixin, CreateSudoPrivilegesMixin, CreateView): - model = Sudo - template_name = 'sudo/create.html' - - -class SudoUpdateView(AdminUserRequiredMixin, UpdateView): - model = Sudo - template_name = 'sudo/update.html' - - -class SudoDetailView(DetailView): - model = Sudo - context_object_name = 'sudo' - template_name = 'sudo/detail.html' - - -class CronListView(AdminUserRequiredMixin, ListView): - paginate_by = settings.CONFIG.DISPLAY_PER_PAGE - model = CronTable - context_object_name = 'crons' - template_name = 'cron/list.html' - - -class CronCreateView(AdminUserRequiredMixin, CreateView): - model = CronTable - template_name = 'cron/create.html' - - -class CronUpdateView(AdminUserRequiredMixin, UpdateView): - model = CronTable - template_name = 'cron/update.html' - - -class CronDetailView(DetailView): - model = CronTable - context_object_name = 'cron' - template_name = 'cron/detail.html' - class TaskListView(AdminUserRequiredMixin, ListView): paginate_by = settings.CONFIG.DISPLAY_PER_PAGE model = Task -- GitLab