|
|
Jumpserver是全球首款完全开源的堡垒机,是符合 4A 的专业运维审计系统。 http://www.jumpserver.org
* ~. d( X6 i4 p' z! w2 O' d6 @& g/ c! I- z, y- e, {
jms很好用,但是引入公司内部,也会有一些问题,比如资产导入,资产和项目关联,如何和现有cmdb打通等问题。我这里只是抛砖引玉,介绍下jms如何二次开发,做企业定制化版的jumpserver,打通DevOps系统的方法。
" B8 m4 c9 ]. @* Q- X首先,以增加项目管理 这个功能为例,说说堡垒机如何二次开发的。
7 \7 ~0 ^% _2 n# O8 M# Z0 P: v8 S, u: i) a8 _) \- \* f' K
基于版本 Version 1.4.10-2 GPLv2.3 f7 O( a; y0 Q) H P! I% O
; y8 C- g" s9 m7 z
一、概述* p! b! A/ g( D* M
Jumpserver用的是python的django框架开发,API 采用了DRF(Django REST framework),基于CBV(Class-based views)开发模式,因此,二次开发也是比较简单的,一般先定义model,然后写api,再使用template渲染,前端基本用datatables较多。
( v: {; s# y$ I8 @" C$ B% e3 O$ F# u* y! y8 s3 c; I
实现效果:' m7 Y5 }3 P. M3 O `
项目管理
1 G' ~* p+ c+ r1 `. g项目详情7 p- F1 P% ?8 {/ N
项目详情2
5 ^) d# E1 `. }, U' P/ j+ P7 J, M" _7 G0 m& L& o: ]4 Y
二、实战
, i1 \1 c8 G5 r# a项目(project)管理或者说是服务(service)管理,服务和主机是有关系的,我这里一个主机可以部署多个服务,同时一个服务也可以部署在多台主机上,所以是多堆多的关系$ s0 i$ i0 F6 M- }
) x) O$ B, ?6 u5 [& v7 J, J
2.1 定义project model* O f) w M4 \# ~- F8 G+ H/ X
jumpserver/apps/assets/models/project.py
; O3 [$ s: z* g" s5 ] X" a# \" t, a4 P4 o. }& O* ?
6 y! r2 l& ]5 s! m8 m3 b
7 J* ~/ l9 y2 ]. a; a#!/usr/bin/env python
6 S; j2 c$ J4 y9 r" `# -*- coding: utf-8 -*-
$ A9 V8 j1 ~. Q! d% m0 ?1 B
' @! M3 c# `/ `. Q$ l$ ~ s# M- afrom __future__ import unicode_literals
& E" ?6 t5 n& N+ ~5 ^4 r# @/ _1 e4 X6 m7 i( w* J/ `: N
from django.db import models
d8 w2 `5 P9 E/ W# zimport logging
% X9 U, ^$ Q- _. f' n* Lfrom django.utils.translation import ugettext_lazy as _" b' z# A, j0 T$ V$ E# T
import uuid- `9 G x; K' h5 J
from django.core.cache import cache4 h* @! j! j9 K
9 C- V0 C0 j ^4 h__all__ = ['Project', 'TYPE_CHOICES'], ` s8 m0 ]" O' N
logger = logging.getLogger(__name__) {8 r' \) o8 Q; b4 p4 b6 @( ?5 f
4 b: H5 |' {5 R- o) g
TYPE_CHOICES = (
, ]6 k% Y0 ]% U4 f) j5 W ('Java', 'java'),6 s+ X! J5 @+ ]% D
('React', 'react'),* p5 A" Z+ C, B* {: {; \
('Cpp', 'c++'),
3 {3 e- O( l) b8 \+ e( F) ~( x ('Python', 'python')," V) r: r/ m5 C) H+ z& U& F! m
('Middleware', 'Middleware'),3 t* ]$ \7 a( B. |# c$ Y
('Other', 'Other'),
5 K5 b+ g4 a p( j )* q: v7 G) g3 d9 V i% e2 h7 ]
& b: `+ m/ k' R6 t
* }# A5 d! H' J0 j7 E4 P3 fclass Project(models.Model):/ x/ D2 i8 H. M" o1 P# S8 B$ G0 w
TYPE_CHOICES =TYPE_CHOICES, J% I P, b5 K% ?9 m: X
" G6 R4 _; r6 C+ V, n+ L# v+ f0 P id = models.UUIDField(default=uuid.uuid4, primary_key=True)
0 j' Y" C& @$ z; e8 T, f0 ?1 U) k name = models.CharField(max_length=64, unique=True, verbose_name=_('Name'))" [ W" P# T" Z+ k
! ^; \7 s. d% e. `3 m* ^2 A0 J parent = models.ForeignKey('self', null=True, blank=True, related_name="children", on_delete=models.SET_NULL)
6 f3 I) v" N/ u [8 |* o: @3 {' B& M9 ]- c/ X
domain_name = models.CharField(max_length=64, blank=True, verbose_name=_('Domain Name'))2 q2 ?* K9 m3 S. W( G" ~2 j. f4 R
git_address = models.CharField(max_length=128, blank=True, verbose_name=_('Git Address'))! o) @5 s3 g: U/ h& N( k1 W, Z* c
port = models.IntegerField(blank=True, null=True, verbose_name=_('Open port'))/ h0 V e& d5 @5 W8 N2 b
type = models.CharField(choices=TYPE_CHOICES, max_length=16, blank=True, null=True,
( M5 k5 W- p, [! ^! V default='Java', verbose_name=_('Project type'), )- i- O; f1 f2 p% v. d% K% W4 A
dev_users = models.ManyToManyField(3 U; v" c7 k+ s: \# x9 q
'users.User', related_name='users1',
# ^$ y# x! h2 v blank=True, verbose_name=_('Dev Users')
) _/ [$ r! r+ m: O )) L& ~9 N/ ]: ~* ?% C9 D K5 y1 O
sa_users = models.ManyToManyField(% Z1 h) F& Y! x
'users.User', related_name='users2',
3 N9 K2 ~! e X9 W blank=True, verbose_name=_('Sa Users')
( ?( r5 Q5 ?# V$ e9 } )
9 k3 |: I3 I1 x4 { scrum_master = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, verbose_name=_('Scrum Master'))! W: ^3 `% F U" x1 ?% J2 x
created_by = models.CharField(max_length=32, blank=True, verbose_name=_('Created by'))2 `5 l6 A* z- D9 v: S) T
updated_by = models.CharField(max_length=32, blank=True, verbose_name=_('Updated by'))
; {8 V, X, F& G9 m date_created = models.DateTimeField(auto_now_add=True, null=True, verbose_name=_('Date created'))
8 L+ U8 l% e) K date_updated = models.DateTimeField(auto_now=True, null=True, verbose_name=_('Date updated'))
, i) D8 {4 J; ?8 T comment = models.TextField(blank=True, verbose_name=_('Comment')), z1 L$ ]1 s0 |2 q& j/ t
level = models.IntegerField(default=0, verbose_name=_('Level'))
6 M/ X @# B- c5 U
. N$ P/ U; ]4 x3 D def __unicode__(self):3 F) h3 u* {0 A# z3 |% B" l' J0 c
return self.name z* R5 i h& a
__str__ = __unicode__9 g8 J6 Y/ b* q
# ?3 K0 V: @4 C
@property- p2 z: V5 i' c0 C! ^' Y; f! L0 e2 ~
def all_sa_users(self):; F M7 Y( u2 z. A4 |* P
return [user.name for user in self.sa_users.all()]
# z/ D1 @% K+ Z0 Q3 Q$ B
* ^% R7 l* G6 ]/ V- }9 j @property' S- h( f4 N. C2 m+ r0 w
def all_dev_users(self):
, a6 w/ h& b5 f+ s+ z6 a+ {6 _ return [user.name for user in self.dev_users.all()]
( B( E1 R+ b: N$ N& A1 m: w4 v5 Y* b: B& `' o: V9 c
def get_related_assets(self):
0 L, V: f% c( `& m assets = self.assets.all()
* ~0 B$ _7 U8 j/ z7 M2 U4 j' _$ n return assets& q* c# Q! s e y2 y& M7 J
, W g/ G+ G4 l( i' x def get_related_assets_ip(self):
1 p# `# X4 f1 @8 ^) l. W+ l0 ^ assets = self.assets.all()
. Z) g. V0 W; R) g4 k6 v( `5 R return [{'ip': asset.ip, 'environment': asset.environment} for asset in assets]9 b8 D4 p) {: T
0 w3 u# m( T! K. z, ~) H# d4 a
@property
- i+ |, R7 B* Z8 ~ def all_assets_ip(self):
1 E' e _" K) `. Z; y$ G* _ return self.get_related_assets_ip(). X# ~$ X; ~8 ?. Z# F
- f+ h/ m: f' _( Z class Meta:
9 j' s6 D- N0 c4 V ordering = ['name']( f' [7 @. k* E! m$ x8 o
2.2 修改asset model4 n0 d% y3 A0 @/ \1 a4 \; P
jumpserver/apps/assets/models/asset.py a, X) X, J t7 Z
4 [/ o/ `( A4 a& z% v7 v; |
4 b5 r& q1 R3 U* [1 n" ~% r- D
9 G+ t* U: \; N* K. l# 增加环境定义+ H2 u1 U; d5 X# s' N1 m4 Y
ENV_CHOICES = (
+ W9 Q5 q5 {* z+ h' J5 E% e$ Y ('DEV', 'dev'),
2 r' d N5 r% U2 S$ ]; { ('TEST', 'test'),
! q& |: G. e% F ('DEMO', 'demo'),9 K0 M/ @5 ~* C* O- m
('TDEMO', 'tdemo'),
8 I# h0 s- L2 k9 m4 l ('PROD', 'prod'),
5 R3 S' i+ |, {& S ('Other', 'other'),, P$ J) O+ a) w7 l/ ~, k$ W0 E r
)
$ b% w8 j9 r4 z' Benvironment = models.CharField(max_length=32, choices=ENV_CHOICES, default='DEV', verbose_name=_('Environment'))
) x6 M! @$ t& v. p" T, i% x, k# Project 和 asset 多对多关系
3 ~- I% o5 M0 q% w# kprojects = models.ManyToManyField(Project, blank=True, verbose_name=_('Projects'), related_name='assets', )
. [/ t( J, S2 A. _2.3 加入全局变量中init
4 {& w' H/ T/ b/ }% ?2 ^7 {jumpserver/apps/assets/models/init.py 末尾增加
3 w1 ]/ h; u _ T8 r; o/ c
- d- ?3 A9 D' g1
- n! b4 Z9 a; pfrom .project import *
, b# J# @7 I# d4 ]2.4 定义DRF的serializers
6 l3 f) }% Y1 d! I5 D2 Cjumpserver/apps/assets/serializers/project.py
: `) {! Z& i- q* c2 A) }& l6 h& q& E) \
& V- _9 g/ v) \+ x1 L
" `$ x) {: U7 ^7 z0 \5 I' b" b- j* M, hfrom rest_framework_bulk import BulkListSerializer, BulkSerializerMixin$ O, a4 C3 Q7 q. E
from rest_framework import serializers
: S2 v* }) ?- @* P; k) w$ Zfrom ..models import Project, Asset
5 p+ o7 ^+ {; l5 Z" C. `7 |& I
# S8 A* U, `% s% B+ g7 B, v
* v; E0 c" Y! b" p6 o) s# D5 wclass ProjectSerializer(serializers.ModelSerializer):2 b# l* z4 b, t* Q n
asset_count = serializers.SerializerMethodField()$ o' K/ d9 x0 q8 \& ?5 }
; I, y6 W' ~- P7 P& H
class Meta:( ?. C9 K6 W0 ~+ E3 F! ]
model = Project
* T0 {$ R C) b$ @6 O! U fields = '__all__'
8 W8 I k; i( U, U! w
$ k1 m' {6 Y% t4 x3 X5 ? @staticmethod
9 b7 z' p" c. M8 R3 O6 C1 n def get_asset_count(obj):6 l1 H9 Y$ P' |3 r$ x# P
return obj.assets.count()/ d. B3 y Z b1 c8 R$ a
& |3 O0 x% k z2 d4 ~( C4 A' a: Z; r1 Y' ` j+ E3 ?
class ProjectAssetsSerializer(serializers.ModelSerializer):9 v9 J1 i0 P z+ s
class Meta:5 N5 m" Z! F4 Y! E/ h! r: p' m
fields = ['id', 'name', 'port']
5 x& q {" H) n q3 @0 M model = Project
3 O' ?: Q- g4 ^7 K, }: M: |! V' d) z
def get_field_names(self, declared_fields, info):! f+ h: f5 U$ o. y" P4 ]
fields = super().get_field_names(declared_fields, info)
+ X* C x4 W0 `: E/ G! O$ Z0 \! } fields.extend([% R# t2 S0 E* r0 G' B
'all_assets_ip'
! N6 d; v( y N( @/ \ j k ])
) p. r( q) A) G return fields7 }6 J7 D% O) P8 i% `" ?
- P+ u( u, e2 C) ]/ u2 {
. U/ r$ L2 u: L# f4 S$ B& Bclass ProjectUpdateSerializer(serializers.ModelSerializer): l6 }- [$ o* X) a
"""update the project, and add or delete the asset to the project"""
$ u; x" m2 z3 l* E assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all())( u9 m' m2 ]: A& _7 d) N
) C" T$ E- K3 I
class Meta:
4 b1 m* C$ X! _% x( @ model = Project# l9 q" [0 y& c
fields = ['id', 'assets']4 o- l6 K; a* I1 W
/ E; ]' t" \. O3 R* _* [
" _; E* X% X* A* j
class AssetUpdateProjectSerializer(serializers.ModelSerializer):7 y$ V, y. l' E q) K! O$ i/ |
projects = serializers.PrimaryKeyRelatedField(many=True, queryset=Project.objects.all())- G8 j5 q8 K6 @& J) F7 a
7 m9 D0 I/ ]/ r; y
class Meta:3 J% B5 l2 N4 o0 i) E! x- l
model = Asset
( a( q' t6 ?: W- S k: i fields = ['id', 'projects']. P6 W! l2 O* R0 s: B3 a0 p2 d! Z
2.5 增加URl支持
! T( o9 t# K8 F6 d6 \8 Q K. E+ I/jumpserver/apps/assets/urls/api_urls.py2 H1 v9 } ^7 n+ Y7 z* }
" P. }6 N# w) s: Q1 ^
/ n, x( {. ^" C3 [" r; ^, L0 A: z$ J
router.register(r'projects', api.ProjectViewSet, 'project')
& O; R+ L# ?! ?, erouter.register(r'projects-assets', api.ProjectAssetsViewSet, 'project-assets-view')
$ }$ R( a0 W1 {: |; ]4 j# \! T3 A( u% R
# urlpatterns 增加:# }( S5 |/ {2 ^$ i6 d& J
path(r'assets/<uuid:pk>/projects/',: c: Z3 v8 ?9 d
api.AssetUpdateProjectApi.as_view(), name='asset-update-project'),- u9 a6 {3 Q3 k! n& W
path(r'projects/<uuid:pk>/assets/',
: \7 o: n9 ?* D1 O# c) b1 b J api.ProjectUpdateApi.as_view(), name='project-update-assets'),5 h. b# K1 f/ @
path('project/<uuid:pk>/assets/',. B: @5 e R3 d, @' h
api.ProjectAssetsListView.as_view(), name='project-assets'),
. H' R, \+ x" d; {API到这里基本写完了,至于前端的一些,都是调用这些api,进行增删改查操作,详情见github
' E B3 ~, q: p
/ l8 ~7 J. j" f/ d3 数据库迁移
4 o8 y: O3 x6 J; k$ j定义了模型以后,需要执行migrate命令进行数据库变更,省了自己执行sql
) R" ?+ G& H# _3 {' d7 Y) T5 v! u7 @0 k
- N4 [# V0 b2 O: e% U; P
# v! S% S v c$ F9 D! \cd jumpserver/utils
, _" t+ n# F0 H2 ?# v9 |bash make_migrations.sh. a; W3 K2 g( j" \! T! R) ~7 a
4 国际化6 _$ Q9 W% w- A# f. k% K0 D6 b( ~
因为我们定义的好多变量都是英文的,这里可以通过国际化手段支持中英文
9 F+ @& R/ J, d6 B) n; m2 y
* N* P; k5 W; c3 K C" Q5 ]4 @" a
) }, H: w9 V, }- K6 ] n* j2 |. `# 先执行
6 ], q( p9 y. r [7 ?cd jumpserver/apps7 v5 A1 y' N* ?9 K3 v5 A q
python manage.py makemessages -l zh
& n! |. Z5 ?0 I# s1 \1 F# 修改jumpserver/apps/locale/zh/LC_MESSAGES/django.po 的中英文对应
: R! j4 a+ R [# N) qvim jumpserver/apps/locale/zh/LC_MESSAGES/django.po( M. a) u! u& E# t: @
# 修改完成后生效,完全生效需要服务重启" q" V( V5 y0 y* L
python manage.py compilemessages
4 i, t# D- D' M( w' L5 Z& I
/ _2 \! Z2 w a" A) N
* G6 @. x) F/ x# [& a$ ?+ m
8 v v; s9 N, X' j |
|