|
|
Jumpserver是全球首款完全开源的堡垒机,是符合 4A 的专业运维审计系统。 http://www.jumpserver.org
6 c# r; t+ U7 C9 l4 G a
! v4 K9 S. L$ w# C Hjms很好用,但是引入公司内部,也会有一些问题,比如资产导入,资产和项目关联,如何和现有cmdb打通等问题。我这里只是抛砖引玉,介绍下jms如何二次开发,做企业定制化版的jumpserver,打通DevOps系统的方法。
5 f G0 d- I5 l: a首先,以增加项目管理 这个功能为例,说说堡垒机如何二次开发的。
. w& u$ J+ }# ]
U% C* {' x# l( o$ b# Q基于版本 Version 1.4.10-2 GPLv2.- ^, M# Y" ]9 Q# E
) i1 b/ ^* v& y! Q# b9 Y6 q$ H
一、概述
& D5 V7 W8 {' D+ Q/ P$ `5 @( T2 rJumpserver用的是python的django框架开发,API 采用了DRF(Django REST framework),基于CBV(Class-based views)开发模式,因此,二次开发也是比较简单的,一般先定义model,然后写api,再使用template渲染,前端基本用datatables较多。& s4 V: H. {3 i- W V
1 |% i4 \' j0 t0 g4 |
实现效果:2 P8 ~+ k6 M' ?
项目管理
6 H, e: e5 J1 v2 y& R% i5 `9 u& R6 L项目详情
9 O9 `' w& u: @8 ^项目详情20 x# k$ e0 j5 C" A
) k. u# a4 Z! v1 e# h! |; H( @
二、实战2 W4 V \7 m: C+ h9 L
项目(project)管理或者说是服务(service)管理,服务和主机是有关系的,我这里一个主机可以部署多个服务,同时一个服务也可以部署在多台主机上,所以是多堆多的关系
1 D; D, d3 P# x* |3 c" c9 F5 X/ I
2.1 定义project model
8 I3 i1 G( u& H+ {/ T# \! tjumpserver/apps/assets/models/project.py1 D, ?* e& Y! q- @
- [1 W, O5 o1 T) M: ]4 d. @0 b+ s" P* G- G8 l
$ k. }) O# X( m2 j9 f% e
#!/usr/bin/env python3 b% q9 W" X. X% S2 ~. I
# -*- coding: utf-8 -*- q1 \1 R" L y( U
2 m. U) C. S9 u0 z; U
from __future__ import unicode_literals2 I4 s7 D5 {- O+ R: U
/ R: b! m2 p+ q( j; o& K" d7 kfrom django.db import models
4 O" _7 e5 j6 B, {* zimport logging. C$ e/ R% p+ j) D
from django.utils.translation import ugettext_lazy as _
0 U5 k, z6 E" E! x' Nimport uuid- B1 c: d! Y$ k! S6 u2 c
from django.core.cache import cache1 u& D1 M1 m* U' J0 N( V% \
& L( e; ~% ]1 P# R
__all__ = ['Project', 'TYPE_CHOICES']
$ }5 q% P- A3 [: C: O# [6 elogger = logging.getLogger(__name__)4 P0 k: d( ]: m& H
5 t9 ^- e" ^) x% dTYPE_CHOICES = (
+ F* r% V" d. E. ]9 o. x ('Java', 'java'),
1 G# l# m3 J: g8 M. y3 C ('React', 'react'), b- L2 Q+ P( G- M' {) G
('Cpp', 'c++'),
# L6 i' G8 h' Z1 b ('Python', 'python'),& A' s R# e6 K1 Q1 _
('Middleware', 'Middleware'),, f- _7 [3 _& [ {
('Other', 'Other'),
; b; b2 ~1 ^0 T1 L5 U4 ` ): t2 J O# @0 _! O( A
2 E7 k$ F1 d' W" f# y9 i% n
. d6 z1 u: ^: M8 |' o1 U, Gclass Project(models.Model):
* O6 M6 d' G* }: ? TYPE_CHOICES =TYPE_CHOICES
+ {! R! N/ Q# d' N& j
( |5 J2 T5 @9 A# h id = models.UUIDField(default=uuid.uuid4, primary_key=True)
2 S# S! X! |! F o0 a* Y name = models.CharField(max_length=64, unique=True, verbose_name=_('Name'))# h% q2 R! w0 }5 E$ V9 E) K
, |0 r( W8 t: X* | parent = models.ForeignKey('self', null=True, blank=True, related_name="children", on_delete=models.SET_NULL)
3 ?4 g! _! Q$ s6 l8 O$ \. @. y; N+ @4 L O
domain_name = models.CharField(max_length=64, blank=True, verbose_name=_('Domain Name'))
. I: f C# g7 C7 n7 f8 r git_address = models.CharField(max_length=128, blank=True, verbose_name=_('Git Address'))
( R* y7 K5 R2 E6 P7 V9 U port = models.IntegerField(blank=True, null=True, verbose_name=_('Open port'))% |' \8 q# ~% V
type = models.CharField(choices=TYPE_CHOICES, max_length=16, blank=True, null=True,- m6 w5 i( s$ ~ E+ z* }6 C6 E
default='Java', verbose_name=_('Project type'), )
1 C" C" d. q- Z dev_users = models.ManyToManyField(
4 S7 \! F9 u7 h- w 'users.User', related_name='users1',
5 p" I2 A* n a: `; A blank=True, verbose_name=_('Dev Users')5 i8 e" b+ k1 {0 U. c/ a4 r7 ^3 O
)% q A* p! x1 ~+ I
sa_users = models.ManyToManyField(
M6 z D( J! q. C1 q 'users.User', related_name='users2',
; d; a, ~$ J {1 |3 N5 F blank=True, verbose_name=_('Sa Users')
; @8 s" K7 X. L" G )7 W/ h' e9 \/ s* e& i6 M
scrum_master = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, verbose_name=_('Scrum Master')), ^3 h9 D/ B- {% j3 f* `
created_by = models.CharField(max_length=32, blank=True, verbose_name=_('Created by'))- M7 c+ Y4 t- T4 f
updated_by = models.CharField(max_length=32, blank=True, verbose_name=_('Updated by'))1 f b/ V9 L1 g. R
date_created = models.DateTimeField(auto_now_add=True, null=True, verbose_name=_('Date created'))0 I8 Z$ ]% n. U0 W
date_updated = models.DateTimeField(auto_now=True, null=True, verbose_name=_('Date updated'))7 R/ ~3 C9 G: k* @' p6 H# L
comment = models.TextField(blank=True, verbose_name=_('Comment'))
% E; q: w0 `8 v- P$ X0 i7 m" q level = models.IntegerField(default=0, verbose_name=_('Level'))
7 v# @2 \' Z+ _1 T7 `
+ c0 C# d9 g1 R. T9 s, V8 X def __unicode__(self):
; B/ {; n" P( z1 t+ x. i( G7 L return self.name) {* u) A0 z# O! d5 y* v
__str__ = __unicode__
0 D, s2 r, r2 S+ y/ T1 v9 ~ G2 H7 W# ^( ^/ y. B5 ?% ?
@property
2 U5 g- c! ]! b7 x3 E def all_sa_users(self):* B" V" k* a( {" x' ~* C R
return [user.name for user in self.sa_users.all()]
/ X, u+ Z' \1 h( l7 s' r. d) @% S5 \& ?6 I4 o) \
@property
: s' l% P" ]5 d3 I) k def all_dev_users(self):: r* B( u1 x3 f# u7 T1 {
return [user.name for user in self.dev_users.all()]' a7 N# @4 w8 g
4 ~& ]& z, ~$ m, V
def get_related_assets(self):9 M$ `7 B: f/ R( O+ W1 q* C+ n
assets = self.assets.all()
5 q- t: N+ L& y0 `( Z return assets6 r: U7 w8 }( i7 X1 K6 j5 M
& G5 W4 v6 m; z9 P1 `
def get_related_assets_ip(self):
! @& I h( r) d3 A: h$ D assets = self.assets.all()
% k6 M% Q* v9 B1 c return [{'ip': asset.ip, 'environment': asset.environment} for asset in assets]
: t4 J) f( g& N1 X/ m- G: m( F q: U& d3 e4 Z9 d
@property7 x, J0 m2 ^: A6 k s$ r
def all_assets_ip(self):, G: E9 h6 `! R, x2 c4 b
return self.get_related_assets_ip()
0 b5 A s/ R5 c( d4 S
! v6 R& F+ `* {+ n+ |' ^* ~ class Meta:
% A# m6 k! j/ W, i! ]# B+ I4 G ordering = ['name']1 [0 _- Y! O7 ]+ I* z
2.2 修改asset model" ~2 R6 j+ f2 z- ~
jumpserver/apps/assets/models/asset.py+ y. W1 i" V$ o: V6 H- f# m( Z" o
- c, W/ [8 d$ S. c7 ^; e7 f/ n' D8 r* F& Y3 l5 \
; t! H/ v( Q( L0 W, F
# 增加环境定义* |3 \& q$ E4 l) C8 P
ENV_CHOICES = (& |- R& v0 `% }+ w
('DEV', 'dev'),5 Z6 z2 H& A$ p6 F- X
('TEST', 'test'),2 f* D9 a2 f& r2 |5 N
('DEMO', 'demo'),
+ I# ^; ]7 Z! _6 X ('TDEMO', 'tdemo'),
! k- E# R9 h- ?& |; N. g ('PROD', 'prod'),) T- b# A- f# m
('Other', 'other'),! ^+ k! K$ p! ^1 P+ _
)2 e) z; U% G L- s- D
environment = models.CharField(max_length=32, choices=ENV_CHOICES, default='DEV', verbose_name=_('Environment'))) @; o: j% s! W! [3 M- B
# Project 和 asset 多对多关系
; q2 e8 P0 Z" W+ R8 k1 x. Rprojects = models.ManyToManyField(Project, blank=True, verbose_name=_('Projects'), related_name='assets', )4 w' Y4 F+ I- s: z0 t- G7 q
2.3 加入全局变量中init
* A% L0 p- ~# K7 y4 g5 J, y) G: qjumpserver/apps/assets/models/init.py 末尾增加
3 j# z/ U1 n9 d8 f: E ^0 A
/ Z! M+ ^9 I- X, O1
9 o2 M- o x8 c. C4 C+ @- |1 efrom .project import *0 e! R0 @4 _( U
2.4 定义DRF的serializers/ \* R3 m" I% L+ K" h
jumpserver/apps/assets/serializers/project.py
! V$ h+ W i& P# O
& T8 B( f' U7 m5 ^# A- s, v% D& R; U- L# H
5 d" ~; f( G$ |; ]8 _" ~0 f( C
from rest_framework_bulk import BulkListSerializer, BulkSerializerMixin
# _5 A0 A; Q( Cfrom rest_framework import serializers) M, n- c% e. S
from ..models import Project, Asset
- O0 Z, u& d5 z, y$ \. g+ @4 B. [; p
* X; }4 ~3 C7 X$ e! _3 u. m
class ProjectSerializer(serializers.ModelSerializer):
( r. o' D' j/ ^" W8 | asset_count = serializers.SerializerMethodField()
4 J% e, t* v3 q: ^2 D# Q5 P
% i7 q( B8 Q( p3 J+ i* q8 f! _. } class Meta:# i n9 w' B3 U# W4 \
model = Project
9 h" E$ f, P" }6 B fields = '__all__'
6 l# b& k- X6 p6 G+ B0 O6 O
% i' d1 o% W! A6 d @staticmethod
% P) m p% d0 `- i, W) q: p def get_asset_count(obj):6 F/ {2 D' \/ ~2 c4 G% R
return obj.assets.count()/ K. ]$ U" q# R5 l0 q0 E% T9 D
) T: c3 `! F6 v$ }, s" j0 c
! e$ D$ f# l! |! g# G8 ^
class ProjectAssetsSerializer(serializers.ModelSerializer):
' D, L$ v7 S; A3 D, n class Meta:
7 z [& Q6 ], `0 q5 I fields = ['id', 'name', 'port']$ O* L2 K- s3 n3 Y
model = Project
1 U5 k5 R' z2 O( H( u4 G( M
, ~7 e! ?1 R, F def get_field_names(self, declared_fields, info):) |; ]0 L/ d' @7 a9 e
fields = super().get_field_names(declared_fields, info)+ s4 h+ _. A& X% I
fields.extend([
: ^. ?1 u$ e* M5 p 'all_assets_ip'
# c# k' j8 o9 ~+ z ])
5 o. W( U: r) r return fields( k; |! p6 Y% y+ ?" m- T+ ]
# x) ~0 W. f2 o! V! Y4 ^+ u6 I- C
, @( `$ ~+ W. R0 v& V
class ProjectUpdateSerializer(serializers.ModelSerializer):
' ]8 A- d( h, Z """update the project, and add or delete the asset to the project"""8 ?2 l) |& N+ Z0 r+ c! n
assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all()). G: }" V" q- K" Z$ G
* }) R1 K9 Q0 i! w3 V' I2 s# Q Z class Meta:: m W0 s! `4 R' v) e
model = Project9 W( q8 X7 ?& ^; `5 {
fields = ['id', 'assets']
$ d2 b; H- q# L* |! e7 \: F5 l! A% f* j& A
% a2 j) l4 b& |$ X5 T; W7 r
class AssetUpdateProjectSerializer(serializers.ModelSerializer):, b, B' M8 v4 T: `
projects = serializers.PrimaryKeyRelatedField(many=True, queryset=Project.objects.all())- t' S+ c! G' N" c; C
3 g; V9 Q7 Q7 r5 n5 {, g class Meta:, u1 f I9 @; l. a' ^6 P: |1 V" L/ h
model = Asset
; ?& N; {( J/ s$ z% ]8 w fields = ['id', 'projects']6 I/ k* S. ?, n i6 L# w
2.5 增加URl支持
) \3 B I2 L. r. X& F/jumpserver/apps/assets/urls/api_urls.py) o' c* ?- t" |$ p
% Q/ Y" i2 G8 G
* M; l# C" o2 [3 r, L
' K/ {$ ~6 J( X+ Rrouter.register(r'projects', api.ProjectViewSet, 'project')
2 B. S6 Y6 C9 y2 q$ lrouter.register(r'projects-assets', api.ProjectAssetsViewSet, 'project-assets-view')
, p) x" f" D$ s; u% a% z
' Q$ W+ G7 ^0 n, y9 I# urlpatterns 增加:
& v& a- q0 C+ q+ { path(r'assets/<uuid:pk>/projects/',. e3 A3 p, k P
api.AssetUpdateProjectApi.as_view(), name='asset-update-project'),
1 X" S- F6 X/ _3 D+ f2 z+ w path(r'projects/<uuid:pk>/assets/',
8 W: F% F$ [/ ^& ~% [% [0 v" z% N api.ProjectUpdateApi.as_view(), name='project-update-assets'),
! o2 V, i& r ? path('project/<uuid:pk>/assets/',
: z3 D- X, j( x; h8 Q/ w. z api.ProjectAssetsListView.as_view(), name='project-assets'),' u1 z: I! H& U- @
API到这里基本写完了,至于前端的一些,都是调用这些api,进行增删改查操作,详情见github
8 b7 x0 G! [2 O$ u: L. b. ]4 K
& }+ L& [' y$ S0 c& L3 数据库迁移
. p0 P+ y9 x" U" b定义了模型以后,需要执行migrate命令进行数据库变更,省了自己执行sql: W$ H# s$ v8 n' [- R4 d$ c5 d0 Q a w
2 t! i9 k& Y7 f, t
) e$ _6 o. P2 n0 F% u# y/ z; a" h. q ]9 a
cd jumpserver/utils
! B+ s! n0 G8 |- ]0 B2 x2 k1 c" Fbash make_migrations.sh
$ n- c' a& I- v& a4 国际化$ T0 N2 {6 q: D {3 m- N( M1 s
因为我们定义的好多变量都是英文的,这里可以通过国际化手段支持中英文
( E( Q- O9 p6 j! `) q* q1 h
9 O3 L1 d5 T4 `5 B
# l b9 [! N7 {, m6 S: ^, S0 _4 R! T. s* w4 a
# 先执行9 I! @' L. w- K1 O: w0 k
cd jumpserver/apps) s/ r/ H y9 X. u# d$ }
python manage.py makemessages -l zh
/ L5 g! k1 h9 V' X# 修改jumpserver/apps/locale/zh/LC_MESSAGES/django.po 的中英文对应
2 q+ [5 Z, S6 m( H, L+ O2 T' ?vim jumpserver/apps/locale/zh/LC_MESSAGES/django.po5 N. r! f) l' `4 w, _) J
# 修改完成后生效,完全生效需要服务重启
. b7 P, E' b. ipython manage.py compilemessages. O" v5 b$ H$ J% }' b, b/ [: w
1 A: @" A H1 b7 |$ J6 |! I
$ ]% z2 ]9 T- q% u3 t1 r! Q* T2 w4 K
|
|