Merge pull request #2145 from jumpserver/dev

Dev
pull/2187/head
老广 2018-12-12 10:54:41 +08:00 committed by GitHub
commit f00a650366
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
246 changed files with 23712 additions and 8520 deletions

2
.gitignore vendored
View File

@ -17,7 +17,6 @@ dump.rdb
.idea/ .idea/
db.sqlite3 db.sqlite3
config.py config.py
migrations/
*.log *.log
host_rsa_key host_rsa_key
*.bat *.bat
@ -33,3 +32,4 @@ celerybeat-schedule.db
data/static data/static
docs/_build/ docs/_build/
xpack xpack
logs/*

23
Dockerfile Normal file
View File

@ -0,0 +1,23 @@
FROM registry.fit2cloud.com/public/python:v3
MAINTAINER Jumpserver Team <ibuler@qq.com>
WORKDIR /opt/jumpserver
RUN useradd jumpserver
COPY ./requirements /tmp/requirements
RUN yum -y install epel-release && cd /tmp/requirements && \
yum -y install $(cat rpm_requirements.txt)
RUN cd /tmp/requirements && pip install -r requirements.txt
COPY . /opt/jumpserver
COPY config_docker.py /opt/jumpserver/config.py
VOLUME /opt/jumpserver/data
VOLUME /opt/jumpserver/logs
ENV LANG=zh_CN.UTF-8
ENV LC_ALL=zh_CN.UTF-8
EXPOSE 8080
ENTRYPOINT ["./entrypoint.sh"]

View File

@ -2,4 +2,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
__version__ = "1.4.4" __version__ = "1.4.5"

View File

@ -9,7 +9,6 @@ from django.views.generic.detail import SingleObjectMixin
from common.utils import get_logger from common.utils import get_logger
from common.permissions import IsOrgAdmin, IsAppUser, IsOrgAdminOrAppUser from common.permissions import IsOrgAdmin, IsAppUser, IsOrgAdminOrAppUser
from ..models import Domain, Gateway from ..models import Domain, Gateway
from ..utils import test_gateway_connectability
from .. import serializers from .. import serializers
@ -54,7 +53,7 @@ class GatewayTestConnectionApi(SingleObjectMixin, APIView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.object = self.get_object(Gateway.objects.all()) self.object = self.get_object(Gateway.objects.all())
ok, e = test_gateway_connectability(self.object) ok, e = self.object.test_connective()
if ok: if ok:
return Response("ok") return Response("ok")
else: else:

View File

@ -142,14 +142,14 @@ class AssetBulkUpdateForm(OrgModelForm):
if k in changed_fields} if k in changed_fields}
assets = cleaned_data.pop('assets') assets = cleaned_data.pop('assets')
labels = cleaned_data.pop('labels', []) labels = cleaned_data.pop('labels', [])
nodes = cleaned_data.pop('nodes') nodes = cleaned_data.pop('nodes', None)
assets = Asset.objects.filter(id__in=[asset.id for asset in assets]) assets = Asset.objects.filter(id__in=[asset.id for asset in assets])
assets.update(**cleaned_data) assets.update(**cleaned_data)
if labels: if labels:
for label in labels: for asset in assets:
label.assets.add(*tuple(assets)) asset.labels.set(labels)
if nodes: if nodes:
for node in nodes: for asset in assets:
node.assets.add(*tuple(assets)) asset.nodes.set(nodes)
return assets return assets

View File

@ -36,6 +36,10 @@ class DomainForm(forms.ModelForm):
class GatewayForm(PasswordAndKeyAuthForm, OrgModelForm): class GatewayForm(PasswordAndKeyAuthForm, OrgModelForm):
protocol = forms.ChoiceField(
choices=[Gateway.PROTOCOL_CHOICES[0]],
)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
password_field = self.fields.get('password') password_field = self.fields.get('password')

View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-05 10:07
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='adminuser',
options={'ordering': ['name'], 'verbose_name': 'Admin user'},
),
migrations.AlterModelOptions(
name='asset',
options={'verbose_name': 'Asset'},
),
migrations.AlterModelOptions(
name='assetgroup',
options={'ordering': ['name'], 'verbose_name': 'Asset group'},
),
migrations.AlterModelOptions(
name='cluster',
options={'ordering': ['name'], 'verbose_name': 'Cluster'},
),
migrations.AlterModelOptions(
name='systemuser',
options={'ordering': ['name'], 'verbose_name': 'System user'},
),
]

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-09 15:31
from __future__ import unicode_literals
import assets.models.asset
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0002_auto_20180105_1807'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='cluster',
field=models.ForeignKey(default=assets.models.asset.default_cluster, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='assets', to='assets.Cluster', verbose_name='Cluster'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-25 04:18
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0003_auto_20180109_2331'),
]
operations = [
migrations.AlterField(
model_name='assetgroup',
name='created_by',
field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by'),
),
]

View File

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-26 08:37
from __future__ import unicode_literals
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('assets', '0004_auto_20180125_1218'),
]
operations = [
migrations.CreateModel(
name='Label',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('value', models.CharField(max_length=128, verbose_name='Value')),
('category', models.CharField(choices=[('S', 'System'), ('U', 'User')], default='U', max_length=128, verbose_name='Category')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('comment', models.TextField(blank=True, null=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
],
options={
'db_table': 'assets_label',
},
),
migrations.AlterUniqueTogether(
name='label',
unique_together=set([('name', 'value')]),
),
migrations.AddField(
model_name='asset',
name='labels',
field=models.ManyToManyField(blank=True, related_name='assets', to='assets.Label', verbose_name='Labels'),
),
]

View File

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-30 07:02
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0005_auto_20180126_1637'),
]
operations = [
migrations.RemoveField(
model_name='asset',
name='cabinet_no',
),
migrations.RemoveField(
model_name='asset',
name='cabinet_pos',
),
migrations.RemoveField(
model_name='asset',
name='env',
),
migrations.RemoveField(
model_name='asset',
name='remote_card_ip',
),
migrations.RemoveField(
model_name='asset',
name='status',
),
migrations.RemoveField(
model_name='asset',
name='type',
),
]

View File

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-02-25 10:15
from __future__ import unicode_literals
import assets.models.asset
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('assets', '0006_auto_20180130_1502'),
]
operations = [
migrations.CreateModel(
name='Node',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('key', models.CharField(max_length=64, unique=True, verbose_name='Key')),
('value', models.CharField(max_length=128, unique=True, verbose_name='Value')),
('child_mark', models.IntegerField(default=0)),
('date_create', models.DateTimeField(auto_now_add=True)),
],
),
migrations.RemoveField(
model_name='asset',
name='cluster',
),
migrations.RemoveField(
model_name='asset',
name='groups',
),
migrations.RemoveField(
model_name='systemuser',
name='cluster',
),
migrations.AlterField(
model_name='asset',
name='admin_user',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='assets.AdminUser', verbose_name='Admin user'),
),
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp')], default='ssh', max_length=16, verbose_name='Protocol'),
),
migrations.AddField(
model_name='asset',
name='nodes',
field=models.ManyToManyField(default=assets.models.asset.default_node, related_name='assets', to='assets.Node', verbose_name='Nodes'),
),
migrations.AddField(
model_name='systemuser',
name='nodes',
field=models.ManyToManyField(blank=True, to='assets.Node', verbose_name='Nodes'),
),
]

View File

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-03-06 10:04
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0007_auto_20180225_1815'),
]
operations = [
migrations.AlterField(
model_name='adminuser',
name='created_by',
field=models.CharField(max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(max_length=128, verbose_name='Username'),
),
migrations.AlterField(
model_name='asset',
name='platform',
field=models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Other', 'Other')], default='Linux', max_length=128, verbose_name='Platform'),
),
migrations.AlterField(
model_name='systemuser',
name='created_by',
field=models.CharField(max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(max_length=128, verbose_name='Username'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-03-07 04:12
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0008_auto_20180306_1804'),
]
operations = [
migrations.AlterField(
model_name='node',
name='value',
field=models.CharField(max_length=128, verbose_name='Value'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-03-07 09:49
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0009_auto_20180307_1212'),
]
operations = [
migrations.AlterField(
model_name='node',
name='value',
field=models.CharField(max_length=128, unique=True, verbose_name='Value'),
),
]

View File

@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-03-26 01:57
from __future__ import unicode_literals
import assets.models.utils
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('assets', '0010_auto_20180307_1749'),
]
operations = [
migrations.CreateModel(
name='Domain',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
],
),
migrations.CreateModel(
name='Gateway',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
('username', models.CharField(max_length=128, verbose_name='Username')),
('_password', models.CharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('_private_key', models.TextField(blank=True, max_length=4096, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key')),
('_public_key', models.TextField(blank=True, max_length=4096, verbose_name='SSH public key')),
('date_created', models.DateTimeField(auto_now_add=True)),
('date_updated', models.DateTimeField(auto_now=True)),
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
('ip', models.GenericIPAddressField(db_index=True, verbose_name='IP')),
('port', models.IntegerField(default=22, verbose_name='Port')),
('protocol', models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp')], default='ssh', max_length=16, verbose_name='Protocol')),
('comment', models.CharField(blank=True, max_length=128, null=True, verbose_name='Comment')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Domain', verbose_name='Domain')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='asset',
name='domain',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assets', to='assets.Domain', verbose_name='Domain'),
),
]

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-04 05:02
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0011_auto_20180326_0957'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='domain',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assets', to='assets.Domain', verbose_name='Domain'),
),
]

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-11 03:35
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0012_auto_20180404_1302'),
]
operations = [
migrations.AddField(
model_name='systemuser',
name='assets',
field=models.ManyToManyField(blank=True, to='assets.Asset', verbose_name='Assets'),
),
migrations.AlterField(
model_name='systemuser',
name='sudo',
field=models.TextField(default='/bin/whoami', verbose_name='Sudo'),
),
]

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-27 04:45
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0013_auto_20180411_1135'),
]
operations = [
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='gateway',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'),
),
]

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-05-10 04:35
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0014_auto_20180427_1245'),
]
operations = [
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='gateway',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-05-11 04:03
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0015_auto_20180510_1235'),
]
operations = [
migrations.AlterField(
model_name='node',
name='value',
field=models.CharField(max_length=128, verbose_name='Value'),
),
]

View File

@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-07-02 06:15
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
def migrate_win_to_ssh_protocol(apps, schema_editor):
asset_model = apps.get_model("assets", "Asset")
db_alias = schema_editor.connection.alias
asset_model.objects.using(db_alias).filter(platform__startswith='Win').update(protocol='rdp')
class Migration(migrations.Migration):
dependencies = [
('assets', '0016_auto_20180511_1203'),
]
operations = [
migrations.AddField(
model_name='asset',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)')], default='ssh', max_length=128, verbose_name='Protocol'),
),
migrations.AddField(
model_name='systemuser',
name='login_mode',
field=models.CharField(choices=[('auto', 'Automatic login'), ('manual', 'Manually login')], default='auto', max_length=10, verbose_name='Login mode'),
),
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='asset',
name='platform',
field=models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Windows2016', 'Windows(2016)'), ('Other', 'Other')], default='Linux', max_length=128, verbose_name='Platform'),
),
migrations.AlterField(
model_name='gateway',
name='username',
field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)')], default='ssh', max_length=16, verbose_name='Protocol'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.RunPython(migrate_win_to_ssh_protocol),
]

View File

@ -0,0 +1,84 @@
# Generated by Django 2.0.7 on 2018-08-07 03:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0017_auto_20180702_1415'),
]
operations = [
migrations.AddField(
model_name='adminuser',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='asset',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='domain',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='gateway',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='label',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='node',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='systemuser',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AlterField(
model_name='adminuser',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterField(
model_name='asset',
name='hostname',
field=models.CharField(max_length=128, verbose_name='Hostname'),
),
migrations.AlterField(
model_name='gateway',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterField(
model_name='systemuser',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterUniqueTogether(
name='adminuser',
unique_together={('name', 'org_id')},
),
migrations.AlterUniqueTogether(
name='asset',
unique_together={('org_id', 'hostname')},
),
migrations.AlterUniqueTogether(
name='gateway',
unique_together={('name', 'org_id')},
),
migrations.AlterUniqueTogether(
name='systemuser',
unique_together={('name', 'org_id')},
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 2.0.7 on 2018-08-16 05:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0018_auto_20180807_1116'),
]
operations = [
migrations.AddField(
model_name='asset',
name='cpu_vcpus',
field=models.IntegerField(null=True, verbose_name='CPU vcpus'),
),
migrations.AlterUniqueTogether(
name='label',
unique_together={('name', 'value', 'org_id')},
),
]

View File

@ -0,0 +1,48 @@
# Generated by Django 2.0.7 on 2018-08-16 08:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0019_auto_20180816_1320'),
]
operations = [
migrations.AlterField(
model_name='adminuser',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='asset',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='domain',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='gateway',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='label',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='node',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='systemuser',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 2.1 on 2018-09-03 03:32
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0020_auto_20180816_1652'),
]
operations = [
migrations.AlterModelOptions(
name='domain',
options={'verbose_name': 'Domain'},
),
migrations.AlterModelOptions(
name='gateway',
options={'verbose_name': 'Gateway'},
),
migrations.AlterModelOptions(
name='node',
options={'verbose_name': 'Node'},
),
]

View File

@ -0,0 +1,56 @@
# Generated by Django 2.1.1 on 2018-10-12 09:17
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('assets', '0021_auto_20180903_1132'),
]
operations = [
migrations.CreateModel(
name='CommandFilter',
fields=[
('org_id', models.CharField(blank=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=64, verbose_name='Name')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True)),
('date_updated', models.DateTimeField(auto_now=True)),
('created_by', models.CharField(blank=True, default='', max_length=128, verbose_name='Created by')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='CommandFilterRule',
fields=[
('org_id', models.CharField(blank=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('type', models.CharField(choices=[('regex', 'Regex'), ('command', 'Command')], default='command', max_length=16, verbose_name='Type')),
('priority', models.IntegerField(default=50, help_text='1-100, the lower will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')),
('content', models.TextField(help_text='One line one command', max_length=1024, verbose_name='Content')),
('action', models.IntegerField(choices=[(0, 'Deny'), (1, 'Allow')], default=0, verbose_name='Action')),
('comment', models.CharField(blank=True, default='', max_length=64, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True)),
('date_updated', models.DateTimeField(auto_now=True)),
('created_by', models.CharField(blank=True, default='', max_length=128, verbose_name='Created by')),
('filter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rules', to='assets.CommandFilter', verbose_name='Filter')),
],
options={
'ordering': ('priority', 'action'),
},
),
migrations.AddField(
model_name='systemuser',
name='cmd_filters',
field=models.ManyToManyField(blank=True, related_name='system_users', to='assets.CommandFilter', verbose_name='Command filter'),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 2.1.1 on 2018-10-16 08:50
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0022_auto_20181012_1717'),
]
operations = [
migrations.AlterModelOptions(
name='commandfilterrule',
options={'ordering': ('-priority', 'action')},
),
migrations.AlterField(
model_name='commandfilterrule',
name='priority',
field=models.IntegerField(default=50, help_text='1-100, the higher will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority'),
),
migrations.AlterField(
model_name='systemuser',
name='priority',
field=models.IntegerField(default=20, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority'),
),
]

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
from .user import * from .user import *
from .label import Label from .label import Label
from .cluster import * from .cluster import *

View File

@ -145,6 +145,13 @@ class Asset(OrgModelMixin):
return True, '' return True, ''
return False, warning return False, warning
def support_ansible(self):
if self.platform in ("Windows", "Windows2016", "Other"):
return False
if self.protocol != 'ssh':
return False
return True
def is_unixlike(self): def is_unixlike(self):
if self.platform not in ("Windows", "Windows2016"): if self.platform not in ("Windows", "Windows2016"):
return True return True
@ -257,7 +264,8 @@ class Asset(OrgModelMixin):
from random import seed, choice from random import seed, choice
import forgery_py import forgery_py
from django.db import IntegrityError from django.db import IntegrityError
from .node import Node
nodes = list(Node.objects.all())
seed() seed()
for i in range(count): for i in range(count):
ip = [str(i) for i in random.sample(range(255), 4)] ip = [str(i) for i in random.sample(range(255), 4)]
@ -268,6 +276,11 @@ class Asset(OrgModelMixin):
created_by='Fake') created_by='Fake')
try: try:
asset.save() asset.save()
if nodes and len(nodes) > 3:
_nodes = random.sample(nodes, 3)
else:
_nodes = [Node.default_node()]
asset.nodes.set(_nodes)
asset.system_users = [choice(SystemUser.objects.all()) for i in range(3)] asset.system_users = [choice(SystemUser.objects.all()) for i in range(3)]
logger.debug('Generate fake asset : %s' % asset.ip) logger.debug('Generate fake asset : %s' % asset.ip)
except IntegrityError: except IntegrityError:

View File

@ -105,6 +105,9 @@ class AssetUser(OrgModelMixin):
if update_fields: if update_fields:
self.save(update_fields=update_fields) self.save(update_fields=update_fields)
def get_auth(self, asset=None):
pass
def clear_auth(self): def clear_auth(self):
self._password = '' self._password = ''
self._private_key = '' self._private_key = ''

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import uuid import uuid
import re
from django.db import models from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
@ -35,7 +36,7 @@ class CommandFilterRule(OrgModelMixin):
(TYPE_COMMAND, _('Command')), (TYPE_COMMAND, _('Command')),
) )
ACTION_DENY, ACTION_ALLOW = range(2) ACTION_DENY, ACTION_ALLOW, ACTION_UNKNOWN = range(3)
ACTION_CHOICES = ( ACTION_CHOICES = (
(ACTION_DENY, _('Deny')), (ACTION_DENY, _('Deny')),
(ACTION_ALLOW, _('Allow')), (ACTION_ALLOW, _('Allow')),
@ -53,8 +54,34 @@ class CommandFilterRule(OrgModelMixin):
date_updated = models.DateTimeField(auto_now=True) date_updated = models.DateTimeField(auto_now=True)
created_by = models.CharField(max_length=128, blank=True, default='', verbose_name=_('Created by')) created_by = models.CharField(max_length=128, blank=True, default='', verbose_name=_('Created by'))
__pattern = None
class Meta: class Meta:
ordering = ('-priority', 'action') ordering = ('-priority', 'action')
@property
def _pattern(self):
if self.__pattern:
return self.__pattern
if self.type == 'command':
regex = []
for cmd in self.content.split('\r\n'):
cmd = cmd.replace(' ', '\s+')
regex.append(r'\b{0}\b'.format(cmd))
self.__pattern = re.compile(r'{}'.format('|'.join(regex)))
else:
self.__pattern = re.compile(r'{0}'.format(self.content))
return self.__pattern
def match(self, data):
found = self._pattern.search(data)
if not found:
return self.ACTION_UNKNOWN, ''
if self.action == self.ACTION_ALLOW:
return self.ACTION_ALLOW, found.group()
else:
return self.ACTION_DENY, found.group()
def __str__(self): def __str__(self):
return '{} % {}'.format(self.type, self.content) return '{} % {}'.format(self.type, self.content)

View File

@ -4,6 +4,8 @@
import uuid import uuid
import random import random
import paramiko
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 _
@ -57,3 +59,37 @@ class Gateway(AssetUser):
class Meta: class Meta:
unique_together = [('name', 'org_id')] unique_together = [('name', 'org_id')]
verbose_name = _("Gateway") verbose_name = _("Gateway")
def test_connective(self):
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
proxy = paramiko.SSHClient()
proxy.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
proxy.connect(self.ip, port=self.port,
username=self.username,
password=self.password,
pkey=self.private_key_obj)
except(paramiko.AuthenticationException,
paramiko.BadAuthenticationType,
paramiko.SSHException) as e:
return False, str(e)
sock = proxy.get_transport().open_channel(
'direct-tcpip', ('127.0.0.1', self.port), ('127.0.0.1', 0)
)
try:
client.connect("127.0.0.1", port=self.port,
username=self.username,
password=self.password,
key_filename=self.private_key_file,
sock=sock,
timeout=5)
except (paramiko.SSHException, paramiko.ssh_exception.SSHException,
paramiko.AuthenticationException, TimeoutError) as e:
return False, str(e)
finally:
client.close()
return True, None

View File

@ -17,7 +17,8 @@ class Label(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_("Name")) name = models.CharField(max_length=128, verbose_name=_("Name"))
value = models.CharField(max_length=128, verbose_name=_("Value")) value = models.CharField(max_length=128, verbose_name=_("Value"))
category = models.CharField(max_length=128, choices=CATEGORY_CHOICES, default=USER_CATEGORY, verbose_name=_("Category")) category = models.CharField(max_length=128, choices=CATEGORY_CHOICES,
default=USER_CATEGORY, verbose_name=_("Category"))
is_active = models.BooleanField(default=True, verbose_name=_("Is active")) is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
comment = models.TextField(blank=True, null=True, verbose_name=_("Comment")) comment = models.TextField(blank=True, null=True, verbose_name=_("Comment"))
date_created = models.DateTimeField( date_created = models.DateTimeField(

View File

@ -203,17 +203,14 @@ class Node(OrgModelMixin):
# 如果使用current_org 在set_current_org时会死循环 # 如果使用current_org 在set_current_org时会死循环
_current_org = get_current_org() _current_org = get_current_org()
with transaction.atomic(): with transaction.atomic():
if _current_org.is_root(): if not _current_org.is_real():
key = '0' return cls.default_node()
elif _current_org.is_default(): set_current_org(Organization.root())
key = '1' org_nodes_roots = cls.objects.filter(key__regex=r'^[0-9]+$')
else: org_nodes_roots_keys = org_nodes_roots.values_list('key', flat=True) or ['1']
set_current_org(Organization.root()) key = max([int(k) for k in org_nodes_roots_keys])
org_nodes_roots = cls.objects.filter(key__regex=r'^[0-9]+$') key = str(key + 1) if key != 0 else '2'
org_nodes_roots_keys = org_nodes_roots.values_list('key', flat=True) or ['1'] set_current_org(_current_org)
key = max([int(k) for k in org_nodes_roots_keys])
key = str(key + 1) if key != 0 else '2'
set_current_org(_current_org)
root = cls.objects.create(key=key, value=_current_org.name) root = cls.objects.create(key=key, value=_current_org.name)
return root return root

View File

@ -173,6 +173,15 @@ class SystemUser(AssetUser):
).distinct() ).distinct()
return rules return rules
def is_command_can_run(self, command):
for rule in self.cmd_filter_rules:
action, matched_cmd = rule.match(command)
if action == rule.ACTION_ALLOW:
return True, None
elif action == rule.ACTION_DENY:
return False, matched_cmd
return True, None
@classmethod @classmethod
def get_system_user_by_id_or_cached(cls, sid): def get_system_user_by_id_or_cached(cls, sid):
cached = cache.get(cls.cache_key.format(sid)) cached = cache.get(cls.cache_key.format(sid))

View File

@ -23,7 +23,6 @@ class DomainSerializer(serializers.ModelSerializer):
class GatewaySerializer(serializers.ModelSerializer): class GatewaySerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Gateway model = Gateway
fields = [ fields = [

View File

@ -68,6 +68,8 @@ class NodeSerializer(serializers.ModelSerializer):
@staticmethod @staticmethod
def get_assets_amount(obj): def get_assets_amount(obj):
if hasattr(obj, 'assets_amount'):
return obj.assets_amount
return obj.get_all_assets().count() return obj.get_all_assets().count()
@staticmethod @staticmethod
@ -86,7 +88,7 @@ class NodeSerializer(serializers.ModelSerializer):
class NodeAssetsSerializer(serializers.ModelSerializer): class NodeAssetsSerializer(serializers.ModelSerializer):
assets = serializers.PrimaryKeyRelatedField(many=True, queryset = Asset.objects.all()) assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all())
class Meta: class Meta:
model = Node model = Node

View File

@ -58,7 +58,7 @@ def on_system_user_nodes_change(sender, instance=None, **kwargs):
def on_system_user_assets_change(sender, instance=None, **kwargs): def on_system_user_assets_change(sender, instance=None, **kwargs):
if instance and kwargs["action"] == "post_add": if instance and kwargs["action"] == "post_add":
assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
push_system_user_to_assets(instance, assets) push_system_user_to_assets.delay(instance, assets)
@receiver(m2m_changed, sender=Asset.nodes.through) @receiver(m2m_changed, sender=Asset.nodes.through)

View File

@ -4,14 +4,15 @@ import re
import os import os
from celery import shared_task from celery import shared_task
from ops.celery import app as celery_app
from django.core.cache import cache from django.core.cache import cache
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from common.utils import get_object_or_none, capacity_convert, \ from common.utils import capacity_convert, \
sum_capacity, encrypt_password, get_logger sum_capacity, encrypt_password, get_logger
from ops.celery.utils import register_as_period_task, after_app_shutdown_clean, \ from ops.celery.utils import register_as_period_task, after_app_shutdown_clean, \
after_app_ready_start after_app_ready_start
from ops.celery import app as celery_app from orgs.utils import set_to_root_org
from .models import SystemUser, AdminUser, Asset from .models import SystemUser, AdminUser, Asset
from . import const from . import const
@ -20,34 +21,34 @@ from . import const
FORKS = 10 FORKS = 10
TIMEOUT = 60 TIMEOUT = 60
logger = get_logger(__file__) logger = get_logger(__file__)
CACHE_MAX_TIME = 60*60*60 CACHE_MAX_TIME = 60*60*2
disk_pattern = re.compile(r'^hd|sd|xvd|vd') disk_pattern = re.compile(r'^hd|sd|xvd|vd')
PERIOD_TASK = os.environ.get("PERIOD_TASK", "off") PERIOD_TASK = os.environ.get("PERIOD_TASK", "off")
@shared_task @shared_task
def set_assets_hardware_info(result, **kwargs): def set_assets_hardware_info(assets, result, **kwargs):
""" """
Using ops task run result, to update asset info Using ops task run result, to update asset info
@shared_task must be exit, because we using it as a task callback, is must @shared_task must be exit, because we using it as a task callback, is must
be a celery task also be a celery task also
:param assets:
:param result: :param result:
:param kwargs: {task_name: ""} :param kwargs: {task_name: ""}
:return: :return:
""" """
result_raw = result[0] result_raw = result[0]
assets_updated = [] assets_updated = []
for hostname, info in result_raw.get('ok', {}).items(): success_result = result_raw.get('ok', {})
for asset in assets:
hostname = asset.hostname
info = success_result.get(hostname, {})
info = info.get('setup', {}).get('ansible_facts', {}) info = info.get('setup', {}).get('ansible_facts', {})
if not info: if not info:
logger.error("Get asset info failed: {}".format(hostname)) logger.error(_("Get asset info failed: {}").format(hostname))
continue continue
asset = Asset.objects.get_object_by_fullname(hostname)
if not asset:
continue
___vendor = info.get('ansible_system_vendor', 'Unknown') ___vendor = info.get('ansible_system_vendor', 'Unknown')
___model = info.get('ansible_product_name', 'Unknown') ___model = info.get('ansible_product_name', 'Unknown')
___sn = info.get('ansible_product_serial', 'Unknown') ___sn = info.get('ansible_product_serial', 'Unknown')
@ -94,34 +95,43 @@ def update_assets_hardware_info_util(assets, task_name=None):
from ops.utils import update_or_create_ansible_task from ops.utils import update_or_create_ansible_task
if task_name is None: if task_name is None:
task_name = _("Update some assets hardware info") task_name = _("Update some assets hardware info")
# task_name = _("更新资产硬件信息")
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
hostname_list = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()] hosts = []
if not hostname_list: for asset in assets:
logger.info("Not hosts get, may be asset is not active or not unixlike platform") if not asset.is_active:
msg = _("Asset has been disabled, skipped: {}").format(asset)
logger.info(msg)
continue
if not asset.support_ansible():
msg = _("Asset may not be support ansible, skipped: {}").format(asset)
logger.info(msg)
continue
hosts.append(asset)
if not hosts:
logger.info(_("No assets matched, stop task"))
return {} return {}
created_by = str(assets[0].org_id)
task, created = update_or_create_ansible_task( task, created = update_or_create_ansible_task(
task_name, hosts=hostname_list, tasks=tasks, pattern='all', task_name, hosts=hosts, tasks=tasks, created_by=created_by,
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System', pattern='all', options=const.TASK_OPTIONS, run_as_admin=True,
) )
result = task.run() result = task.run()
# Todo: may be somewhere using # Todo: may be somewhere using
# Manual run callback function # Manual run callback function
set_assets_hardware_info(result) set_assets_hardware_info(assets, result)
return result return result
@shared_task @shared_task
def update_asset_hardware_info_manual(asset): def update_asset_hardware_info_manual(asset):
task_name = _("Update asset hardware info") task_name = _("Update asset hardware info: {}").format(asset.hostname)
# task_name = _("更新资产硬件信息") # task_name = _("更新资产硬件信息")
return update_assets_hardware_info_util([asset], task_name=task_name) return update_assets_hardware_info_util(
[asset], task_name=task_name
)
@celery_app.task @shared_task
@register_as_period_task(interval=3600)
@after_app_ready_start
@after_app_shutdown_clean
def update_assets_hardware_info_period(): def update_assets_hardware_info_period():
""" """
Update asset hardware period task Update asset hardware period task
@ -132,25 +142,28 @@ def update_assets_hardware_info_period():
return return
from ops.utils import update_or_create_ansible_task from ops.utils import update_or_create_ansible_task
from orgs.models import Organization
orgs = Organization.objects.all().values_list('id', flat=True)
orgs.append('')
task_name = _("Update assets hardware info period") task_name = _("Update assets hardware info period")
# task_name = _("定期更新资产硬件信息") # for org_id in orgs:
hostname_list = [ # org_id = str(org_id)
asset.fullname for asset in Asset.objects.all() # hostname_list = [
if asset.is_active and asset.is_unixlike() # asset for asset in Asset.objects.all()
] # if asset.is_active and asset.is_unixlike()
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS # ]
# tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
# Only create, schedule by celery beat #
update_or_create_ansible_task( # # Only create, schedule by celery beat
task_name, hosts=hostname_list, tasks=tasks, pattern='all', # update_or_create_ansible_task(
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System', # task_name, hosts=hostname_list, tasks=tasks, pattern='all',
interval=60*60*24, is_periodic=True, callback=set_assets_hardware_info.name, # options=const.TASK_OPTIONS, run_as_admin=True, created_by='System',
) # interval=60*60*24, is_periodic=True, callback=set_assets_hardware_info.name,
# )
## ADMIN USER CONNECTIVE ## ## ADMIN USER CONNECTIVE ##
@shared_task
def set_admin_user_connectability_info(result, **kwargs): def set_admin_user_connectability_info(result, **kwargs):
admin_user = kwargs.get("admin_user") admin_user = kwargs.get("admin_user")
task_name = kwargs.get("task_name") task_name = kwargs.get("task_name")
@ -182,36 +195,39 @@ def test_admin_user_connectability_util(admin_user, task_name):
from ops.utils import update_or_create_ansible_task from ops.utils import update_or_create_ansible_task
assets = admin_user.get_related_assets() assets = admin_user.get_related_assets()
hosts = [asset.fullname for asset in assets hosts = []
if asset.is_active and asset.is_unixlike()] for asset in assets:
if not asset.is_active:
msg = _("Asset has been disabled, skipped: {}").format(asset)
logger.info(msg)
continue
if not asset.support_ansible():
msg = _("Asset may not be support ansible, skipped: {}").format(asset)
logger.info(msg)
continue
hosts.append(asset)
if not hosts: if not hosts:
return logger.info(_("No assets matched, stop task"))
return {}
tasks = const.TEST_ADMIN_USER_CONN_TASKS tasks = const.TEST_ADMIN_USER_CONN_TASKS
task, created = update_or_create_ansible_task( task, created = update_or_create_ansible_task(
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all', task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System', options=const.TASK_OPTIONS, run_as_admin=True, created_by=admin_user.org_id,
) )
result = task.run() result = task.run()
set_admin_user_connectability_info(result, admin_user=admin_user.name) set_admin_user_connectability_info(result, admin_user=admin_user.name)
return result return result
@celery_app.task @shared_task
@register_as_period_task(interval=3600) @register_as_period_task(interval=3600)
@after_app_ready_start
@after_app_shutdown_clean
def test_admin_user_connectability_period(): def test_admin_user_connectability_period():
""" """
A period task that update the ansible task period A period task that update the ansible task period
""" """
if PERIOD_TASK != "on":
logger.debug("Period task disabled, test admin user connectability pass")
return
admin_users = AdminUser.objects.all() admin_users = AdminUser.objects.all()
for admin_user in admin_users: for admin_user in admin_users:
task_name = _("Test admin user connectability period: {}".format(admin_user.name)) task_name = _("Test admin user connectability period: {}").format(admin_user.name)
# task_name = _("定期测试管理账号可连接性: {}".format(admin_user.name))
test_admin_user_connectability_util(admin_user, task_name) test_admin_user_connectability_util(admin_user, task_name)
@ -229,14 +245,25 @@ def test_asset_connectability_util(assets, task_name=None):
if task_name is None: if task_name is None:
task_name = _("Test assets connectability") task_name = _("Test assets connectability")
# task_name = _("测试资产可连接性") # task_name = _("测试资产可连接性")
hosts = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()] hosts = []
for asset in assets:
if not asset.is_active:
msg = _("Asset has been disabled, skip: {}").format(asset)
logger.info(msg)
continue
if not asset.support_ansible():
msg = _("Asset may not be support ansible, skip: {}").format(asset)
logger.info(msg)
continue
hosts.append(asset)
if not hosts: if not hosts:
logger.info("No hosts, passed") logger.info(_("No assets, task stop"))
return {} return {}
tasks = const.TEST_ADMIN_USER_CONN_TASKS tasks = const.TEST_ADMIN_USER_CONN_TASKS
created_by = assets[0].org_id
task, created = update_or_create_ansible_task( task, created = update_or_create_ansible_task(
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all', task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System', options=const.TASK_OPTIONS, run_as_admin=True, created_by=created_by,
) )
result = task.run() result = task.run()
summary = result[1] summary = result[1]
@ -250,7 +277,8 @@ def test_asset_connectability_util(assets, task_name=None):
@shared_task @shared_task
def test_asset_connectability_manual(asset): def test_asset_connectability_manual(asset):
summary = test_asset_connectability_util([asset]) task_name = _("Test assets connectability: {}").format(asset)
summary = test_asset_connectability_util([asset], task_name=task_name)
if summary.get('dark'): if summary.get('dark'):
return False, summary['dark'] return False, summary['dark']
@ -267,7 +295,7 @@ def set_system_user_connectablity_info(result, **kwargs):
system_user = kwargs.get("system_user") system_user = kwargs.get("system_user")
if system_user is None: if system_user is None:
system_user = task_name.split(":")[-1] system_user = task_name.split(":")[-1]
cache_key = const.SYSTEM_USER_CONN_CACHE_KEY.format(system_user) cache_key = const.SYSTEM_USER_CONN_CACHE_KEY.format(str(system_user.id))
cache.set(cache_key, summary, CACHE_MAX_TIME) cache.set(cache_key, summary, CACHE_MAX_TIME)
@ -281,19 +309,28 @@ def test_system_user_connectability_util(system_user, assets, task_name):
:return: :return:
""" """
from ops.utils import update_or_create_ansible_task from ops.utils import update_or_create_ansible_task
# assets = system_user.get_assets() hosts = []
hosts = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
tasks = const.TEST_SYSTEM_USER_CONN_TASKS tasks = const.TEST_SYSTEM_USER_CONN_TASKS
for asset in assets:
if not asset.is_active:
msg = _("Asset has been disabled, skip: {}").format(asset)
logger.info(msg)
continue
if not asset.support_ansible():
msg = _("Asset may not be support ansible, skip: {}").format(asset)
logger.info(msg)
continue
hosts.append(asset)
if not hosts: if not hosts:
logger.info("No hosts, passed") logger.info(_("No assets matched, stop task"))
return {} return {}
task, created = update_or_create_ansible_task( task, created = update_or_create_ansible_task(
task_name, hosts=hosts, tasks=tasks, pattern='all', task_name, hosts=hosts, tasks=tasks, pattern='all',
options=const.TASK_OPTIONS, options=const.TASK_OPTIONS,
run_as=system_user.name, created_by="System", run_as=system_user, created_by=system_user.org_id,
) )
result = task.run() result = task.run()
set_system_user_connectablity_info(result, system_user=system_user.name) set_system_user_connectablity_info(result, system_user=system_user)
return result return result
@ -313,17 +350,13 @@ def test_system_user_connectability_a_asset(system_user, asset):
@shared_task @shared_task
@register_as_period_task(interval=3600)
@after_app_ready_start
@after_app_shutdown_clean
def test_system_user_connectability_period(): def test_system_user_connectability_period():
if PERIOD_TASK != "on": if PERIOD_TASK != "on":
logger.debug("Period task disabled, test system user connectability pass") logger.debug("Period task disabled, test system user connectability pass")
return return
system_users = SystemUser.objects.all() system_users = SystemUser.objects.all()
for system_user in system_users: for system_user in system_users:
task_name = _("Test system user connectability period: {}".format(system_user)) task_name = _("Test system user connectability period: {}").format(system_user)
# task_name = _("定期测试系统用户可连接性: {}".format(system_user)) # task_name = _("定期测试系统用户可连接性: {}".format(system_user))
test_system_user_connectability_util(system_user, task_name) test_system_user_connectability_util(system_user, task_name)
@ -374,28 +407,33 @@ def get_push_system_user_tasks(system_user):
@shared_task @shared_task
def push_system_user_util(system_users, assets, task_name): def push_system_user_util(system_user, assets, task_name):
from ops.utils import update_or_create_ansible_task from ops.utils import update_or_create_ansible_task
tasks = [] if not system_user.is_need_push():
for system_user in system_users: msg = _("Push system user task skip, auto push not enable or "
if not system_user.is_need_push(): "protocol is not ssh: {}").format(system_user.name)
msg = "push system user `{}` passed, may be not auto push or ssh " \ logger.info(msg)
"protocol is not ssh".format(system_user.name) return
tasks = get_push_system_user_tasks(system_user)
hosts = []
for asset in assets:
if not asset.is_active:
msg = _("Asset has been disabled, skip: {}").format(asset)
logger.info(msg) logger.info(msg)
continue continue
tasks.extend(get_push_system_user_tasks(system_user)) if not asset.support_ansible():
msg = _("Asset may not be support ansible, skip: {}").format(asset)
if not tasks: logger.info(msg)
logger.info("Not tasks, passed") continue
return {} hosts.append(asset)
hosts = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
if not hosts: if not hosts:
logger.info("Not hosts, passed") logger.info(_("No assets matched, stop task"))
return {} return {}
task, created = update_or_create_ansible_task( task, created = update_or_create_ansible_task(
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all', task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System' options=const.TASK_OPTIONS, run_as_admin=True,
created_by=system_user.org_id,
) )
return task.run() return task.run()
@ -403,24 +441,22 @@ def push_system_user_util(system_users, assets, task_name):
@shared_task @shared_task
def push_system_user_to_assets_manual(system_user): def push_system_user_to_assets_manual(system_user):
assets = system_user.get_assets() assets = system_user.get_assets()
# task_name = "推送系统用户到入资产: {}".format(system_user.name)
task_name = _("Push system users to assets: {}").format(system_user.name) task_name = _("Push system users to assets: {}").format(system_user.name)
return push_system_user_util([system_user], assets, task_name=task_name) return push_system_user_util(system_user, assets, task_name=task_name)
@shared_task @shared_task
def push_system_user_a_asset_manual(system_user, asset): def push_system_user_a_asset_manual(system_user, asset):
task_name = _("Push system users to asset: {} => {}").format( task_name = _("Push system users to asset: {} => {}").format(
system_user.name, asset.fullname system_user.name, asset
) )
return push_system_user_util([system_user], [asset], task_name=task_name) return push_system_user_util(system_user, [asset], task_name=task_name)
@shared_task @shared_task
def push_system_user_to_assets(system_user, assets): def push_system_user_to_assets(system_user, assets):
# task_name = _("推送系统用户到入资产: {}").format(system_user.name)
task_name = _("Push system users to assets: {}").format(system_user.name) task_name = _("Push system users to assets: {}").format(system_user.name)
return push_system_user_util.delay([system_user], assets, task_name) return push_system_user_util(system_user, assets, task_name)
# @shared_task # @shared_task

View File

@ -86,6 +86,9 @@ $(document).ready(function () {
allowClear: true, allowClear: true,
templateSelection: format templateSelection: format
}); });
$('#id_nodes.select2').select2({
closeOnSelect: false
});
$("#id_protocol").change(function (){ $("#id_protocol").change(function (){
var protocol = $("#id_protocol option:selected").text(); var protocol = $("#id_protocol option:selected").text();
var port = 22; var port = 22;

View File

@ -159,7 +159,7 @@
</span> </span>
</td> </td>
</tr> </tr>
{% if asset.is_unixlike %} {% if asset.protocol == 'ssh' %}
<tr> <tr>
<td>{% trans 'Refresh hardware' %}:</td> <td>{% trans 'Refresh hardware' %}:</td>
<td> <td>

View File

@ -452,7 +452,7 @@ $(document).ready(function(){
$.each(rows, function (index, obj) { $.each(rows, function (index, obj) {
assets.push(obj.id) assets.push(obj.id)
}); });
var _node_id = current_node ? current_node : null; var _node_id = current_node ? current_node.node_id : null;
$.ajax({ $.ajax({
url: "{% url "assets:asset-export" %}", url: "{% url "assets:asset-export" %}",
method: 'POST', method: 'POST',

View File

@ -32,9 +32,7 @@ $(document).ready(function () {
}) })
.on('click', '#btn_asset_modal_confirm', function () { .on('click', '#btn_asset_modal_confirm', function () {
var assets = asset_table2.selected; var assets = asset_table2.selected;
$.each(assets, function (id, data) { $('.select2').val(assets).trigger('change');
$('.select2').val(assets).trigger('change');
});
$("#asset_list_modal").modal('hide'); $("#asset_list_modal").modal('hide');
}) })

View File

@ -35,12 +35,7 @@ $(document).ready(function () {
}) })
.on('click', '#btn_asset_modal_confirm', function () { .on('click', '#btn_asset_modal_confirm', function () {
var assets = asset_table2.selected; var assets = asset_table2.selected;
$('.select2 option:selected').each(function (i, data) { $('#id_assets').val(assets).trigger('change');
assets.push($(data).attr('value'))
});
$.each(assets, function (id, data) {
$('.select2').val(assets).trigger('change');
});
$("#asset_list_modal").modal('hide'); $("#asset_list_modal").modal('hide');
}) })
</script> </script>

View File

@ -71,7 +71,6 @@ function initTable() {
} else { } else {
inited = true; inited = true;
} }
console.log("init table")
url = "{% url 'api-perms:my-assets' %}"; url = "{% url 'api-perms:my-assets' %}";
var options = { var options = {
ele: $('#user_assets_table'), ele: $('#user_assets_table'),
@ -108,7 +107,8 @@ function initTable() {
function onSelected(event, treeNode) { function onSelected(event, treeNode) {
url = '{% url "api-perms:my-node-assets" node_id=DEFAULT_PK %}'; url = '{% url "api-perms:my-node-assets" node_id=DEFAULT_PK %}';
url = url.replace("{{ DEFAULT_PK }}", treeNode.node_id); var node_id = treeNode.meta.node.id;
url = url.replace("{{ DEFAULT_PK }}", node_id);
setCookie('node_selected', treeNode.id); setCookie('node_selected', treeNode.id);
asset_table.ajax.url(url); asset_table.ajax.url(url);
asset_table.ajax.reload(); asset_table.ajax.reload();
@ -131,21 +131,10 @@ function initTree() {
}; };
var zNodes = []; var zNodes = [];
$.get("{% url 'api-perms:my-nodes' %}", function(data, status){ $.get("{% url 'api-perms:my-nodes-assets-as-tree' %}?show_assets=0", function(data, status){
$.each(data, function (index, value) {
value["node_id"] = value["id"];
value["id"] = value["tree_id"];
if (value["tree_id"] !== value["tree_parent"]) {
value["pId"] = value["tree_parent"];
}
value["isParent"] = value["is_node"];
value['name'] = value['value'];
});
zNodes = data; zNodes = data;
$.fn.zTree.init($("#assetTree"), setting, zNodes); $.fn.zTree.init($("#assetTree"), setting, zNodes);
zTree = $.fn.zTree.getZTreeObj("assetTree"); zTree = $.fn.zTree.getZTreeObj("assetTree");
var root = zTree.getNodes()[0];
zTree.expandNode(root);
}); });
} }

View File

@ -9,7 +9,11 @@ from .models import Asset, SystemUser, Label
def get_assets_by_id_list(id_list): def get_assets_by_id_list(id_list):
return Asset.objects.filter(id__in=id_list) return Asset.objects.filter(id__in=id_list).filter(is_active=True)
def get_system_users_by_id_list(id_list):
return SystemUser.objects.filter(id__in=id_list)
def get_assets_by_fullname_list(hostname_list): def get_assets_by_fullname_list(hostname_list):
@ -21,6 +25,11 @@ def get_system_user_by_name(name):
return system_user return system_user
def get_system_user_by_id(id):
system_user = get_object_or_none(SystemUser, id=id)
return system_user
class LabelFilter: class LabelFilter:
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset) queryset = super().filter_queryset(queryset)
@ -40,44 +49,3 @@ class LabelFilter:
for kwargs in conditions: for kwargs in conditions:
queryset = queryset.filter(**kwargs) queryset = queryset.filter(**kwargs)
return queryset return queryset
def test_gateway_connectability(gateway):
"""
Test system cant connect his assets or not.
:param gateway:
:return:
"""
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
proxy = paramiko.SSHClient()
proxy.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
proxy.connect(gateway.ip, gateway.port,
username=gateway.username,
password=gateway.password,
pkey=gateway.private_key_obj)
except(paramiko.AuthenticationException,
paramiko.BadAuthenticationType,
SSHException) as e:
return False, str(e)
sock = proxy.get_transport().open_channel(
'direct-tcpip', ('127.0.0.1', gateway.port), ('127.0.0.1', 0)
)
try:
client.connect("127.0.0.1", port=gateway.port,
username=gateway.username,
password=gateway.password,
key_filename=gateway.private_key_file,
sock=sock,
timeout=5
)
except (paramiko.SSHException, paramiko.ssh_exception.SSHException,
paramiko.AuthenticationException, TimeoutError) as e:
return False, str(e)
finally:
client.close()
return True, None

View File

@ -216,6 +216,7 @@ class AssetExportView(LoginRequiredMixin, View):
return HttpResponse('Json object not valid', status=400) return HttpResponse('Json object not valid', status=400)
if not assets_id: if not assets_id:
print(node_id)
node = get_object_or_none(Node, id=node_id) if node_id else Node.root() node = get_object_or_none(Node, id=node_id) if node_id else Node.root()
assets = node.get_all_assets() assets = node.get_all_assets()
for asset in assets: for asset in assets:
@ -277,7 +278,8 @@ class BulkImportAssetView(AdminUserRequiredMixin, JSONResponseMixin, FormView):
v = '' v = ''
elif k == 'domain': elif k == 'domain':
v = get_object_or_none(Domain, name=v) v = get_object_or_none(Domain, name=v)
elif k == 'platform':
v = v.lower().capitalize()
if v != '': if v != '':
asset_dict[k] = v asset_dict[k] = v

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-06 04:30
from __future__ import unicode_literals
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='FTPLog',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('user', models.CharField(max_length=128, verbose_name='User')),
('remote_addr', models.CharField(blank=True, max_length=15, null=True, verbose_name='Remote addr')),
('asset', models.CharField(max_length=1024, verbose_name='Asset')),
('system_user', models.CharField(max_length=128, verbose_name='System user')),
('operate', models.CharField(max_length=16, verbose_name='Operate')),
('filename', models.CharField(max_length=1024, verbose_name='Filename')),
('is_success', models.BooleanField(default=True, verbose_name='Success')),
('date_start', models.DateTimeField(auto_now_add=True)),
],
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.0.7 on 2018-08-07 03:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('audits', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='ftplog',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.0.7 on 2018-08-16 08:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('audits', '0002_ftplog_org_id'),
]
operations = [
migrations.AlterField(
model_name='ftplog',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
]

View File

@ -0,0 +1,51 @@
# Generated by Django 2.1 on 2018-09-03 03:32
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('users', '0014_auto_20180816_1652'),
('audits', '0003_auto_20180816_1652'),
]
operations = [
migrations.CreateModel(
name='OperateLog',
fields=[
('org_id', models.CharField(blank=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('user', models.CharField(max_length=128, verbose_name='User')),
('action', models.CharField(choices=[('create', 'Create'), ('update', 'Update'), ('delete', 'Delete')], max_length=16, verbose_name='Action')),
('resource_type', models.CharField(max_length=64, verbose_name='Resource Type')),
('resource', models.CharField(max_length=128, verbose_name='Resource')),
('remote_addr', models.CharField(blank=True, max_length=15, null=True, verbose_name='Remote addr')),
('datetime', models.DateTimeField(auto_now=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='PasswordChangeLog',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('user', models.CharField(max_length=128, verbose_name='User')),
('change_by', models.CharField(max_length=128, verbose_name='Change by')),
('remote_addr', models.CharField(blank=True, max_length=15, null=True, verbose_name='Remote addr')),
('datetime', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='UserLoginLog',
fields=[
],
options={
'proxy': True,
'indexes': [],
},
bases=('users.loginlog',),
),
]

View File

@ -13,4 +13,5 @@ urlpatterns = [
path('ftp-log/', views.FTPLogListView.as_view(), name='ftp-log-list'), path('ftp-log/', views.FTPLogListView.as_view(), name='ftp-log-list'),
path('operate-log/', views.OperateLogListView.as_view(), name='operate-log-list'), path('operate-log/', views.OperateLogListView.as_view(), name='operate-log-list'),
path('password-change-log/', views.PasswordChangeLogList.as_view(), name='password-change-log-list'), path('password-change-log/', views.PasswordChangeLogList.as_view(), name='password-change-log-list'),
path('command-execution-log/', views.CommandExecutionListView.as_view(), name='command-execution-log-list'),
] ]

View File

@ -7,6 +7,8 @@ from common.mixins import DatetimeSearchMixin
from common.permissions import AdminUserRequiredMixin from common.permissions import AdminUserRequiredMixin
from orgs.utils import current_org from orgs.utils import current_org
from ops.views import CommandExecutionListView as UserCommandExecutionListView
from users.models import User
from .models import FTPLog, OperateLog, PasswordChangeLog, UserLoginLog from .models import FTPLog, OperateLog, PasswordChangeLog, UserLoginLog
@ -122,7 +124,10 @@ class PasswordChangeLogList(AdminUserRequiredMixin, DatetimeSearchMixin, ListVie
date_from = date_to = None date_from = date_to = None
def get_queryset(self): def get_queryset(self):
self.queryset = super().get_queryset() users = current_org.get_org_users()
self.queryset = super().get_queryset().filter(
user__in=[user.__str__() for user in users]
)
self.user = self.request.GET.get('user') self.user = self.request.GET.get('user')
filter_kwargs = dict() filter_kwargs = dict()
@ -184,7 +189,7 @@ class LoginLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = { context = {
'app': _('Users'), 'app': _('Audits'),
'action': _('Login log'), 'action': _('Login log'),
'date_from': self.date_from, 'date_from': self.date_from,
'date_to': self.date_to, 'date_to': self.date_to,
@ -193,4 +198,35 @@ class LoginLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
'user_list': self.get_org_users(), 'user_list': self.get_org_users(),
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class CommandExecutionListView(UserCommandExecutionListView):
user_id = None
def get_queryset(self):
queryset = self._get_queryset()
self.user_id = self.request.GET.get('user')
org_users = self.get_user_list()
if self.user_id:
queryset = queryset.filter(user=self.user_id)
else:
queryset = queryset.filter(user__in=org_users)
return queryset
def get_user_list(self):
users = current_org.get_org_users()
return users
def get_context_data(self, **kwargs):
context = {
'app': _('Audits'),
'action': _('Command execution list'),
'date_from': self.date_from,
'date_to': self.date_to,
'user_list': self.get_user_list(),
'keyword': self.keyword,
'user_id': self.user_id,
}
kwargs.update(context)
return super().get_context_data(**kwargs)

View File

View File

@ -0,0 +1,95 @@
# coding:utf-8
#
import ldap
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django_auth_ldap.backend import _LDAPUser, LDAPBackend
from django_auth_ldap.config import _LDAPConfig, LDAPSearch, LDAPSearchUnion
logger = _LDAPConfig.get_logger()
class LDAPAuthorizationBackend(LDAPBackend):
"""
Override this class to override _LDAPUser to LDAPUser
"""
def authenticate(self, request=None, username=None, password=None, **kwargs):
if password or self.settings.PERMIT_EMPTY_PASSWORD:
ldap_user = LDAPUser(self, username=username.strip(), request=request)
user = self.authenticate_ldap_user(ldap_user, password)
else:
logger.debug('Rejecting empty password for {}'.format(username))
user = None
return user
def get_user(self, user_id):
user = None
try:
user = self.get_user_model().objects.get(pk=user_id)
LDAPUser(self, user=user) # This sets user.ldap_user
except ObjectDoesNotExist:
pass
return user
def get_group_permissions(self, user, obj=None):
if not hasattr(user, 'ldap_user') and self.settings.AUTHORIZE_ALL_USERS:
LDAPUser(self, user=user) # This sets user.ldap_user
if hasattr(user, 'ldap_user'):
permissions = user.ldap_user.get_group_permissions()
else:
permissions = set()
return permissions
def populate_user(self, username):
ldap_user = LDAPUser(self, username=username)
user = ldap_user.populate_user()
return user
class LDAPUser(_LDAPUser):
def _search_for_user_dn(self):
"""
This method was overridden because the AUTH_LDAP_USER_SEARCH
configuration in the settings.py file
is configured with a `lambda` problem value
"""
user_search_union = [
LDAPSearch(
USER_SEARCH, ldap.SCOPE_SUBTREE,
settings.AUTH_LDAP_SEARCH_FILTER
)
for USER_SEARCH in str(settings.AUTH_LDAP_SEARCH_OU).split("|")
]
search = LDAPSearchUnion(*user_search_union)
if search is None:
raise ImproperlyConfigured(
'AUTH_LDAP_USER_SEARCH must be an LDAPSearch instance.'
)
results = search.execute(self.connection, {'user': self._username})
if results is not None and len(results) == 1:
(user_dn, self._user_attrs) = next(iter(results))
else:
user_dn = None
return user_dn
def _populate_user_from_attributes(self):
super()._populate_user_from_attributes()
if not hasattr(self._user, 'email') or '@' not in self._user.email:
email = '{}@{}'.format(self._user.username, settings.EMAIL_SUFFIX)
setattr(self._user, 'email', email)

View File

@ -1,7 +1,8 @@
from django.http.request import QueryDict from django.http.request import QueryDict
from django.contrib.auth.signals import user_logged_out
from django.dispatch import receiver
from django.conf import settings from django.conf import settings
from django.dispatch import receiver
from django.contrib.auth.signals import user_logged_out
from django_auth_ldap.backend import populate_user
from .openid import client from .openid import client
from .signals import post_create_openid_user from .signals import post_create_openid_user
@ -31,3 +32,9 @@ def on_post_create_openid_user(sender, user=None, **kwargs):
user.source = user.SOURCE_OPENID user.source = user.SOURCE_OPENID
user.save() user.save()
@receiver(populate_user)
def on_ldap_create_user(sender, user, ldap_user, **kwargs):
if user and user.name != 'admin':
user.source = user.SOURCE_LDAP
user.save()

View File

@ -168,13 +168,16 @@ class DjangoSettingsAPI(APIView):
return Response("Not in debug mode") return Response("Not in debug mode")
data = {} data = {}
for k, v in settings.__dict__.items(): for i in [settings, getattr(settings, '_wrapped')]:
if k and k.isupper(): if not i:
try: continue
json.dumps(v) for k, v in i.__dict__.items():
data[k] = v if k and k.isupper():
except (json.JSONDecodeError, TypeError): try:
data[k] = str(v) json.dumps(v)
data[k] = v
except (json.JSONDecodeError, TypeError):
data[k] = str(v)
return Response(data) return Response(data)

View File

@ -4,29 +4,28 @@ import json
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db import transaction from django.db import transaction
from django.conf import settings
from .models import Setting, common_settings from .models import Setting, settings
from .fields import FormDictField, FormEncryptCharField, \ from .fields import FormDictField, FormEncryptCharField, \
FormEncryptMixin, FormEncryptDictField FormEncryptMixin
class BaseForm(forms.Form): class BaseForm(forms.Form):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
for name, field in self.fields.items(): for name, field in self.fields.items():
db_value = getattr(common_settings, name) value = getattr(settings, name, None)
django_value = getattr(settings, name) if hasattr(settings, name) else None # django_value = getattr(settings, name) if hasattr(settings, name) else None
if db_value is None and django_value is None: if value is None: # and django_value is None:
continue continue
if db_value is False or db_value: if value is not None:
if isinstance(db_value, dict): if isinstance(value, dict):
db_value = json.dumps(db_value) value = json.dumps(value)
initial_value = db_value initial_value = value
elif django_value is False or django_value: # elif django_value is False or django_value:
initial_value = django_value # initial_value = django_value
else: else:
initial_value = '' initial_value = ''
field.initial = initial_value field.initial = initial_value
@ -44,7 +43,7 @@ class BaseForm(forms.Form):
field = self.fields[name] field = self.fields[name]
if isinstance(field.widget, forms.PasswordInput) and not value: if isinstance(field.widget, forms.PasswordInput) and not value:
continue continue
if value == getattr(common_settings, name): if value == getattr(settings, name):
continue continue
encrypted = True if isinstance(field, FormEncryptMixin) else False encrypted = True if isinstance(field, FormEncryptMixin) else False
@ -70,7 +69,7 @@ class BasicSettingForm(BaseForm):
) )
EMAIL_SUBJECT_PREFIX = forms.CharField( EMAIL_SUBJECT_PREFIX = forms.CharField(
max_length=1024, label=_("Email Subject Prefix"), max_length=1024, label=_("Email Subject Prefix"),
initial="[Jumpserver] " help_text=_("Tips: Some word will be intercept by mail provider")
) )
@ -98,21 +97,21 @@ class EmailSettingForm(BaseForm):
class LDAPSettingForm(BaseForm): class LDAPSettingForm(BaseForm):
AUTH_LDAP_SERVER_URI = forms.CharField( AUTH_LDAP_SERVER_URI = forms.CharField(
label=_("LDAP server"), initial='ldap://localhost:389' label=_("LDAP server"),
) )
AUTH_LDAP_BIND_DN = forms.CharField( AUTH_LDAP_BIND_DN = forms.CharField(
label=_("Bind DN"), initial='cn=admin,dc=jumpserver,dc=org' label=_("Bind DN"),
) )
AUTH_LDAP_BIND_PASSWORD = FormEncryptCharField( AUTH_LDAP_BIND_PASSWORD = FormEncryptCharField(
label=_("Password"), initial='', label=_("Password"),
widget=forms.PasswordInput, required=False widget=forms.PasswordInput, required=False
) )
AUTH_LDAP_SEARCH_OU = forms.CharField( AUTH_LDAP_SEARCH_OU = forms.CharField(
label=_("User OU"), initial='ou=tech,dc=jumpserver,dc=org', label=_("User OU"),
help_text=_("Use | split User OUs") help_text=_("Use | split User OUs")
) )
AUTH_LDAP_SEARCH_FILTER = forms.CharField( AUTH_LDAP_SEARCH_FILTER = forms.CharField(
label=_("User search filter"), initial='(cn=%(user)s)', label=_("User search filter"),
help_text=_("Choice may be (cn|uid|sAMAccountName)=%(user)s)") help_text=_("Choice may be (cn|uid|sAMAccountName)=%(user)s)")
) )
AUTH_LDAP_USER_ATTR_MAP = FormDictField( AUTH_LDAP_USER_ATTR_MAP = FormDictField(
@ -120,14 +119,14 @@ class LDAPSettingForm(BaseForm):
help_text=_( help_text=_(
"User attr map present how to map LDAP user attr to jumpserver, " "User attr map present how to map LDAP user attr to jumpserver, "
"username,name,email is jumpserver attr" "username,name,email is jumpserver attr"
) ),
) )
# AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU # AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU
# AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER # AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER
AUTH_LDAP_START_TLS = forms.BooleanField( AUTH_LDAP_START_TLS = forms.BooleanField(
label=_("Use SSL"), initial=False, required=False label=_("Use SSL"), required=False
) )
AUTH_LDAP = forms.BooleanField(label=_("Enable LDAP auth"), initial=False, required=False) AUTH_LDAP = forms.BooleanField(label=_("Enable LDAP auth"), required=False)
class TerminalSettingForm(BaseForm): class TerminalSettingForm(BaseForm):
@ -173,10 +172,11 @@ class SecuritySettingForm(BaseForm):
initial=30, min_value=5, initial=30, min_value=5,
label=_("No logon interval"), label=_("No logon interval"),
help_text=_( help_text=_(
"Tip :(unit/minute) if the user has failed to log in for a limited " "Tip: (unit/minute) if the user has failed to log in for a limited "
"number of times, no login is allowed during this time interval." "number of times, no login is allowed during this time interval."
) )
) )
# ssh max idle time
SECURITY_MAX_IDLE_TIME = forms.IntegerField( SECURITY_MAX_IDLE_TIME = forms.IntegerField(
initial=30, required=False, initial=30, required=False,
label=_("Connection max idle time"), label=_("Connection max idle time"),
@ -185,6 +185,18 @@ class SecuritySettingForm(BaseForm):
'Unit: minute' 'Unit: minute'
), ),
) )
# password expiration time
SECURITY_PASSWORD_EXPIRATION_TIME = forms.IntegerField(
initial=9999, label=_("Password expiration time"),
min_value=1,
help_text=_(
"Tip: (unit: day) "
"If the user does not update the password during the time, "
"the user password will expire failure;"
"The password expiration reminder mail will be automatic sent to the user "
"by system within 5 days (daily) before the password expires"
)
)
# min length # min length
SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField( SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField(
initial=6, label=_("Password minimum length"), initial=6, label=_("Password minimum length"),

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-11 05:35
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.manager
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Settings',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
('value', models.TextField(verbose_name='Value')),
('enabled', models.BooleanField(default=True, verbose_name='Enabled')),
('comment', models.TextField(verbose_name='Comment')),
],
managers=[
('configs', django.db.models.manager.Manager()),
],
),
]

View File

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-11 06:07
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('common', '0001_initial'),
]
operations = [
migrations.RenameModel(
old_name='Settings',
new_name='Setting',
),
migrations.AlterModelManagers(
name='setting',
managers=[
],
),
migrations.AlterModelTable(
name='setting',
table='settings',
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-22 03:54
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0002_auto_20180111_1407'),
]
operations = [
migrations.AddField(
model_name='setting',
name='category',
field=models.CharField(default='default', max_length=128),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.1 on 2018-09-03 03:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0003_setting_category'),
]
operations = [
migrations.AddField(
model_name='setting',
name='encrypted',
field=models.BooleanField(default=False),
),
]

View File

@ -1,11 +1,10 @@
import json import json
import ldap
from django.db import models from django.db import models
from django.core.cache import cache
from django.db.utils import ProgrammingError, OperationalError from django.db.utils import ProgrammingError, OperationalError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.conf import settings from django.conf import settings
from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion
from .utils import get_signer from .utils import get_signer
@ -40,11 +39,7 @@ class Setting(models.Model):
return self.name return self.name
def __getattr__(self, item): def __getattr__(self, item):
instances = self.__class__.objects.filter(name=item) return cache.get(item)
if len(instances) == 1:
return instances[0].cleaned_value
else:
return None
@property @property
def cleaned_value(self): def cleaned_value(self):
@ -69,6 +64,11 @@ class Setting(models.Model):
@classmethod @classmethod
def save_storage(cls, name, data): def save_storage(cls, name, data):
"""
:param name: TERMINAL_REPLAY_STORAGE or TERMINAL_COMMAND_STORAGE
:param data: {}
:return: Setting object
"""
obj = cls.objects.filter(name=name).first() obj = cls.objects.filter(name=name).first()
if not obj: if not obj:
obj = cls() obj = cls()
@ -84,7 +84,14 @@ class Setting(models.Model):
@classmethod @classmethod
def delete_storage(cls, name, storage_name): def delete_storage(cls, name, storage_name):
obj = cls.objects.get(name=name) """
:param name: TERMINAL_REPLAY_STORAGE or TERMINAL_COMMAND_STORAGE
:param storage_name: ""
:return: bool
"""
obj = cls.objects.filter(name=name).first()
if not obj:
return False
value = obj.cleaned_value value = obj.cleaned_value
value.pop(storage_name, '') value.pop(storage_name, '')
obj.cleaned_value = value obj.cleaned_value = value
@ -102,22 +109,15 @@ class Setting(models.Model):
def refresh_setting(self): def refresh_setting(self):
setattr(settings, self.name, self.cleaned_value) setattr(settings, self.name, self.cleaned_value)
if self.name == "AUTH_LDAP": if self.name == "AUTH_LDAP":
if self.cleaned_value and settings.AUTH_LDAP_BACKEND not in settings.AUTHENTICATION_BACKENDS: if self.cleaned_value and settings.AUTH_LDAP_BACKEND not in settings.AUTHENTICATION_BACKENDS:
settings.AUTHENTICATION_BACKENDS.insert(0, settings.AUTH_LDAP_BACKEND) old_setting = settings.AUTHENTICATION_BACKENDS
old_setting.insert(0, settings.AUTH_LDAP_BACKEND)
settings.AUTHENTICATION_BACKENDS = old_setting
elif not self.cleaned_value and settings.AUTH_LDAP_BACKEND in settings.AUTHENTICATION_BACKENDS: elif not self.cleaned_value and settings.AUTH_LDAP_BACKEND in settings.AUTHENTICATION_BACKENDS:
settings.AUTHENTICATION_BACKENDS.remove(settings.AUTH_LDAP_BACKEND) old_setting = settings.AUTHENTICATION_BACKENDS
old_setting.remove(settings.AUTH_LDAP_BACKEND)
if self.name == "AUTH_LDAP_SEARCH_FILTER": settings.AUTHENTICATION_BACKENDS = old_setting
settings.AUTH_LDAP_USER_SEARCH_UNION = [
LDAPSearch(USER_SEARCH, ldap.SCOPE_SUBTREE, settings.AUTH_LDAP_SEARCH_FILTER)
for USER_SEARCH in str(settings.AUTH_LDAP_SEARCH_OU).split("|")
]
settings.AUTH_LDAP_USER_SEARCH = LDAPSearchUnion(*settings.AUTH_LDAP_USER_SEARCH_UNION)
class Meta: class Meta:
db_table = "settings" db_table = "settings"
common_settings = Setting()

View File

@ -5,6 +5,7 @@ from rest_framework import permissions
from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.mixins import UserPassesTestMixin
from django.shortcuts import redirect from django.shortcuts import redirect
from django.http.response import HttpResponseForbidden from django.http.response import HttpResponseForbidden
from django.conf import settings
from orgs.utils import current_org from orgs.utils import current_org
@ -96,3 +97,12 @@ class SuperUserRequiredMixin(UserPassesTestMixin):
def test_func(self): def test_func(self):
if self.request.user.is_authenticated and self.request.user.is_superuser: if self.request.user.is_authenticated and self.request.user.is_superuser:
return True return True
class WithBootstrapToken(permissions.BasePermission):
def has_permission(self, request, view):
authorization = request.META.get('HTTP_AUTHORIZATION', '')
if not authorization:
return False
request_bootstrap_token = authorization.split()[-1]
return settings.BOOTSTRAP_TOKEN == request_bootstrap_token

View File

@ -4,4 +4,3 @@
from django.dispatch import Signal from django.dispatch import Signal
django_ready = Signal() django_ready = Signal()
ldap_auth_enable = Signal(providing_args=["enabled"])

View File

@ -2,13 +2,14 @@
# #
from django.dispatch import receiver from django.dispatch import receiver
from django.db.models.signals import post_save, pre_save from django.db.models.signals import post_save, pre_save
from django.conf import settings from django.conf import LazySettings, empty
from django.db.utils import ProgrammingError, OperationalError from django.db.utils import ProgrammingError, OperationalError
from django.core.cache import cache
from jumpserver.utils import current_request from jumpserver.utils import current_request
from .models import Setting from .models import Setting
from .utils import get_logger from .utils import get_logger
from .signals import django_ready, ldap_auth_enable from .signals import django_ready
logger = get_logger(__file__) logger = get_logger(__file__)
@ -25,27 +26,47 @@ def refresh_settings_on_changed(sender, instance=None, **kwargs):
def refresh_all_settings_on_django_ready(sender, **kwargs): def refresh_all_settings_on_django_ready(sender, **kwargs):
logger.debug("Receive django ready signal") logger.debug("Receive django ready signal")
logger.debug(" - fresh all settings") logger.debug(" - fresh all settings")
CACHE_KEY_PREFIX = '_SETTING_'
def monkey_patch_getattr(self, name):
key = CACHE_KEY_PREFIX + name
cached = cache.get(key)
if cached is not None:
return cached
if self._wrapped is empty:
self._setup(name)
val = getattr(self._wrapped, name)
# self.__dict__[name] = val # Never set it
return val
def monkey_patch_setattr(self, name, value):
key = CACHE_KEY_PREFIX + name
cache.set(key, value, None)
if name == '_wrapped':
self.__dict__.clear()
else:
self.__dict__.pop(name, None)
super(LazySettings, self).__setattr__(name, value)
def monkey_patch_delattr(self, name):
super(LazySettings, self).__delattr__(name)
self.__dict__.pop(name, None)
key = CACHE_KEY_PREFIX + name
cache.delete(key)
try: try:
LazySettings.__getattr__ = monkey_patch_getattr
LazySettings.__setattr__ = monkey_patch_setattr
LazySettings.__delattr__ = monkey_patch_delattr
Setting.refresh_all_settings() Setting.refresh_all_settings()
except (ProgrammingError, OperationalError): except (ProgrammingError, OperationalError):
pass pass
@receiver(ldap_auth_enable, dispatch_uid="my_unique_identifier")
def ldap_auth_on_changed(sender, enabled=True, **kwargs):
if enabled:
logger.debug("Enable LDAP auth")
if settings.AUTH_LDAP_BACKEND not in settings.AUTHENTICATION_BACKENDS:
settings.AUTHENTICATION_BACKENDS.insert(0, settings.AUTH_LDAP_BACKEND)
else:
logger.debug("Disable LDAP auth")
if settings.AUTH_LDAP_BACKEND in settings.AUTHENTICATION_BACKENDS:
settings.AUTHENTICATION_BACKENDS.remove(settings.AUTH_LDAP_BACKEND)
@receiver(pre_save, dispatch_uid="my_unique_identifier") @receiver(pre_save, dispatch_uid="my_unique_identifier")
def on_create_set_created_by(sender, instance=None, **kwargs): def on_create_set_created_by(sender, instance=None, **kwargs):
if getattr(instance, '_ignore_auto_created_by', False) is True:
return
if hasattr(instance, 'created_by') and not instance.created_by: if hasattr(instance, 'created_by') and not instance.created_by:
if current_request and current_request.user.is_authenticated: if current_request and current_request.user.is_authenticated:
instance.created_by = current_request.user.name instance.created_by = current_request.user.name

View File

@ -3,7 +3,6 @@ from django.conf import settings
from celery import shared_task from celery import shared_task
from .utils import get_logger from .utils import get_logger
from .models import Setting from .models import Setting
from common.models import common_settings
logger = get_logger(__file__) logger = get_logger(__file__)
@ -23,13 +22,9 @@ def send_mail_async(*args, **kwargs):
Example: Example:
send_mail_sync.delay(subject, message, recipient_list, fail_silently=False, html_message=None) send_mail_sync.delay(subject, message, recipient_list, fail_silently=False, html_message=None)
""" """
configs = Setting.objects.filter(name__startswith='EMAIL')
for config in configs:
setattr(settings, config.name, config.cleaned_value)
if len(args) == 3: if len(args) == 3:
args = list(args) args = list(args)
args[0] = common_settings.EMAIL_SUBJECT_PREFIX + args[0] args[0] = settings.EMAIL_SUBJECT_PREFIX + args[0]
args.insert(2, settings.EMAIL_HOST_USER) args.insert(2, settings.EMAIL_HOST_USER)
args = tuple(args) args = tuple(args)

View File

@ -39,9 +39,9 @@
{% endif %} {% endif %}
{% csrf_token %} {% csrf_token %}
<h3>{% trans "User login settings" %}</h3> <h3>{% trans "Security setting" %}</h3>
{% for field in form %} {% for field in form %}
{% if forloop.counter == 5 %} {% if forloop.counter == 6 %}
<div class="hr-line-dashed"></div> <div class="hr-line-dashed"></div>
<h3>{% trans "Password check rule" %}</h3> <h3>{% trans "Password check rule" %}</h3>
{% endif %} {% endif %}

View File

@ -87,12 +87,12 @@
<tr> <tr>
<td>{{ name }}</td> <td>{{ name }}</td>
<td>{{ setting.TYPE }}</td> <td>{{ setting.TYPE }}</td>
<td><a class="btn btn-xs btn-danger m-l-xs btn-del-command" data-name="{{ name }}">{% trans 'Delete' %}</a></td> <td><a class="btn btn-xs btn-danger m-l-xs btn-del-command" {% if setting.TYPE == 'server' and name == 'default' %} disabled {% endif %} data-name="{{ name }}">{% trans 'Delete' %}</a></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<a href="{% url 'common:command-storage-create' %}" class="btn btn-primary">{% trans 'Add' %}</a> <a href="{% url 'common:command-storage-create' %}" class="btn btn-primary btn-xs">{% trans 'Add' %}</a>
<div class="hr-line-dashed"></div> <div class="hr-line-dashed"></div>
<h3>{% trans "Replay storage" %}</h3> <h3>{% trans "Replay storage" %}</h3>
@ -109,12 +109,12 @@
<tr> <tr>
<td>{{ name }}</td> <td>{{ name }}</td>
<td>{{ setting.TYPE }}</td> <td>{{ setting.TYPE }}</td>
<td><a class="btn btn-xs btn-danger m-l-xs btn-del-replay" data-name="{{ name }}">{% trans 'Delete' %}</a></td> <td><a class="btn btn-xs btn-danger m-l-xs btn-del-replay" {% if setting.TYPE == 'server' and name == 'default' %} disabled {% endif %} data-name="{{ name }}">{% trans 'Delete' %}</a></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<a href="{% url 'common:replay-storage-create' %}" class="btn btn-primary">{% trans 'Add' %}</a> <a href="{% url 'common:replay-storage-create' %}" class="btn btn-primary btn-xs">{% trans 'Add' %}</a>
<div class="hr-line-dashed"></div> <div class="hr-line-dashed"></div>
</form> </form>
@ -151,7 +151,7 @@ function deleteStorage($this, the_url){
toastr.success("{% trans 'Delete succeed' %}"); toastr.success("{% trans 'Delete succeed' %}");
}; };
var error = function(){ var error = function(){
toastr.error("{% trans 'Delete failed' %}}"); toastr.error("{% trans 'Delete failed' %}");
}; };
ajaxAPI(the_url, JSON.stringify(data), success, error, method); ajaxAPI(the_url, JSON.stringify(data), success, error, method);
} }

View File

@ -111,3 +111,13 @@ def sort(data):
@register.filter @register.filter
def subtract(value, arg): def subtract(value, arg):
return value - arg return value - arg
@register.filter
def state_show(state):
success = '<i class ="fa fa-check text-navy"> </i>'
failed = '<i class ="fa fa-times text-danger"> </i>'
if state:
return success
else:
return failed

95
apps/common/tree.py Normal file
View File

@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
class TreeNode:
id = ""
name = ""
comment = ""
title = ""
isParent = False
pId = ""
open = False
iconSkin = ""
meta = {}
_tree = None
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
@classmethod
def root(cls):
return cls(id="#", name='Root', title='Root', isParent=True, open=True)
def get_parent(self):
return self._tree.get_node(self.pId)
def get_parents(self):
parent = self.get_parent()
if parent == self._tree.root:
return []
parents = [parent]
parents.extend(parent.get_parents())
return parents
def add_child(self, child):
self._tree.add_node(child, self)
def __str__(self):
return '<{}: {}>'.format(self.id, self.name)
__repr__ = __str__
def __gt__(self, other):
if self.isParent and not other.isParent:
return False
return self.id > other.id
def __eq__(self, other):
return self.id == other.id
def __lt__(self, other):
if self.isParent and not other.isParent:
return True
return self.id < other.id
class Tree:
def __init__(self):
self.nodes = {}
self.root = TreeNode.root()
self.root._tree = self
def add_node(self, node, parent=None):
node._tree = self
if not parent:
parent = self.root
if parent.id not in self.nodes and parent != self.root:
raise ValueError("Parent not in tree")
elif node in parent.get_parents():
raise ValueError("Parent must not be node parent")
node.pId = parent.id
parent.isParent = True
self.nodes[node.id] = node
def get_nodes(self):
return sorted(self.nodes.values())
def get_node(self, tid):
return self.nodes.get(tid) or TreeNode.root()
class TreeNodeSerializer(serializers.Serializer):
id = serializers.CharField(max_length=128)
name = serializers.CharField(max_length=128)
title = serializers.CharField(max_length=128)
pId = serializers.CharField(max_length=128)
isParent = serializers.BooleanField(default=False)
open = serializers.BooleanField(default=False)
iconSkin = serializers.CharField(max_length=128, allow_blank=True)
meta = serializers.JSONField()

View File

@ -13,5 +13,5 @@ urlpatterns = [
path('terminal/replay-storage/delete/', api.ReplayStorageDeleteAPI.as_view(), name='replay-storage-delete'), path('terminal/replay-storage/delete/', api.ReplayStorageDeleteAPI.as_view(), name='replay-storage-delete'),
path('terminal/command-storage/create/', api.CommandStorageCreateAPI.as_view(), name='command-storage-create'), path('terminal/command-storage/create/', api.CommandStorageCreateAPI.as_view(), name='command-storage-create'),
path('terminal/command-storage/delete/', api.CommandStorageDeleteAPI.as_view(), name='command-storage-delete'), path('terminal/command-storage/delete/', api.CommandStorageDeleteAPI.as_view(), name='command-storage-delete'),
# path('django-settings/', api.DjangoSettingsAPI.as_view(), name='django-settings'), path('django-settings/', api.DjangoSettingsAPI.as_view(), name='django-settings'),
] ]

View File

@ -37,8 +37,7 @@ def reverse(view_name, urlconf=None, args=None, kwargs=None,
kwargs=kwargs, current_app=current_app) kwargs=kwargs, current_app=current_app)
if external: if external:
from common.models import common_settings site_url = settings.SITE_URL
site_url = common_settings.SITE_URL or settings.SITE_URL
url = site_url.strip('/') + url url = site_url.strip('/') + url
return url return url
@ -389,53 +388,18 @@ def get_request_ip(request):
return login_ip return login_ip
def get_command_storage_or_create_default_storage(): def get_command_storage_setting():
from common.models import common_settings, Setting default = settings.DEFAULT_TERMINAL_COMMAND_STORAGE
name = 'TERMINAL_COMMAND_STORAGE' value = settings.TERMINAL_COMMAND_STORAGE
default = {'default': {'TYPE': 'server'}} value.update(default)
try: return value
command_storage = common_settings.TERMINAL_COMMAND_STORAGE
except Exception:
return default
if command_storage is None:
obj = Setting()
obj.name = name
obj.encrypted = True
obj.cleaned_value = default
obj.save()
if isinstance(command_storage, dict) and not command_storage:
obj = Setting.objects.get(name=name)
value = obj.cleaned_value
value.update(default)
obj.cleaned_value = value
obj.save()
command_storage = common_settings.TERMINAL_COMMAND_STORAGE
return command_storage
def get_replay_storage_or_create_default_storage(): def get_replay_storage_setting():
from common.models import common_settings, Setting default = settings.DEFAULT_TERMINAL_REPLAY_STORAGE
name = 'TERMINAL_REPLAY_STORAGE' value = settings.TERMINAL_REPLAY_STORAGE
default = {'default': {'TYPE': 'server'}} value.update(default)
try: return value
replay_storage = common_settings.TERMINAL_REPLAY_STORAGE
except Exception:
return default
if replay_storage is None:
obj = Setting()
obj.name = name
obj.encrypted = True
obj.cleaned_value = default
obj.save()
replay_storage = common_settings.TERMINAL_REPLAY_STORAGE
if isinstance(replay_storage, dict) and not replay_storage:
obj = Setting.objects.get(name=name)
value = obj.cleaned_value
value.update(default)
obj.cleaned_value = value
obj.save()
replay_storage = common_settings.TERMINAL_REPLAY_STORAGE
return replay_storage
class TeeObj: class TeeObj:

View File

@ -2,13 +2,10 @@ from django.views.generic import TemplateView
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.contrib import messages from django.contrib import messages
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.conf import settings
from common.models import common_settings
from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \ from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \
TerminalSettingForm, SecuritySettingForm TerminalSettingForm, SecuritySettingForm
from common.permissions import SuperUserRequiredMixin from common.permissions import SuperUserRequiredMixin
from .signals import ldap_auth_enable
from . import utils from . import utils
@ -29,7 +26,7 @@ class BasicSettingView(SuperUserRequiredMixin, TemplateView):
form = self.form_class(request.POST) form = self.form_class(request.POST)
if form.is_valid(): if form.is_valid():
form.save() form.save()
msg = _("Update setting successfully, please restart program") msg = _("Update setting successfully")
messages.success(request, msg) messages.success(request, msg)
return redirect('settings:basic-setting') return redirect('settings:basic-setting')
else: else:
@ -55,7 +52,7 @@ class EmailSettingView(SuperUserRequiredMixin, TemplateView):
form = self.form_class(request.POST) form = self.form_class(request.POST)
if form.is_valid(): if form.is_valid():
form.save() form.save()
msg = _("Update setting successfully, please restart program") msg = _("Update setting successfully")
messages.success(request, msg) messages.success(request, msg)
return redirect('settings:email-setting') return redirect('settings:email-setting')
else: else:
@ -81,9 +78,7 @@ class LDAPSettingView(SuperUserRequiredMixin, TemplateView):
form = self.form_class(request.POST) form = self.form_class(request.POST)
if form.is_valid(): if form.is_valid():
form.save() form.save()
if "AUTH_LDAP" in form.cleaned_data: msg = _("Update setting successfully")
ldap_auth_enable.send(sender=self.__class__, enabled=form.cleaned_data["AUTH_LDAP"])
msg = _("Update setting successfully, please restart program")
messages.success(request, msg) messages.success(request, msg)
return redirect('settings:ldap-setting') return redirect('settings:ldap-setting')
else: else:
@ -97,8 +92,8 @@ class TerminalSettingView(SuperUserRequiredMixin, TemplateView):
template_name = "common/terminal_setting.html" template_name = "common/terminal_setting.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
command_storage = utils.get_command_storage_or_create_default_storage() command_storage = utils.get_command_storage_setting()
replay_storage = utils.get_replay_storage_or_create_default_storage() replay_storage = utils.get_replay_storage_setting()
context = { context = {
'app': _('Settings'), 'app': _('Settings'),
@ -114,7 +109,7 @@ class TerminalSettingView(SuperUserRequiredMixin, TemplateView):
form = self.form_class(request.POST) form = self.form_class(request.POST)
if form.is_valid(): if form.is_valid():
form.save() form.save()
msg = _("Update setting successfully, please restart program") msg = _("Update setting successfully")
messages.success(request, msg) messages.success(request, msg)
return redirect('settings:terminal-setting') return redirect('settings:terminal-setting')
else: else:
@ -164,7 +159,7 @@ class SecuritySettingView(SuperUserRequiredMixin, TemplateView):
form = self.form_class(request.POST) form = self.form_class(request.POST)
if form.is_valid(): if form.is_valid():
form.save() form.save()
msg = _("Update setting successfully, please restart program") msg = _("Update setting successfully")
messages.success(request, msg) messages.success(request, msg)
return redirect('settings:security-setting') return redirect('settings:security-setting')
else: else:

333
apps/jumpserver/conf.py Normal file
View File

@ -0,0 +1,333 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
import os
import sys
import types
import errno
import json
import yaml
from importlib import import_module
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_DIR = os.path.dirname(BASE_DIR)
def import_string(dotted_path):
try:
module_path, class_name = dotted_path.rsplit('.', 1)
except ValueError as err:
raise ImportError("%s doesn't look like a module path" % dotted_path) from err
module = import_module(module_path)
try:
return getattr(module, class_name)
except AttributeError as err:
raise ImportError('Module "%s" does not define a "%s" attribute/class' % (
module_path, class_name)
) from err
class Config(dict):
"""Works exactly like a dict but provides ways to fill it from files
or special dictionaries. There are two common patterns to populate the
config.
Either you can fill the config from a config file::
app.config.from_pyfile('yourconfig.cfg')
Or alternatively you can define the configuration options in the
module that calls :meth:`from_object` or provide an import path to
a module that should be loaded. It is also possible to tell it to
use the same module and with that provide the configuration values
just before the call::
DEBUG = True
SECRET_KEY = 'development key'
app.config.from_object(__name__)
In both cases (loading from any Python file or loading from modules),
only uppercase keys are added to the config. This makes it possible to use
lowercase values in the config file for temporary values that are not added
to the config or to define the config keys in the same file that implements
the application.
Probably the most interesting way to load configurations is from an
environment variable pointing to a file::
app.config.from_envvar('YOURAPPLICATION_SETTINGS')
In this case before launching the application you have to set this
environment variable to the file you want to use. On Linux and OS X
use the export statement::
export YOURAPPLICATION_SETTINGS='/path/to/config/file'
On windows use `set` instead.
:param root_path: path to which files are read relative from. When the
config object is created by the application, this is
the application's :attr:`~flask.Flask.root_path`.
:param defaults: an optional dictionary of default values
"""
def __init__(self, root_path=None, defaults=None):
self.defaults = defaults or {}
self.root_path = root_path
super().__init__({})
def from_envvar(self, variable_name, silent=False):
"""Loads a configuration from an environment variable pointing to
a configuration file. This is basically just a shortcut with nicer
error messages for this line of code::
app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS'])
:param variable_name: name of the environment variable
:param silent: set to ``True`` if you want silent failure for missing
files.
:return: bool. ``True`` if able to load config, ``False`` otherwise.
"""
rv = os.environ.get(variable_name)
if not rv:
if silent:
return False
raise RuntimeError('The environment variable %r is not set '
'and as such configuration could not be '
'loaded. Set this variable and make it '
'point to a configuration file' %
variable_name)
return self.from_pyfile(rv, silent=silent)
def from_pyfile(self, filename, silent=False):
"""Updates the values in the config from a Python file. This function
behaves as if the file was imported as module with the
:meth:`from_object` function.
:param filename: the filename of the config. This can either be an
absolute filename or a filename relative to the
root path.
:param silent: set to ``True`` if you want silent failure for missing
files.
.. versionadded:: 0.7
`silent` parameter.
"""
if self.root_path:
filename = os.path.join(self.root_path, filename)
d = types.ModuleType('config')
d.__file__ = filename
try:
with open(filename, mode='rb') as config_file:
exec(compile(config_file.read(), filename, 'exec'), d.__dict__)
except IOError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
return False
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
raise
self.from_object(d)
return True
def from_object(self, obj):
"""Updates the values from the given object. An object can be of one
of the following two types:
- a string: in this case the object with that name will be imported
- an actual object reference: that object is used directly
Objects are usually either modules or classes. :meth:`from_object`
loads only the uppercase attributes of the module/class. A ``dict``
object will not work with :meth:`from_object` because the keys of a
``dict`` are not attributes of the ``dict`` class.
Example of module-based configuration::
app.config.from_object('yourapplication.default_config')
from yourapplication import default_config
app.config.from_object(default_config)
You should not use this function to load the actual configuration but
rather configuration defaults. The actual config should be loaded
with :meth:`from_pyfile` and ideally from a location not within the
package because the package might be installed system wide.
See :ref:`config-dev-prod` for an example of class-based configuration
using :meth:`from_object`.
:param obj: an import name or object
"""
if isinstance(obj, str):
obj = import_string(obj)
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key)
def from_json(self, filename, silent=False):
"""Updates the values in the config from a JSON file. This function
behaves as if the JSON object was a dictionary and passed to the
:meth:`from_mapping` function.
:param filename: the filename of the JSON file. This can either be an
absolute filename or a filename relative to the
root path.
:param silent: set to ``True`` if you want silent failure for missing
files.
.. versionadded:: 0.11
"""
if self.root_path:
filename = os.path.join(self.root_path, filename)
try:
with open(filename) as json_file:
obj = json.loads(json_file.read())
except IOError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
return False
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
raise
return self.from_mapping(obj)
def from_yaml(self, filename, silent=False):
if self.root_path:
filename = os.path.join(self.root_path, filename)
try:
with open(filename) as json_file:
obj = yaml.load(json_file)
except IOError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
return False
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
raise
return self.from_mapping(obj)
def from_mapping(self, *mapping, **kwargs):
"""Updates the config like :meth:`update` ignoring items with non-upper
keys.
.. versionadded:: 0.11
"""
mappings = []
if len(mapping) == 1:
if hasattr(mapping[0], 'items'):
mappings.append(mapping[0].items())
else:
mappings.append(mapping[0])
elif len(mapping) > 1:
raise TypeError(
'expected at most 1 positional argument, got %d' % len(mapping)
)
mappings.append(kwargs.items())
for mapping in mappings:
for (key, value) in mapping:
if key.isupper():
self[key] = value
return True
def get_namespace(self, namespace, lowercase=True, trim_namespace=True):
"""Returns a dictionary containing a subset of configuration options
that match the specified namespace/prefix. Example usage::
app.config['IMAGE_STORE_TYPE'] = 'fs'
app.config['IMAGE_STORE_PATH'] = '/var/app/images'
app.config['IMAGE_STORE_BASE_URL'] = 'http://img.website.com'
image_store_config = app.config.get_namespace('IMAGE_STORE_')
The resulting dictionary `image_store_config` would look like::
{
'types': 'fs',
'path': '/var/app/images',
'base_url': 'http://img.website.com'
}
This is often useful when configuration options map directly to
keyword arguments in functions or class constructors.
:param namespace: a configuration namespace
:param lowercase: a flag indicating if the keys of the resulting
dictionary should be lowercase
:param trim_namespace: a flag indicating if the keys of the resulting
dictionary should not include the namespace
.. versionadded:: 0.11
"""
rv = {}
for k, v in self.items():
if not k.startswith(namespace):
continue
if trim_namespace:
key = k[len(namespace):]
else:
key = k
if lowercase:
key = key.lower()
rv[key] = v
return rv
def __repr__(self):
return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self))
def __getitem__(self, item):
try:
value = super().__getitem__(item)
except KeyError:
value = None
if value is not None:
return value
value = os.environ.get(item, None)
if value is not None:
return value
return self.defaults.get(item)
def __getattr__(self, item):
return self.__getitem__(item)
defaults = {
'SECRET_KEY': '2vym+ky!997d5kkcc64mnz06y1mmui3lut#(^wd=%s_qj$1%x',
'BOOTSTRAP_TOKEN': 'PleaseChangeMe',
'DEBUG': True,
'SITE_URL': 'http://localhost',
'LOG_LEVEL': 'DEBUG',
'LOG_DIR': os.path.join(PROJECT_DIR, 'logs'),
'DB_ENGINE': 'mysql',
'DB_NAME': 'jumpserver',
'DB_HOST': '127.0.0.1',
'DB_PORT': 3306,
'DB_USER': 'root',
'DB_PASSWORD': '',
'REDIS_HOST': '127.0.0.1',
'REDIS_PORT': 6379,
'REDIS_PASSWORD': '',
'REDIS_DB_CELERY': 3,
'REDIS_DB_CACHE': 4,
'CAPTCHA_TEST_MODE': None,
'TOKEN_EXPIRATION': 3600,
'DISPLAY_PER_PAGE': 25,
'DEFAULT_EXPIRED_YEARS': 70,
'SESSION_COOKIE_DOMAIN': None,
'CSRF_COOKIE_DOMAIN': None,
'SESSION_COOKIE_AGE': 3600 * 24,
'SESSION_EXPIRE_AT_BROWSER_CLOSE': False,
'AUTH_OPENID': False,
'EMAIL_SUFFIX': 'jumpserver.org'
}
def load_user_config():
sys.path.insert(0, PROJECT_DIR)
config = Config(PROJECT_DIR, defaults)
try:
from config import config as c
config.from_object(c)
except ImportError:
msg = """
Error: No config file found.
You can run `cp config_example.py config.py`, and edit it.
"""
raise ImportError(msg)
return config

View File

@ -14,27 +14,15 @@ import os
import sys import sys
import ldap import ldap
from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion # from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion
from django.urls import reverse_lazy from django.urls import reverse_lazy
from .conf import load_user_config
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_DIR = os.path.dirname(BASE_DIR) PROJECT_DIR = os.path.dirname(BASE_DIR)
CONFIG = load_user_config()
sys.path.append(PROJECT_DIR)
# Import project config setting
try:
from config import config as CONFIG
except ImportError:
msg = """
Error: No config file found.
You can run `cp config_example.py config.py`, and edit it.
"""
raise ImportError(msg)
# CONFIG = type('_', (), {'__getattr__': lambda arg1, arg2: None})()
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
@ -42,16 +30,19 @@ except ImportError:
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = CONFIG.SECRET_KEY SECRET_KEY = CONFIG.SECRET_KEY
# SECURITY WARNING: keep the token secret, remove it if all coco, guacamole ok
BOOTSTRAP_TOKEN = CONFIG.BOOTSTRAP_TOKEN
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = CONFIG.DEBUG or False DEBUG = CONFIG.DEBUG
# Absolute url for some case, for example email link # Absolute url for some case, for example email link
SITE_URL = CONFIG.SITE_URL or 'http://localhost' SITE_URL = CONFIG.SITE_URL
# LOG LEVEL # LOG LEVEL
LOG_LEVEL = 'DEBUG' if DEBUG else CONFIG.LOG_LEVEL or 'WARNING' LOG_LEVEL = CONFIG.LOG_LEVEL
ALLOWED_HOSTS = CONFIG.ALLOWED_HOSTS or [] ALLOWED_HOSTS = ['*']
# Application definition # Application definition
@ -152,9 +143,10 @@ TEMPLATES = [
LOGIN_REDIRECT_URL = reverse_lazy('index') LOGIN_REDIRECT_URL = reverse_lazy('index')
LOGIN_URL = reverse_lazy('users:login') LOGIN_URL = reverse_lazy('users:login')
SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN or None SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN
CSRF_COOKIE_DOMAIN = CONFIG.CSRF_COOKIE_DOMAIN or None CSRF_COOKIE_DOMAIN = CONFIG.CSRF_COOKIE_DOMAIN
SESSION_COOKIE_AGE = CONFIG.SESSION_COOKIE_AGE or 3600 * 24 SESSION_COOKIE_AGE = CONFIG.SESSION_COOKIE_AGE
SESSION_EXPIRE_AT_BROWSER_CLOSE = CONFIG.SESSION_EXPIRE_AT_BROWSER_CLOSE
MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'
# Database # Database
@ -317,13 +309,13 @@ MEDIA_ROOT = os.path.join(PROJECT_DIR, 'data', 'media').replace('\\', '/') + '/'
FIXTURE_DIRS = [os.path.join(BASE_DIR, 'fixtures'), ] FIXTURE_DIRS = [os.path.join(BASE_DIR, 'fixtures'), ]
# Email config # Email config
EMAIL_HOST = CONFIG.EMAIL_HOST EMAIL_HOST = 'smtp.jumpserver.org'
EMAIL_PORT = CONFIG.EMAIL_PORT EMAIL_PORT = 25
EMAIL_HOST_USER = CONFIG.EMAIL_HOST_USER EMAIL_HOST_USER = 'noreply@jumpserver.org'
EMAIL_HOST_PASSWORD = CONFIG.EMAIL_HOST_PASSWORD EMAIL_HOST_PASSWORD = ''
EMAIL_USE_SSL = CONFIG.EMAIL_USE_SSL EMAIL_USE_SSL = False
EMAIL_USE_TLS = CONFIG.EMAIL_USE_TLS EMAIL_USE_TLS = False
EMAIL_SUBJECT_PREFIX = CONFIG.EMAIL_SUBJECT_PREFIX or '' EMAIL_SUBJECT_PREFIX = '[JMS] '
REST_FRAMEWORK = { REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions, # Use Django's standard `django.contrib.auth` permissions,
@ -362,31 +354,29 @@ AUTH_USER_MODEL = 'users.User'
FILE_UPLOAD_PERMISSIONS = 0o644 FILE_UPLOAD_PERMISSIONS = 0o644
FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o755 FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o755
# OTP settings
OTP_ISSUER_NAME = CONFIG.OTP_ISSUER_NAME
# Auth LDAP settings # Auth LDAP settings
AUTH_LDAP = CONFIG.AUTH_LDAP AUTH_LDAP = False
AUTH_LDAP_SERVER_URI = CONFIG.AUTH_LDAP_SERVER_URI AUTH_LDAP_SERVER_URI = 'ldap://localhost:389'
AUTH_LDAP_BIND_DN = CONFIG.AUTH_LDAP_BIND_DN AUTH_LDAP_BIND_DN = 'cn=admin,dc=jumpserver,dc=org'
AUTH_LDAP_BIND_PASSWORD = CONFIG.AUTH_LDAP_BIND_PASSWORD AUTH_LDAP_BIND_PASSWORD = ''
AUTH_LDAP_SEARCH_OU = CONFIG.AUTH_LDAP_SEARCH_OU AUTH_LDAP_SEARCH_OU = 'ou=tech,dc=jumpserver,dc=org'
AUTH_LDAP_SEARCH_FILTER = CONFIG.AUTH_LDAP_SEARCH_FILTER AUTH_LDAP_SEARCH_FILTER = '(cn=%(user)s)'
AUTH_LDAP_START_TLS = CONFIG.AUTH_LDAP_START_TLS AUTH_LDAP_START_TLS = False
AUTH_LDAP_USER_ATTR_MAP = CONFIG.AUTH_LDAP_USER_ATTR_MAP AUTH_LDAP_USER_ATTR_MAP = {"username": "cn", "name": "sn", "email": "mail"}
AUTH_LDAP_USER_SEARCH_UNION = [ # AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU
LDAPSearch(USER_SEARCH, ldap.SCOPE_SUBTREE, AUTH_LDAP_SEARCH_FILTER) # AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER
for USER_SEARCH in str(AUTH_LDAP_SEARCH_OU).split("|") # AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
] # AUTH_LDAP_GROUP_SEARCH_OU, ldap.SCOPE_SUBTREE, AUTH_LDAP_GROUP_SEARCH_FILTER
AUTH_LDAP_USER_SEARCH = LDAPSearchUnion(*AUTH_LDAP_USER_SEARCH_UNION) # )
AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU
AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
AUTH_LDAP_GROUP_SEARCH_OU, ldap.SCOPE_SUBTREE, AUTH_LDAP_GROUP_SEARCH_FILTER
)
AUTH_LDAP_CONNECTION_OPTIONS = { AUTH_LDAP_CONNECTION_OPTIONS = {
ldap.OPT_TIMEOUT: 5 ldap.OPT_TIMEOUT: 5
} }
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 1 AUTH_LDAP_GROUP_CACHE_TIMEOUT = 1
AUTH_LDAP_ALWAYS_UPDATE_USER = True AUTH_LDAP_ALWAYS_UPDATE_USER = True
AUTH_LDAP_BACKEND = 'django_auth_ldap.backend.LDAPBackend' AUTH_LDAP_BACKEND = 'authentication.ldap.backends.LDAPAuthorizationBackend'
if AUTH_LDAP: if AUTH_LDAP:
AUTHENTICATION_BACKENDS.insert(0, AUTH_LDAP_BACKEND) AUTHENTICATION_BACKENDS.insert(0, AUTH_LDAP_BACKEND)
@ -411,10 +401,10 @@ if AUTH_OPENID:
# Celery using redis as broker # Celery using redis as broker
CELERY_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % { CELERY_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % {
'password': CONFIG.REDIS_PASSWORD if CONFIG.REDIS_PASSWORD else '', 'password': CONFIG.REDIS_PASSWORD,
'host': CONFIG.REDIS_HOST or '127.0.0.1', 'host': CONFIG.REDIS_HOST,
'port': CONFIG.REDIS_PORT or 6379, 'port': CONFIG.REDIS_PORT,
'db':CONFIG.REDIS_DB_CELERY_BROKER or 3, 'db': CONFIG.REDIS_DB_CELERY,
} }
CELERY_TASK_SERIALIZER = 'pickle' CELERY_TASK_SERIALIZER = 'pickle'
CELERY_RESULT_SERIALIZER = 'pickle' CELERY_RESULT_SERIALIZER = 'pickle'
@ -436,10 +426,10 @@ CACHES = {
'default': { 'default': {
'BACKEND': 'redis_cache.RedisCache', 'BACKEND': 'redis_cache.RedisCache',
'LOCATION': 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % { 'LOCATION': 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % {
'password': CONFIG.REDIS_PASSWORD if CONFIG.REDIS_PASSWORD else '', 'password': CONFIG.REDIS_PASSWORD,
'host': CONFIG.REDIS_HOST or '127.0.0.1', 'host': CONFIG.REDIS_HOST,
'port': CONFIG.REDIS_PORT or 6379, 'port': CONFIG.REDIS_PORT,
'db': CONFIG.REDIS_DB_CACHE or 4, 'db': CONFIG.REDIS_DB_CACHE,
} }
} }
} }
@ -454,27 +444,45 @@ COMMAND_STORAGE = {
'ENGINE': 'terminal.backends.command.db', 'ENGINE': 'terminal.backends.command.db',
} }
TERMINAL_COMMAND_STORAGE = { DEFAULT_TERMINAL_COMMAND_STORAGE = {
"default": { "default": {
"TYPE": "server", "TYPE": "server",
}, },
}
TERMINAL_COMMAND_STORAGE = {
# 'ali-es': { # 'ali-es': {
# 'TYPE': 'elasticsearch', # 'TYPE': 'elasticsearch',
# 'HOSTS': ['http://elastic:changeme@localhost:9200'], # 'HOSTS': ['http://elastic:changeme@localhost:9200'],
# }, # },
} }
TERMINAL_REPLAY_STORAGE = { DEFAULT_TERMINAL_REPLAY_STORAGE = {
"default": { "default": {
"TYPE": "server", "TYPE": "server",
}, },
} }
TERMINAL_REPLAY_STORAGE = {
}
DEFAULT_PASSWORD_MIN_LENGTH = 6 SECURITY_MFA_AUTH = False
DEFAULT_LOGIN_LIMIT_COUNT = 7 SECURITY_LOGIN_LIMIT_COUNT = 7
DEFAULT_LOGIN_LIMIT_TIME = 30 # Unit: minute SECURITY_LOGIN_LIMIT_TIME = 30 # Unit: minute
DEFAULT_SECURITY_MAX_IDLE_TIME = 30 # Unit: minute SECURITY_MAX_IDLE_TIME = 30 # Unit: minute
SECURITY_PASSWORD_EXPIRATION_TIME = 9999 # Unit: day
SECURITY_PASSWORD_MIN_LENGTH = 6 # Unit: bit
SECURITY_PASSWORD_UPPER_CASE = False
SECURITY_PASSWORD_LOWER_CASE = False
SECURITY_PASSWORD_NUMBER = False
SECURITY_PASSWORD_SPECIAL_CHAR = False
SECURITY_PASSWORD_RULES = [
'SECURITY_PASSWORD_MIN_LENGTH',
'SECURITY_PASSWORD_UPPER_CASE',
'SECURITY_PASSWORD_LOWER_CASE',
'SECURITY_PASSWORD_NUMBER',
'SECURITY_PASSWORD_SPECIAL_CHAR'
]
# Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html # Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html
BOOTSTRAP3 = { BOOTSTRAP3 = {
@ -486,16 +494,20 @@ BOOTSTRAP3 = {
'success_css_class': '', 'success_css_class': '',
} }
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION or 3600 TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
DISPLAY_PER_PAGE = CONFIG.DISPLAY_PER_PAGE or 25 DISPLAY_PER_PAGE = CONFIG.DISPLAY_PER_PAGE
DEFAULT_EXPIRED_YEARS = 70 DEFAULT_EXPIRED_YEARS = 70
USER_GUIDE_URL = "" USER_GUIDE_URL = ""
SWAGGER_SETTINGS = { SWAGGER_SETTINGS = {
'DEFAULT_AUTO_SCHEMA_CLASS': 'jumpserver.swagger.CustomSwaggerAutoSchema',
'SECURITY_DEFINITIONS': { 'SECURITY_DEFINITIONS': {
'basic': { 'basic': {
'type': 'basic' 'type': 'basic'
} }
}, },
} }
# Default email suffix
EMAIL_SUFFIX = CONFIG.EMAIL_SUFFIX

View File

@ -0,0 +1,35 @@
from drf_yasg.inspectors import SwaggerAutoSchema
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
class CustomSwaggerAutoSchema(SwaggerAutoSchema):
def get_tags(self, operation_keys):
if len(operation_keys) > 2 and operation_keys[1].startswith('v'):
return [operation_keys[2]]
return super().get_tags(operation_keys)
def get_swagger_view(version='v1'):
from .urls import api_v1_patterns, api_v2_patterns
if version == "v2":
patterns = api_v2_patterns
else:
patterns = api_v1_patterns
schema_view = get_schema_view(
openapi.Info(
title="Jumpserver API Docs",
default_version=version,
description="Jumpserver Restful api docs",
terms_of_service="https://www.jumpserver.org",
contact=openapi.Contact(email="support@fit2cloud.com"),
license=openapi.License(name="GPLv2 License"),
),
public=True,
patterns=patterns,
permission_classes=(permissions.AllowAny,),
)
return schema_view

View File

@ -1,70 +1,34 @@
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
from __future__ import unicode_literals from __future__ import unicode_literals
import re
import os
from django.urls import path, include, re_path from django.urls import path, include, re_path
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.conf.urls.i18n import i18n_patterns from django.conf.urls.i18n import i18n_patterns
from django.views.i18n import JavaScriptCatalog from django.views.i18n import JavaScriptCatalog
from rest_framework.response import Response
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse
from django.utils.encoding import iri_to_uri
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from .views import IndexView, LunaView, I18NView from .views import IndexView, LunaView, I18NView
from .swagger import get_swagger_view
schema_view = get_schema_view(
openapi.Info(
title="Jumpserver API Docs",
default_version='v1',
description="Jumpserver Restful api docs",
terms_of_service="https://www.jumpserver.org",
contact=openapi.Contact(email="support@fit2cloud.com"),
license=openapi.License(name="GPLv2 License"),
),
public=True,
permission_classes=(permissions.AllowAny,),
)
api_url_pattern = re.compile(r'^/api/(?P<version>\w+)/(?P<app>\w+)/(?P<extra>.*)$')
class HttpResponseTemporaryRedirect(HttpResponse): api_v1_patterns = [
status_code = 307 path('api/', include([
path('users/v1/', include('users.urls.api_urls', namespace='api-users')),
path('assets/v1/', include('assets.urls.api_urls', namespace='api-assets')),
path('perms/v1/', include('perms.urls.api_urls', namespace='api-perms')),
path('terminal/v1/', include('terminal.urls.api_urls', namespace='api-terminal')),
path('ops/v1/', include('ops.urls.api_urls', namespace='api-ops')),
path('audits/v1/', include('audits.urls.api_urls', namespace='api-audits')),
path('orgs/v1/', include('orgs.urls.api_urls', namespace='api-orgs')),
path('common/v1/', include('common.urls.api_urls', namespace='api-common')),
]))
]
def __init__(self, redirect_to): api_v2_patterns = [
HttpResponse.__init__(self) path('api/', include([
self['Location'] = iri_to_uri(redirect_to) path('terminal/v2/', include('terminal.urls.api_urls_v2', namespace='api-terminal-v2')),
path('users/v2/', include('users.urls.api_urls_v2', namespace='api-users-v2')),
]))
@csrf_exempt
def redirect_format_api(request, *args, **kwargs):
_path, query = request.path, request.GET.urlencode()
matched = api_url_pattern.match(_path)
if matched:
version, app, extra = matched.groups()
_path = '/api/{app}/{version}/{extra}?{query}'.format(**{
"app": app, "version": version, "extra": extra,
"query": query
})
return HttpResponseTemporaryRedirect(_path)
else:
return Response({"msg": "Redirect url failed: {}".format(_path)}, status=404)
v1_api_patterns = [
path('users/v1/', include('users.urls.api_urls', namespace='api-users')),
path('assets/v1/', include('assets.urls.api_urls', namespace='api-assets')),
path('perms/v1/', include('perms.urls.api_urls', namespace='api-perms')),
path('terminal/v1/', include('terminal.urls.api_urls', namespace='api-terminal')),
path('ops/v1/', include('ops.urls.api_urls', namespace='api-ops')),
path('audits/v1/', include('audits.urls.api_urls', namespace='api-audits')),
path('orgs/v1/', include('orgs.urls.api_urls', namespace='api-orgs')),
path('common/v1/', include('common.urls.api_urls', namespace='api-common')),
] ]
app_view_patterns = [ app_view_patterns = [
@ -78,6 +42,7 @@ app_view_patterns = [
path('auth/', include('authentication.urls.view_urls'), name='auth'), path('auth/', include('authentication.urls.view_urls'), name='auth'),
] ]
if settings.XPACK_ENABLED: if settings.XPACK_ENABLED:
app_view_patterns.append(path('xpack/', include('xpack.urls', namespace='xpack'))) app_view_patterns.append(path('xpack/', include('xpack.urls', namespace='xpack')))
@ -87,12 +52,13 @@ js_i18n_patterns = i18n_patterns(
urlpatterns = [ urlpatterns = [
path('', IndexView.as_view(), name='index'), path('', IndexView.as_view(), name='index'),
path('', include(api_v2_patterns)),
path('', include(api_v1_patterns)),
path('luna/', LunaView.as_view(), name='luna-error'), path('luna/', LunaView.as_view(), name='luna-error'),
path('i18n/<str:lang>/', I18NView.as_view(), name='i18n-switch'), path('i18n/<str:lang>/', I18NView.as_view(), name='i18n-switch'),
path('settings/', include('common.urls.view_urls', namespace='settings')), path('settings/', include('common.urls.view_urls', namespace='settings')),
path('common/', include('common.urls.view_urls', namespace='common')), path('common/', include('common.urls.view_urls', namespace='common')),
path('api/v1/', redirect_format_api), # path('api/v2/', include(api_v2_patterns)),
path('api/', include(v1_api_patterns)),
# External apps url # External apps url
path('captcha/', include('captcha.urls')), path('captcha/', include('captcha.urls')),
@ -104,7 +70,13 @@ urlpatterns += js_i18n_patterns
if settings.DEBUG: if settings.DEBUG:
urlpatterns += [ urlpatterns += [
re_path('swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=None), name='schema-json'), re_path('^swagger(?P<format>\.json|\.yaml)$',
path('docs/', schema_view.with_ui('swagger', cache_timeout=None), name="docs"), get_swagger_view().without_ui(cache_timeout=1), name='schema-json'),
path('redoc/', schema_view.with_ui('redoc', cache_timeout=None), name='redoc'), path('docs/', get_swagger_view().with_ui('swagger', cache_timeout=1), name="docs"),
path('redoc/', get_swagger_view().with_ui('redoc', cache_timeout=1), name='redoc'),
re_path('^v2/swagger(?P<format>\.json|\.yaml)$',
get_swagger_view().without_ui(cache_timeout=1), name='schema-json'),
path('docs/v2/', get_swagger_view("v2").with_ui('swagger', cache_timeout=1), name="docs"),
path('redoc/v2/', get_swagger_view("v2").with_ui('redoc', cache_timeout=1), name='redoc'),
] ]

View File

@ -1,4 +1,5 @@
import datetime import datetime
import re
from django.http import HttpResponse, HttpResponseRedirect from django.http import HttpResponse, HttpResponseRedirect
from django.conf import settings from django.conf import settings
@ -8,6 +9,10 @@ from django.utils.translation import ugettext_lazy as _
from django.db.models import Count from django.db.models import Count
from django.shortcuts import redirect from django.shortcuts import redirect
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from rest_framework.response import Response
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse
from django.utils.encoding import iri_to_uri
from users.models import User from users.models import User
from assets.models import Asset from assets.models import Asset
@ -188,3 +193,29 @@ class I18NView(View):
response = HttpResponseRedirect(referer_url) response = HttpResponseRedirect(referer_url)
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang) response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang)
return response return response
api_url_pattern = re.compile(r'^/api/(?P<version>\w+)/(?P<app>\w+)/(?P<extra>.*)$')
class HttpResponseTemporaryRedirect(HttpResponse):
status_code = 307
def __init__(self, redirect_to):
HttpResponse.__init__(self)
self['Location'] = iri_to_uri(redirect_to)
@csrf_exempt
def redirect_format_api(request, *args, **kwargs):
_path, query = request.path, request.GET.urlencode()
matched = api_url_pattern.match(_path)
if matched:
version, app, extra = matched.groups()
_path = '/api/{app}/{version}/{extra}?{query}'.format(**{
"app": app, "version": version, "extra": extra,
"query": query
})
return HttpResponseTemporaryRedirect(_path)
else:
return Response({"msg": "Redirect url failed: {}".format(_path)}, status=404)

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-08-08 14:48+0800\n" "POT-Creation-Date: 2018-11-21 19:14+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,58 +17,58 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: static/js/jumpserver.js:158 #: static/js/jumpserver.js:168
msgid "Update is successful!" msgid "Update is successful!"
msgstr "更新成功" msgstr "更新成功"
#: static/js/jumpserver.js:160 #: static/js/jumpserver.js:170
msgid "An unknown error occurred while updating.." msgid "An unknown error occurred while updating.."
msgstr "更新时发生未知错误" msgstr "更新时发生未知错误"
#: static/js/jumpserver.js:205 static/js/jumpserver.js:247 #: static/js/jumpserver.js:236 static/js/jumpserver.js:273
#: static/js/jumpserver.js:252 #: static/js/jumpserver.js:276
msgid "Error" msgid "Error"
msgstr "错误" msgstr "错误"
#: static/js/jumpserver.js:205 #: static/js/jumpserver.js:236
msgid "Being used by the asset, please unbind the asset first." msgid "Being used by the asset, please unbind the asset first."
msgstr "正在被资产使用中,请先解除资产绑定" msgstr "正在被资产使用中,请先解除资产绑定"
#: static/js/jumpserver.js:212 static/js/jumpserver.js:260 #: static/js/jumpserver.js:242 static/js/jumpserver.js:283
msgid "Delete the success" msgid "Delete the success"
msgstr "删除成功" msgstr "删除成功"
#: static/js/jumpserver.js:219 #: static/js/jumpserver.js:248
msgid "Are you sure about deleting it?" msgid "Are you sure about deleting it?"
msgstr "你确定删除吗 ?" msgstr "你确定删除吗 ?"
#: static/js/jumpserver.js:224 static/js/jumpserver.js:273 #: static/js/jumpserver.js:252 static/js/jumpserver.js:293
msgid "Cancel" msgid "Cancel"
msgstr "取消" msgstr "取消"
#: static/js/jumpserver.js:227 static/js/jumpserver.js:276 #: static/js/jumpserver.js:254 static/js/jumpserver.js:295
msgid "Confirm" msgid "Confirm"
msgstr "确认" msgstr "确认"
#: static/js/jumpserver.js:247 #: static/js/jumpserver.js:273
msgid "" msgid ""
"The organization contains undeleted information. Please try again after " "The organization contains undeleted information. Please try again after "
"deleting" "deleting"
msgstr "组织中包含未删除信息,请删除后重试" msgstr "组织中包含未删除信息,请删除后重试"
#: static/js/jumpserver.js:252 #: static/js/jumpserver.js:276
msgid "" msgid ""
"Do not perform this operation under this organization. Try again after " "Do not perform this operation under this organization. Try again after "
"switching to another organization" "switching to another organization"
msgstr "请勿在此组织下执行此操作,切换到其他组织后重试" msgstr "请勿在此组织下执行此操作,切换到其他组织后重试"
#: static/js/jumpserver.js:267 #: static/js/jumpserver.js:289
msgid "" msgid ""
"Please ensure that the following information in the organization has been " "Please ensure that the following information in the organization has been "
"deleted" "deleted"
msgstr "请确保组织内的以下信息已删除" msgstr "请确保组织内的以下信息已删除"
#: static/js/jumpserver.js:269 #: static/js/jumpserver.js:290
msgid "" msgid ""
"User list、User group、Asset list、Domain list、Admin user、System user、" "User list、User group、Asset list、Domain list、Admin user、System user、"
"Labels、Asset permission" "Labels、Asset permission"
@ -76,32 +76,52 @@ msgstr ""
"用户列表、用户组、资产列表、网域列表、管理用户、系统用户、标签管理、资产授权" "用户列表、用户组、资产列表、网域列表、管理用户、系统用户、标签管理、资产授权"
"规则" "规则"
#: static/js/jumpserver.js:311 #: static/js/jumpserver.js:329
msgid "Loading ..." msgid "Loading ..."
msgstr "加载中 ..." msgstr "加载中 ..."
#: static/js/jumpserver.js:313 #: static/js/jumpserver.js:330
msgid "Search" msgid "Search"
msgstr "搜索" msgstr "搜索"
#: static/js/jumpserver.js:317 #: static/js/jumpserver.js:333
#, javascript-format #, javascript-format
msgid "Selected item %d" msgid "Selected item %d"
msgstr "选中 %d 项" msgstr "选中 %d 项"
#: static/js/jumpserver.js:322 #: static/js/jumpserver.js:337
msgid "Per page _MENU_" msgid "Per page _MENU_"
msgstr "每页 _MENU_" msgstr "每页 _MENU_"
#: static/js/jumpserver.js:324 #: static/js/jumpserver.js:338
msgid "" msgid ""
"Displays the results of items _START_ to _END_; A total of _TOTAL_ entries" "Displays the results of items _START_ to _END_; A total of _TOTAL_ entries"
msgstr "显示第 _START_ 至 _END_ 项结果; 总共 _TOTAL_ 项" msgstr "显示第 _START_ 至 _END_ 项结果; 总共 _TOTAL_ 项"
#: static/js/jumpserver.js:328 #: static/js/jumpserver.js:341
msgid "No match" msgid "No match"
msgstr "没有匹配项" msgstr "没有匹配项"
#: static/js/jumpserver.js:330 #: static/js/jumpserver.js:342
msgid "No record" msgid "No record"
msgstr "没有记录" msgstr "没有记录"
#: static/js/jumpserver.js:701
msgid "Password minimum length {N} bits"
msgstr "密码最小长度 {N} 位"
#: static/js/jumpserver.js:702
msgid "Must contain capital letters"
msgstr "必须包含大写字母"
#: static/js/jumpserver.js:703
msgid "Must contain lowercase letters"
msgstr "必须包含小写字母"
#: static/js/jumpserver.js:704
msgid "Must contain numeric characters"
msgstr "必须包含数字字符"
#: static/js/jumpserver.js:705
msgid "Must contain special characters"
msgstr "必须包含特殊字符"

View File

@ -1,18 +1,17 @@
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
import sys import datetime
import json
from collections import defaultdict
from ansible import constants as C
from ansible.plugins.callback import CallbackBase from ansible.plugins.callback import CallbackBase
from ansible.plugins.callback.default import CallbackModule from ansible.plugins.callback.default import CallbackModule
from ansible.plugins.callback.minimal import CallbackModule as CMDCallBackModule
from .display import TeeObj
class AdHocResultCallback(CallbackModule): class CallbackMixin:
""" def __init__(self, display=None):
Task result Callback
"""
def __init__(self, display=None, options=None, file_obj=None):
# result_raw example: { # result_raw example: {
# "ok": {"hostname": {"task_name": {}...},..}, # "ok": {"hostname": {"task_name": {}...},..},
# "failed": {"hostname": {"task_name": {}..}, ..}, # "failed": {"hostname": {"task_name": {}..}, ..},
@ -20,71 +19,138 @@ class AdHocResultCallback(CallbackModule):
# "skipped": {"hostname": {"task_name": {}, ..}, ..}, # "skipped": {"hostname": {"task_name": {}, ..}, ..},
# } # }
# results_summary example: { # results_summary example: {
# "contacted": {"hostname",...}, # "contacted": {"hostname": {"task_name": {}}, "hostname": {}},
# "dark": {"hostname": {"task_name": {}, "task_name": {}},...,}, # "dark": {"hostname": {"task_name": {}, "task_name": {}},...,},
# "success": True
# } # }
self.results_raw = dict(ok={}, failed={}, unreachable={}, skipped={}) self.results_raw = dict(
self.results_summary = dict(contacted=[], dark={}) ok=defaultdict(dict),
failed=defaultdict(dict),
unreachable=defaultdict(dict),
skippe=defaultdict(dict),
)
self.results_summary = dict(
contacted=defaultdict(dict),
dark=defaultdict(dict),
success=True
)
self.results = {
'raw': self.results_raw,
'summary': self.results_summary,
}
super().__init__() super().__init__()
if file_obj is not None: if display:
sys.stdout = TeeObj(file_obj) self._display = display
self._display.columns = 79
def gather_result(self, t, res): def display(self, msg):
self._clean_results(res._result, res._task.action) self._display.display(msg)
host = res._host.get_name()
task_name = res.task_name
task_result = res._result
if self.results_raw[t].get(host): def gather_result(self, t, result):
self.results_raw[t][host][task_name] = task_result self._clean_results(result._result, result._task.action)
else: host = result._host.get_name()
self.results_raw[t][host] = {task_name: task_result} task_name = result.task_name
task_result = result._result
self.results_raw[t][host][task_name] = task_result
self.clean_result(t, host, task_name, task_result) self.clean_result(t, host, task_name, task_result)
class AdHocResultCallback(CallbackMixin, CallbackModule, CMDCallBackModule):
"""
Task result Callback
"""
def clean_result(self, t, host, task_name, task_result): def clean_result(self, t, host, task_name, task_result):
contacted = self.results_summary["contacted"] contacted = self.results_summary["contacted"]
dark = self.results_summary["dark"] dark = self.results_summary["dark"]
if t in ("ok", "skipped") and host not in dark:
if host not in contacted: if task_result.get('rc') is not None:
contacted.append(host) cmd = task_result.get('cmd')
else: if isinstance(cmd, list):
if dark.get(host): cmd = " ".join(cmd)
dark[host][task_name] = task_result.values
else: else:
dark[host] = {task_name: task_result} cmd = str(cmd)
if host in contacted: detail = {
contacted.remove(host) 'cmd': cmd,
'stderr': task_result.get('stderr'),
'stdout': task_result.get('stdout'),
'rc': task_result.get('rc'),
'delta': task_result.get('delta'),
'msg': task_result.get('msg', '')
}
else:
detail = {
"changed": task_result.get('changed', False),
"msg": task_result.get('msg', '')
}
if t in ("ok", "skipped"):
contacted[host][task_name] = detail
else:
dark[host][task_name] = detail
def v2_runner_on_failed(self, result, ignore_errors=False): def v2_runner_on_failed(self, result, ignore_errors=False):
self.results_summary['success'] = False
self.gather_result("failed", result) self.gather_result("failed", result)
super().v2_runner_on_failed(result, ignore_errors=ignore_errors)
if result._task.action in C.MODULE_NO_JSON:
CMDCallBackModule.v2_runner_on_failed(self,
result, ignore_errors=ignore_errors
)
else:
super().v2_runner_on_failed(
result, ignore_errors=ignore_errors
)
def v2_runner_on_ok(self, result): def v2_runner_on_ok(self, result):
self.gather_result("ok", result) self.gather_result("ok", result)
super().v2_runner_on_ok(result) if result._task.action in C.MODULE_NO_JSON:
CMDCallBackModule.v2_runner_on_ok(self, result)
else:
super().v2_runner_on_ok(result)
def v2_runner_on_skipped(self, result): def v2_runner_on_skipped(self, result):
self.gather_result("skipped", result) self.gather_result("skipped", result)
super().v2_runner_on_skipped(result) super().v2_runner_on_skipped(result)
def v2_runner_on_unreachable(self, result): def v2_runner_on_unreachable(self, result):
self.results_summary['success'] = False
self.gather_result("unreachable", result) self.gather_result("unreachable", result)
super().v2_runner_on_unreachable(result) super().v2_runner_on_unreachable(result)
def on_playbook_start(self, name):
date_start = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self.display(
"{} Start task: {}\r\n".format(date_start, name)
)
def on_playbook_end(self, name):
date_finished = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self.display(
"{} Task finish\r\n".format(date_finished)
)
def display_skipped_hosts(self):
pass
def display_ok_hosts(self):
pass
class CommandResultCallback(AdHocResultCallback): class CommandResultCallback(AdHocResultCallback):
""" """
Command result callback Command result callback
results_command: {
"cmd": "",
"stderr": "",
"stdout": "",
"rc": 0,
"delta": 0:0:0.123
}
""" """
def __init__(self, display=None): def __init__(self, display=None, **kwargs):
# results_command: {
# "cmd": "",
# "stderr": "",
# "stdout": "",
# "rc": 0,
# "delta": 0:0:0.123
# }
#
self.results_command = dict() self.results_command = dict()
super().__init__(display) super().__init__(display)
@ -92,6 +158,43 @@ class CommandResultCallback(AdHocResultCallback):
super().gather_result(t, res) super().gather_result(t, res)
self.gather_cmd(t, res) self.gather_cmd(t, res)
def v2_playbook_on_play_start(self, play):
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
msg = '$ {} ({})'.format(play.name, now)
self._play = play
self._display.banner(msg)
def v2_runner_on_unreachable(self, result):
self.results_summary['success'] = False
self.gather_result("unreachable", result)
msg = result._result.get("msg")
if not msg:
msg = json.dumps(result._result, indent=4)
self._display.display("%s | FAILED! => \n%s" % (
result._host.get_name(),
msg,
), color=C.COLOR_ERROR)
def v2_runner_on_failed(self, result, ignore_errors=False):
self.results_summary['success'] = False
self.gather_result("failed", result)
msg = result._result.get("msg", '')
stderr = result._result.get("stderr")
if stderr:
msg += '\n' + stderr
module_stdout = result._result.get("module_stdout")
if module_stdout:
msg += '\n' + module_stdout
if not msg:
msg = json.dumps(result._result, indent=4)
self._display.display("%s | FAILED! => \n%s" % (
result._host.get_name(),
msg,
), color=C.COLOR_ERROR)
def _print_task_banner(self, task):
pass
def gather_cmd(self, t, res): def gather_cmd(self, t, res):
host = res._host.get_name() host = res._host.get_name()
cmd = {} cmd = {}

View File

@ -17,7 +17,7 @@ from common.utils import get_logger
from .exceptions import AnsibleError from .exceptions import AnsibleError
__all__ = ["AdHocRunner", "PlayBookRunner"] __all__ = ["AdHocRunner", "PlayBookRunner", "CommandRunner"]
C.HOST_KEY_CHECKING = False C.HOST_KEY_CHECKING = False
logger = get_logger(__name__) logger = get_logger(__name__)
@ -45,7 +45,7 @@ def get_default_options():
listtasks=False, listtasks=False,
listhosts=False, listhosts=False,
syntax=False, syntax=False,
timeout=60, timeout=30,
connection='ssh', connection='ssh',
module_path='', module_path='',
forks=10, forks=10,
@ -135,6 +135,7 @@ class AdHocRunner:
loader_class = DataLoader loader_class = DataLoader
variable_manager_class = VariableManager variable_manager_class = VariableManager
default_options = get_default_options() default_options = get_default_options()
command_modules_choices = ('shell', 'raw', 'command', 'script', 'win_shell')
def __init__(self, inventory, options=None): def __init__(self, inventory, options=None):
self.options = self.update_options(options) self.options = self.update_options(options)
@ -145,7 +146,7 @@ class AdHocRunner:
) )
def get_result_callback(self, file_obj=None): def get_result_callback(self, file_obj=None):
return self.__class__.results_callback_class(file_obj=file_obj) return self.__class__.results_callback_class()
@staticmethod @staticmethod
def check_module_args(module_name, module_args=''): def check_module_args(module_name, module_args=''):
@ -163,10 +164,28 @@ class AdHocRunner:
"pattern: %s dose not match any hosts." % pattern "pattern: %s dose not match any hosts." % pattern
) )
def clean_args(self, module, args):
if module not in self.command_modules_choices:
return args
if isinstance(args, str):
if args.startswith('executable='):
_args = args.split(' ')
executable, command = _args[0].split('=')[1], ' '.join(_args[1:])
args = {'executable': executable, '_raw_params': command}
else:
args = {'_raw_params': args}
return args
else:
return args
def clean_tasks(self, tasks): def clean_tasks(self, tasks):
cleaned_tasks = [] cleaned_tasks = []
for task in tasks: for task in tasks:
self.check_module_args(task['action']['module'], task['action'].get('args')) module = task['action']['module']
args = task['action'].get('args')
cleaned_args = self.clean_args(module, args)
task['action']['args'] = cleaned_args
self.check_module_args(module, cleaned_args)
cleaned_tasks.append(task) cleaned_tasks.append(task)
return cleaned_tasks return cleaned_tasks
@ -177,17 +196,16 @@ class AdHocRunner:
options = self.__class__.default_options options = self.__class__.default_options
return options return options
def run(self, tasks, pattern, play_name='Ansible Ad-hoc', gather_facts='no', file_obj=None): def run(self, tasks, pattern, play_name='Ansible Ad-hoc', gather_facts='no'):
""" """
:param tasks: [{'action': {'module': 'shell', 'args': 'ls'}, ...}, ] :param tasks: [{'action': {'module': 'shell', 'args': 'ls'}, ...}, ]
:param pattern: all, *, or others :param pattern: all, *, or others
:param play_name: The play name :param play_name: The play name
:param gather_facts: :param gather_facts:
:param file_obj: logging to file_obj
:return: :return:
""" """
self.check_pattern(pattern) self.check_pattern(pattern)
self.results_callback = self.get_result_callback(file_obj) self.results_callback = self.get_result_callback()
cleaned_tasks = self.clean_tasks(tasks) cleaned_tasks = self.clean_tasks(tasks)
play_source = dict( play_source = dict(
@ -211,10 +229,6 @@ class AdHocRunner:
stdout_callback=self.results_callback, stdout_callback=self.results_callback,
passwords=self.options.passwords, passwords=self.options.passwords,
) )
print("Get matched hosts: {}".format(
self.inventory.get_matched_hosts(pattern)
))
try: try:
tqm.run(play) tqm.run(play)
return self.results_callback return self.results_callback
@ -229,16 +243,12 @@ class CommandRunner(AdHocRunner):
results_callback_class = CommandResultCallback results_callback_class = CommandResultCallback
modules_choices = ('shell', 'raw', 'command', 'script') modules_choices = ('shell', 'raw', 'command', 'script')
def execute(self, cmd, pattern, module=None): def execute(self, cmd, pattern, module='shell'):
if module and module not in self.modules_choices: if module and module not in self.modules_choices:
raise AnsibleError("Module should in {}".format(self.modules_choices)) raise AnsibleError("Module should in {}".format(self.modules_choices))
else:
module = "shell"
tasks = [ tasks = [
{"action": {"module": module, "args": cmd}} {"action": {"module": module, "args": cmd}}
] ]
hosts = self.inventory.get_hosts(pattern=pattern) return self.run(tasks, pattern, play_name=cmd)
name = "Run command {} on {}".format(cmd, ", ".join([host.name for host in hosts]))
return self.run(tasks, pattern, play_name=name)

5
apps/ops/api/__init__.py Normal file
View File

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
#
from .adhoc import *
from .celery import *
from .command import *

View File

@ -1,31 +1,37 @@
# ~*~ coding: utf-8 ~*~ # -*- coding: utf-8 -*-
import uuid #
import os
from django.core.cache import cache
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _
from rest_framework import viewsets, generics from rest_framework import viewsets, generics
from rest_framework.views import Response from rest_framework.views import Response
from common.permissions import IsOrgAdmin from common.permissions import IsOrgAdmin
from .models import Task, AdHoc, AdHocRunHistory, CeleryTask from orgs.utils import current_org
from .serializers import TaskSerializer, AdHocSerializer, \ from ..models import Task, AdHoc, AdHocRunHistory
from ..serializers import TaskSerializer, AdHocSerializer, \
AdHocRunHistorySerializer AdHocRunHistorySerializer
from .tasks import run_ansible_task from ..tasks import run_ansible_task
__all__ = [
'TaskViewSet', 'TaskRun', 'AdHocViewSet', 'AdHocRunHistoryViewSet'
]
class TaskViewSet(viewsets.ModelViewSet): class TaskViewSet(viewsets.ModelViewSet):
queryset = Task.objects.all() queryset = Task.objects.all()
serializer_class = TaskSerializer serializer_class = TaskSerializer
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
label = None
help_text = '' def get_queryset(self):
queryset = super().get_queryset()
if current_org:
queryset = queryset.filter(created_by=current_org.id)
return queryset
class TaskRun(generics.RetrieveAPIView): class TaskRun(generics.RetrieveAPIView):
queryset = Task.objects.all() queryset = Task.objects.all()
serializer_class = TaskViewSet # serializer_class = TaskViewSet
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
@ -47,7 +53,7 @@ class AdHocViewSet(viewsets.ModelViewSet):
return self.queryset return self.queryset
class AdHocRunHistorySet(viewsets.ModelViewSet): class AdHocRunHistoryViewSet(viewsets.ModelViewSet):
queryset = AdHocRunHistory.objects.all() queryset = AdHocRunHistory.objects.all()
serializer_class = AdHocRunHistorySerializer serializer_class = AdHocRunHistorySerializer
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
@ -66,28 +72,6 @@ class AdHocRunHistorySet(viewsets.ModelViewSet):
return self.queryset return self.queryset
class CeleryTaskLogApi(generics.RetrieveAPIView):
permission_classes = (IsOrgAdmin,)
buff_size = 1024 * 10
end = False
queryset = CeleryTask.objects.all()
def get(self, request, *args, **kwargs):
mark = request.query_params.get("mark") or str(uuid.uuid4())
task = self.get_object()
log_path = task.full_log_path
if not log_path or not os.path.isfile(log_path):
return Response({"data": _("Waiting ...")}, status=203)
with open(log_path, 'r') as f:
offset = cache.get(mark, 0)
f.seek(offset)
data = f.read(self.buff_size).replace('\n', '\r\n')
mark = str(uuid.uuid4())
cache.set(mark, f.tell(), 5)
if data == '' and task.is_finished():
self.end = True
return Response({"data": data, 'end': self.end, 'mark': mark})

53
apps/ops/api/celery.py Normal file
View File

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
#
import uuid
import os
from celery.result import AsyncResult
from django.core.cache import cache
from django.utils.translation import ugettext as _
from rest_framework import generics
from rest_framework.views import Response
from common.permissions import IsOrgAdmin, IsValidUser
from ..models import CeleryTask
from ..serializers import CeleryResultSerializer
__all__ = ['CeleryTaskLogApi', 'CeleryResultApi']
class CeleryTaskLogApi(generics.RetrieveAPIView):
permission_classes = (IsValidUser,)
buff_size = 1024 * 10
end = False
queryset = CeleryTask.objects.all()
def get(self, request, *args, **kwargs):
mark = request.query_params.get("mark") or str(uuid.uuid4())
task = self.get_object()
log_path = task.full_log_path
if not log_path or not os.path.isfile(log_path):
return Response({"data": _("Waiting ...")}, status=203)
with open(log_path, 'r') as f:
offset = cache.get(mark, 0)
f.seek(offset)
data = f.read(self.buff_size).replace('\n', '\r\n')
mark = str(uuid.uuid4())
cache.set(mark, f.tell(), 5)
if data == '' and task.is_finished():
self.end = True
return Response({"data": data, 'end': self.end, 'mark': mark})
class CeleryResultApi(generics.RetrieveAPIView):
permission_classes = (IsValidUser,)
serializer_class = CeleryResultSerializer
def get_object(self):
pk = self.kwargs.get('pk')
return AsyncResult(pk)

27
apps/ops/api/command.py Normal file
View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
#
from rest_framework import viewsets
from common.permissions import IsValidUser
from ..models import CommandExecution
from ..serializers import CommandExecutionSerializer
from ..tasks import run_command_execution
class CommandExecutionViewSet(viewsets.ModelViewSet):
serializer_class = CommandExecutionSerializer
permission_classes = (IsValidUser,)
task = None
def get_queryset(self):
return CommandExecution.objects.filter(
user_id=str(self.request.user.id)
)
def perform_create(self, serializer):
instance = serializer.save()
instance.user = self.request.user
instance.save()
run_command_execution.apply_async(
args=(instance.id,), task_id=str(instance.id)
)

View File

@ -27,7 +27,6 @@ def on_app_ready(sender=None, headers=None, body=None, **kwargs):
if cache.get("CELERY_APP_READY", 0) == 1: if cache.get("CELERY_APP_READY", 0) == 1:
return return
cache.set("CELERY_APP_READY", 1, 10) cache.set("CELERY_APP_READY", 1, 10)
logger.debug("App ready signal recv")
tasks = get_after_app_ready_tasks() tasks = get_after_app_ready_tasks()
logger.debug("Start need start task: [{}]".format( logger.debug("Start need start task: [{}]".format(
", ".join(tasks)) ", ".join(tasks))

17
apps/ops/forms.py Normal file
View File

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
#
from django import forms
from assets.models import SystemUser
from .models import CommandExecution
class CommandExecutionForm(forms.ModelForm):
class Meta:
model = CommandExecution
fields = ['run_as', 'command']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
run_as_field = self.fields.get('run_as')
run_as_field.queryset = SystemUser.objects.all()

View File

@ -2,7 +2,7 @@
# #
from .ansible.inventory import BaseInventory from .ansible.inventory import BaseInventory
from assets.utils import get_assets_by_fullname_list, get_system_user_by_name from assets.utils import get_assets_by_id_list, get_system_user_by_id
__all__ = [ __all__ = [
'JMSInventory' 'JMSInventory'
@ -14,19 +14,18 @@ class JMSInventory(BaseInventory):
JMS Inventory is the manager with jumpserver assets, so you can JMS Inventory is the manager with jumpserver assets, so you can
write you own manager, construct you inventory write you own manager, construct you inventory
""" """
def __init__(self, hostname_list, run_as_admin=False, run_as=None, become_info=None): def __init__(self, assets, run_as_admin=False, run_as=None, become_info=None):
""" """
:param hostname_list: ["test1", ] :param host_id_list: ["test1", ]
:param run_as_admin: True 是否使用管理用户去执行, 每台服务器的管理用户可能不同 :param run_as_admin: True 是否使用管理用户去执行, 每台服务器的管理用户可能不同
:param run_as: 是否统一使用某个系统用户去执行 :param run_as: 是否统一使用某个系统用户去执行
:param become_info: 是否become成某个用户去执行 :param become_info: 是否become成某个用户去执行
""" """
self.hostname_list = hostname_list self.assets = assets
self.using_admin = run_as_admin self.using_admin = run_as_admin
self.run_as = run_as self.run_as = run_as
self.become_info = become_info self.become_info = become_info
assets = self.get_jms_assets()
host_list = [] host_list = []
for asset in assets: for asset in assets:
@ -43,14 +42,10 @@ class JMSInventory(BaseInventory):
host.update(become_info) host.update(become_info)
super().__init__(host_list=host_list) super().__init__(host_list=host_list)
def get_jms_assets(self):
assets = get_assets_by_fullname_list(self.hostname_list)
return assets
def convert_to_ansible(self, asset, run_as_admin=False): def convert_to_ansible(self, asset, run_as_admin=False):
info = { info = {
'id': asset.id, 'id': asset.id,
'hostname': asset.fullname, 'hostname': asset.hostname,
'ip': asset.ip, 'ip': asset.ip,
'port': asset.port, 'port': asset.port,
'vars': dict(), 'vars': dict(),
@ -75,7 +70,7 @@ class JMSInventory(BaseInventory):
return info return info
def get_run_user_info(self): def get_run_user_info(self):
system_user = get_system_user_by_name(self.run_as) system_user = self.run_as
if not system_user: if not system_user:
return {} return {}
else: else:

View File

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-02 09:45
from __future__ import unicode_literals
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('ops', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='CeleryTask',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=1024)),
('status', models.CharField(choices=[('waiting', 'waiting'), ('running', 'running'), ('finished', 'finished')], max_length=128)),
('log_path', models.CharField(blank=True, max_length=256, null=True)),
('date_published', models.DateTimeField(auto_now_add=True)),
('date_start', models.DateTimeField(null=True)),
('date_finished', models.DateTimeField(null=True)),
],
),
]

View File

@ -0,0 +1,56 @@
# Generated by Django 2.1.4 on 2018-12-07 09:44
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('assets', '0023_auto_20181016_1650'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('ops', '0002_celerytask'),
]
operations = [
migrations.CreateModel(
name='CommandExecution',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('command', models.TextField(verbose_name='Command')),
('_result', models.TextField(blank=True, null=True, verbose_name='Result')),
('is_finished', models.BooleanField(default=False)),
('date_created', models.DateTimeField(auto_now_add=True)),
('date_start', models.DateTimeField(null=True)),
('date_finished', models.DateTimeField(null=True)),
('hosts', models.ManyToManyField(to='assets.Asset')),
('run_as', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.SystemUser')),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.RemoveField(
model_name='adhoc',
name='run_as',
),
migrations.AddField(
model_name='adhoc',
name='hosts',
field=models.ManyToManyField(to='assets.Asset', verbose_name='Host'),
),
migrations.AlterField(
model_name='task',
name='created_by',
field=models.CharField(blank=True, default='', max_length=128),
),
migrations.AlterField(
model_name='task',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterUniqueTogether(
name='task',
unique_together={('name', 'created_by')},
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 2.1.4 on 2018-12-07 09:44
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0023_auto_20181016_1650'),
('ops', '0003_auto_20181207_1744'),
]
operations = [
migrations.AddField(
model_name='adhoc',
name='run_as',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.SystemUser'),
),
]

View File

@ -2,4 +2,5 @@
# #
from .adhoc import * from .adhoc import *
from .celery import * from .celery import *
from .command import *

View File

@ -34,16 +34,17 @@ class Task(models.Model):
One task can have some versions of adhoc, run a task only run the latest version adhoc One task can have some versions of adhoc, run a task only run the latest version adhoc
""" """
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name')) name = models.CharField(max_length=128, verbose_name=_('Name'))
interval = models.IntegerField(verbose_name=_("Interval"), null=True, blank=True, help_text=_("Units: seconds")) 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 * * * *")) crontab = models.CharField(verbose_name=_("Crontab"), null=True, blank=True, max_length=128, help_text=_("5 * * * *"))
is_periodic = models.BooleanField(default=False) is_periodic = models.BooleanField(default=False)
callback = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Callback")) # Callback must be a registered celery task callback = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Callback")) # Callback must be a registered celery task
is_deleted = models.BooleanField(default=False) is_deleted = models.BooleanField(default=False)
comment = models.TextField(blank=True, verbose_name=_("Comment")) comment = models.TextField(blank=True, verbose_name=_("Comment"))
created_by = models.CharField(max_length=128, blank=True, null=True, default='') created_by = models.CharField(max_length=128, blank=True, default='')
date_created = models.DateTimeField(auto_now_add=True) date_created = models.DateTimeField(auto_now_add=True)
__latest_adhoc = None __latest_adhoc = None
_ignore_auto_created_by = True
@property @property
def short_id(self): def short_id(self):
@ -94,7 +95,7 @@ class Task(models.Model):
update_fields=None): update_fields=None):
from ..tasks import run_ansible_task from ..tasks import run_ansible_task
super().save( super().save(
force_insert=force_insert, force_update=force_update, force_insert=force_insert, force_update=force_update,
using=using, update_fields=update_fields, using=using, update_fields=update_fields,
) )
@ -108,7 +109,7 @@ class Task(models.Model):
crontab = self.crontab crontab = self.crontab
tasks = { tasks = {
self.name: { self.__str__(): {
"task": run_ansible_task.name, "task": run_ansible_task.name,
"interval": interval, "interval": interval,
"crontab": crontab, "crontab": crontab,
@ -119,11 +120,11 @@ class Task(models.Model):
} }
create_or_update_celery_periodic_tasks(tasks) create_or_update_celery_periodic_tasks(tasks)
else: else:
disable_celery_periodic_task(self.name) disable_celery_periodic_task(self.__str__())
def delete(self, using=None, keep_parents=False): def delete(self, using=None, keep_parents=False):
super().delete(using=using, keep_parents=keep_parents) super().delete(using=using, keep_parents=keep_parents)
delete_celery_periodic_task(self.name) delete_celery_periodic_task(self.__str__())
@property @property
def schedule(self): def schedule(self):
@ -133,10 +134,11 @@ class Task(models.Model):
return None return None
def __str__(self): def __str__(self):
return self.name return self.name + '@' + str(self.created_by)
class Meta: class Meta:
db_table = 'ops_task' db_table = 'ops_task'
unique_together = ('name', 'created_by')
get_latest_by = 'date_created' get_latest_by = 'date_created'
@ -157,8 +159,9 @@ class AdHoc(models.Model):
pattern = models.CharField(max_length=64, default='{}', verbose_name=_('Pattern')) pattern = models.CharField(max_length=64, default='{}', verbose_name=_('Pattern'))
_options = models.CharField(max_length=1024, default='', verbose_name=_('Options')) _options = models.CharField(max_length=1024, default='', verbose_name=_('Options'))
_hosts = models.TextField(blank=True, verbose_name=_('Hosts')) # ['hostname1', 'hostname2'] _hosts = models.TextField(blank=True, verbose_name=_('Hosts')) # ['hostname1', 'hostname2']
hosts = models.ManyToManyField('assets.Asset', verbose_name=_("Host"))
run_as_admin = models.BooleanField(default=False, verbose_name=_('Run as admin')) run_as_admin = models.BooleanField(default=False, verbose_name=_('Run as admin'))
run_as = models.CharField(max_length=128, default='', verbose_name=_("Run as")) run_as = models.ForeignKey('assets.SystemUser', null=True, on_delete=models.CASCADE)
_become = models.CharField(max_length=1024, default='', verbose_name=_("Become")) _become = models.CharField(max_length=1024, default='', verbose_name=_("Become"))
created_by = models.CharField(max_length=64, default='', null=True, verbose_name=_('Create by')) created_by = models.CharField(max_length=64, default='', null=True, verbose_name=_('Create by'))
date_created = models.DateTimeField(auto_now_add=True) date_created = models.DateTimeField(auto_now_add=True)
@ -174,14 +177,6 @@ class AdHoc(models.Model):
else: else:
raise SyntaxError('Tasks should be a list: {}'.format(item)) raise SyntaxError('Tasks should be a list: {}'.format(item))
@property
def hosts(self):
return json.loads(self._hosts)
@hosts.setter
def hosts(self, item):
self._hosts = json.dumps(item)
@property @property
def inventory(self): def inventory(self):
if self.become: if self.become:
@ -194,7 +189,7 @@ class AdHoc(models.Model):
become_info = None become_info = None
inventory = JMSInventory( inventory = JMSInventory(
self.hosts, run_as_admin=self.run_as_admin, self.hosts.all(), run_as_admin=self.run_as_admin,
run_as=self.run_as, become_info=become_info run_as=self.run_as, become_info=become_info
) )
return inventory return inventory
@ -242,14 +237,13 @@ class AdHoc(models.Model):
history.timedelta = time.time() - time_start history.timedelta = time.time() - time_start
history.save() history.save()
def _run_only(self, file_obj=None): def _run_only(self):
runner = AdHocRunner(self.inventory, options=self.options) runner = AdHocRunner(self.inventory, options=self.options)
try: try:
result = runner.run( result = runner.run(
self.tasks, self.tasks,
self.pattern, self.pattern,
self.task.name, self.task.name,
file_obj=file_obj,
) )
return result.results_raw, result.results_summary return result.results_raw, result.results_summary
except AnsibleError as e: except AnsibleError as e:

View File

@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
#
import uuid
import json
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
from django.db import models
from ..ansible.runner import CommandRunner
from ..inventory import JMSInventory
class CommandExecution(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
hosts = models.ManyToManyField('assets.Asset')
run_as = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE)
command = models.TextField(verbose_name=_("Command"))
_result = models.TextField(blank=True, null=True, verbose_name=_('Result'))
user = models.ForeignKey('users.User', on_delete=models.CASCADE, null=True)
is_finished = models.BooleanField(default=False)
date_created = models.DateTimeField(auto_now_add=True)
date_start = models.DateTimeField(null=True)
date_finished = models.DateTimeField(null=True)
def __str__(self):
return self.command[:10]
@property
def inventory(self):
return JMSInventory(self.hosts.all(), run_as=self.run_as)
@property
def result(self):
if self._result:
return json.loads(self._result)
else:
return {}
@result.setter
def result(self, item):
self._result = json.dumps(item)
@property
def is_success(self):
if 'error' in self.result:
return False
return True
def get_hosts_names(self):
return ','.join(self.hosts.all().values_list('hostname', flat=True))
def run(self):
print('-'*10 + ' ' + ugettext('Task start') + ' ' + '-'*10)
self.date_start = timezone.now()
ok, msg = self.run_as.is_command_can_run(self.command)
if ok:
runner = CommandRunner(self.inventory)
try:
result = runner.execute(self.command, 'all')
self.result = result.results_command
except Exception as e:
print("Error occur: {}".format(e))
self.result = {"error": str(e)}
else:
msg = _("Command `{}` is forbidden ........").format(self.command)
print('\033[31m' + msg + '\033[0m')
self.result = {"error": msg}
self.is_finished = True
self.date_finished = timezone.now()
self.save()
print('-'*10 + ' ' + ugettext('Task end') + ' ' + '-'*10)
return self.result

View File

@ -1,8 +1,19 @@
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework import serializers from rest_framework import serializers
from django.shortcuts import reverse
from .models import Task, AdHoc, AdHocRunHistory from .models import Task, AdHoc, AdHocRunHistory, CommandExecution
class CeleryResultSerializer(serializers.Serializer):
id = serializers.UUIDField()
result = serializers.JSONField()
state = serializers.CharField(max_length=16)
class CeleryTaskSerializer(serializers.Serializer):
pass
class TaskSerializer(serializers.ModelSerializer): class TaskSerializer(serializers.ModelSerializer):
@ -51,3 +62,23 @@ class AdHocRunHistorySerializer(serializers.ModelSerializer):
fields = super().get_field_names(declared_fields, info) fields = super().get_field_names(declared_fields, info)
fields.extend(['summary', 'short_id']) fields.extend(['summary', 'short_id'])
return fields return fields
class CommandExecutionSerializer(serializers.ModelSerializer):
result = serializers.JSONField(read_only=True)
log_url = serializers.SerializerMethodField()
class Meta:
model = CommandExecution
fields = [
'id', 'hosts', 'run_as', 'command', 'result', 'log_url',
'is_finished', 'date_created', 'date_finished'
]
read_only_fields = [
'id', 'result', 'is_finished', 'log_url', 'date_created',
'date_finished'
]
@staticmethod
def get_log_url(obj):
return reverse('api-ops:celery-task-log', kwargs={'pk': obj.id})

Some files were not shown because too many files have changed in this diff Show More