mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-15 16:42:34 +00:00
Compare commits
298 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3153458fce | ||
|
|
4b981fd93c | ||
|
|
6720ecc6e0 | ||
|
|
b0f86e43a6 | ||
|
|
9b0c81333f | ||
|
|
05fc966444 | ||
|
|
b87650038f | ||
|
|
d4f69a7ff8 | ||
|
|
0e1e26c29c | ||
|
|
1b8cdbc4dd | ||
|
|
2a781c228f | ||
|
|
35d6b0f16a | ||
|
|
ca8987fef6 | ||
|
|
b385133071 | ||
|
|
aa78a03efa | ||
|
|
31f8a19392 | ||
|
|
7a528b499a | ||
|
|
1c6ce422cf | ||
|
|
f9cf2ea2e5 | ||
|
|
575b3a617f | ||
|
|
b7362d3f51 | ||
|
|
6ee3860124 | ||
|
|
7e111da529 | ||
|
|
578458f734 | ||
|
|
bd56697d6d | ||
|
|
aad824d127 | ||
|
|
63f828da0b | ||
|
|
7c211b3fb6 | ||
|
|
3881edd2ba | ||
|
|
b882b12d04 | ||
|
|
addd2e7d1c | ||
|
|
ad6d2e1cd7 | ||
|
|
5f07271afa | ||
|
|
efdcd4c708 | ||
|
|
b62763bca3 | ||
|
|
e95da730f2 | ||
|
|
43fa3f420a | ||
|
|
0311446384 | ||
|
|
f7030e4fee | ||
|
|
fce8cc375f | ||
|
|
920199c6df | ||
|
|
d09eb3c4fa | ||
|
|
6e8affcdd6 | ||
|
|
0b3a7bb020 | ||
|
|
647736f4e3 | ||
|
|
cbc09d84df | ||
|
|
4c957dd03b | ||
|
|
d34b65890f | ||
|
|
b53968ac00 | ||
|
|
f2ccb15101 | ||
|
|
db5bf046fc | ||
|
|
59c87483e6 | ||
|
|
26420b78f8 | ||
|
|
e47bdc093e | ||
|
|
3dde80a60a | ||
|
|
e373a79d63 | ||
|
|
744a5cd0e3 | ||
|
|
37ca4a46ee | ||
|
|
0dc9214f98 | ||
|
|
513508654b | ||
|
|
ef2b12fa0f | ||
|
|
4e719ecacd | ||
|
|
755a124b50 | ||
|
|
d6888776e7 | ||
|
|
29e233e715 | ||
|
|
99c3696d96 | ||
|
|
ed6de83e8c | ||
|
|
134f1a440c | ||
|
|
7da82242fe | ||
|
|
2fd50d2425 | ||
|
|
41a3e89248 | ||
|
|
b125297c37 | ||
|
|
24255b69ee | ||
|
|
3bb51b39c4 | ||
|
|
b54da7d3b3 | ||
|
|
534af0abf0 | ||
|
|
8b0073333b | ||
|
|
d8af2274f4 | ||
|
|
3dd828d703 | ||
|
|
fa6b4a5b63 | ||
|
|
8bd86c77f9 | ||
|
|
3828e89cf8 | ||
|
|
e531b040ef | ||
|
|
3eee84a34e | ||
|
|
ab29df5991 | ||
|
|
b042f00688 | ||
|
|
5beebaf51c | ||
|
|
50f075cc7e | ||
|
|
e997236159 | ||
|
|
c8b1d892e3 | ||
|
|
9cb9e7328b | ||
|
|
85129da942 | ||
|
|
1cb00b1db4 | ||
|
|
c3798bfa95 | ||
|
|
1d280599ae | ||
|
|
ee8d7cdcac | ||
|
|
1b4114fd5f | ||
|
|
3c6c476f2e | ||
|
|
f19e3fedbd | ||
|
|
542e64278f | ||
|
|
cd76294e81 | ||
|
|
4f9158b2ad | ||
|
|
e319f20296 | ||
|
|
b00f3a851c | ||
|
|
ab529fd22c | ||
|
|
c2784c44ad | ||
|
|
512e727ac6 | ||
|
|
2dd0154967 | ||
|
|
f55869a449 | ||
|
|
b6f3c23787 | ||
|
|
6982ab1efc | ||
|
|
db4d841bb0 | ||
|
|
ef91ebb468 | ||
|
|
6264319c51 | ||
|
|
1417abecfb | ||
|
|
bd548b3fe2 | ||
|
|
94cef9ea6e | ||
|
|
a338613b5a | ||
|
|
0d833a966c | ||
|
|
76b6489636 | ||
|
|
763fe778d5 | ||
|
|
cf1dc79c68 | ||
|
|
7973239424 | ||
|
|
1baacd0b2c | ||
|
|
054d385ffc | ||
|
|
50d3a4906a | ||
|
|
c8b7008d42 | ||
|
|
e94520a3fd | ||
|
|
55e8e34226 | ||
|
|
8755ece633 | ||
|
|
c545e2a3aa | ||
|
|
1068662ab1 | ||
|
|
75141741a1 | ||
|
|
9da507bb62 | ||
|
|
160293365a | ||
|
|
7a19007aba | ||
|
|
f866b93f96 | ||
|
|
b9e64747ac | ||
|
|
25a473dc99 | ||
|
|
e3bf015aa9 | ||
|
|
6d3d4a08af | ||
|
|
9554de4ea6 | ||
|
|
6157ff7b7d | ||
|
|
774fd176fd | ||
|
|
b489db8054 | ||
|
|
6b9fa6e01f | ||
|
|
9b59954393 | ||
|
|
ecaf19563f | ||
|
|
c431e96eaf | ||
|
|
d86f241450 | ||
|
|
3252db31fe | ||
|
|
dac118dd26 | ||
|
|
181eb621c0 | ||
|
|
828582333d | ||
|
|
657f7f822b | ||
|
|
93627e4f9d | ||
|
|
2adb2519fa | ||
|
|
56373e362b | ||
|
|
32ec48ac14 | ||
|
|
b3a0d81740 | ||
|
|
2b160fbbc2 | ||
|
|
60fcf5fcd3 | ||
|
|
7c2e50435d | ||
|
|
57f91d0973 | ||
|
|
49c033e003 | ||
|
|
6476a8fee8 | ||
|
|
c10db2ab0f | ||
|
|
647beffc01 | ||
|
|
ac0c6ef3d5 | ||
|
|
e13741827d | ||
|
|
29caf0154e | ||
|
|
fbdcc437e6 | ||
|
|
b38e5df1aa | ||
|
|
0a39ba0a75 | ||
|
|
c56e1bdbbe | ||
|
|
6b00ba271f | ||
|
|
bddb1de2f8 | ||
|
|
32ae77c42d | ||
|
|
3b1701b1aa | ||
|
|
3b9bcc719e | ||
|
|
8e6aa4524d | ||
|
|
cea63e6083 | ||
|
|
5d2d8ca487 | ||
|
|
81146f44f7 | ||
|
|
9adaa27f6c | ||
|
|
01c565f93f | ||
|
|
cb97afffab | ||
|
|
1b55bf1670 | ||
|
|
b1c68165bb | ||
|
|
5d3e633e83 | ||
|
|
c863bf63b1 | ||
|
|
c71a6ae4ba | ||
|
|
38e3d9de8b | ||
|
|
0c73acd4b9 | ||
|
|
581a5c73a6 | ||
|
|
e1ed1d7c4c | ||
|
|
805e7d1d5f | ||
|
|
1957c2983b | ||
|
|
6b1ceae6c5 | ||
|
|
2a5c41dfaf | ||
|
|
7a38c9136e | ||
|
|
9a3fdf76fc | ||
|
|
136db61011 | ||
|
|
0d338f80c5 | ||
|
|
bd3909ad27 | ||
|
|
96399f8315 | ||
|
|
4e90d17484 | ||
|
|
13de75c41f | ||
|
|
a77ebc5fee | ||
|
|
99ce82a6a0 | ||
|
|
ec95d25704 | ||
|
|
7c6e83d124 | ||
|
|
ad5e88f1e3 | ||
|
|
b1e958d806 | ||
|
|
8506ae9edd | ||
|
|
ceb2a9bb17 | ||
|
|
8d83c953d3 | ||
|
|
9825f9fbd2 | ||
|
|
41b2ce06a8 | ||
|
|
920cfdac5c | ||
|
|
8abf7876cc | ||
|
|
2e625f2c33 | ||
|
|
88037b2038 | ||
|
|
457021040a | ||
|
|
4887b21d35 | ||
|
|
03a66fd563 | ||
|
|
ef656a8dfd | ||
|
|
5e45129e32 | ||
|
|
ea64b01da6 | ||
|
|
c3b863c2be | ||
|
|
6a7896b712 | ||
|
|
83c1f8e4d3 | ||
|
|
9d3fdd37a3 | ||
|
|
419195895e | ||
|
|
c92188887d | ||
|
|
dcfc4e6e7b | ||
|
|
836adab5d0 | ||
|
|
e93227a53c | ||
|
|
d6f6bb9c1b | ||
|
|
85825165fc | ||
|
|
66047c7926 | ||
|
|
456bcd2d3f | ||
|
|
259f68a806 | ||
|
|
4e6231ab19 | ||
|
|
d7bbfdcce6 | ||
|
|
a0cc9e5db5 | ||
|
|
ea6cd853de | ||
|
|
53a388a7e0 | ||
|
|
13b1938efb | ||
|
|
6677985e4a | ||
|
|
cfa1034161 | ||
|
|
815973fb63 | ||
|
|
92d369aaca | ||
|
|
281a2d9679 | ||
|
|
e9f4615caa | ||
|
|
c0d2efa72a | ||
|
|
247f4d5c19 | ||
|
|
29c29b17d4 | ||
|
|
5608f7d20d | ||
|
|
aa8ae36255 | ||
|
|
2292e6f2eb | ||
|
|
bf82a1c721 | ||
|
|
8ef84bbc03 | ||
|
|
e36d51cc0b | ||
|
|
5c1d0238e1 | ||
|
|
c6befe4c4b | ||
|
|
5a57c296a1 | ||
|
|
34ddfd24be | ||
|
|
39051ef0fd | ||
|
|
ddd813241c | ||
|
|
60f7cbef9a | ||
|
|
4adc981a21 | ||
|
|
c42913c15e | ||
|
|
bb6d60b46d | ||
|
|
afe7f03c16 | ||
|
|
ba8d3be9a6 | ||
|
|
d14d8869ac | ||
|
|
2f7391efc3 | ||
|
|
75fa96b29c | ||
|
|
c56ab9bc1e | ||
|
|
443e492fd4 | ||
|
|
b8c223d525 | ||
|
|
a509afe24b | ||
|
|
9654add528 | ||
|
|
d0a9409078 | ||
|
|
5836583490 | ||
|
|
57d689bee6 | ||
|
|
8a3fb6bd4d | ||
|
|
78bd3f581a | ||
|
|
d07c476507 | ||
|
|
50d196eda4 | ||
|
|
823d9af91d | ||
|
|
3731123369 | ||
|
|
1a68c4b44a | ||
|
|
0f79006b59 | ||
|
|
c95ad5a31c | ||
|
|
e25a96d359 | ||
|
|
04284adc87 |
@@ -8,3 +8,4 @@ celerybeat.pid
|
||||
.vagrant/
|
||||
apps/xpack/.git
|
||||
.history/
|
||||
.idea
|
||||
72
.github/workflows/build-base-image.yml
vendored
Normal file
72
.github/workflows/build-base-image.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
name: Build and Push Base Image
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'dev'
|
||||
- 'v*'
|
||||
paths:
|
||||
- poetry.lock
|
||||
- pyproject.toml
|
||||
- Dockerfile-base
|
||||
- package.json
|
||||
- go.mod
|
||||
- yarn.lock
|
||||
- pom.xml
|
||||
- install_deps.sh
|
||||
- utils/clean_site_packages.sh
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Extract date
|
||||
id: vars
|
||||
run: echo "IMAGE_TAG=$(date +'%Y%m%d_%H%M%S')" >> $GITHUB_ENV
|
||||
|
||||
- name: Extract repository name
|
||||
id: repo
|
||||
run: echo "REPO=$(basename ${{ github.repository }})" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push multi-arch image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
file: Dockerfile-base
|
||||
tags: jumpserver/core-base:${{ env.IMAGE_TAG }}
|
||||
|
||||
- name: Update Dockerfile
|
||||
run: |
|
||||
sed -i 's|-base:.* AS stage-build|-base:${{ env.IMAGE_TAG }} AS stage-build|' Dockerfile
|
||||
|
||||
- name: Commit changes
|
||||
run: |
|
||||
git config --global user.name 'github-actions[bot]'
|
||||
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
git add Dockerfile
|
||||
git commit -m "perf: Update Dockerfile with new base image tag"
|
||||
git push origin ${{ github.event.pull_request.head.ref }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
31
.github/workflows/check-compilemessages.yml
vendored
Normal file
31
.github/workflows/check-compilemessages.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Check I18n files CompileMessages
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'dev'
|
||||
paths:
|
||||
- 'apps/i18n/core/**/*.po'
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
jobs:
|
||||
compile-messages-check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and check compilemessages
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
push: false
|
||||
file: Dockerfile
|
||||
target: stage-build
|
||||
tags: jumpserver/core:stage-build
|
||||
@@ -1,3 +1,4 @@
|
||||
[settings]
|
||||
line_length=120
|
||||
known_first_party=common,users,assets,perms,authentication,jumpserver,notification,ops,orgs,rbac,settings,terminal,tickets
|
||||
|
||||
|
||||
103
Dockerfile
103
Dockerfile
@@ -1,101 +1,25 @@
|
||||
FROM debian:bullseye-slim as stage-1
|
||||
ARG TARGETARCH
|
||||
|
||||
ARG DEPENDENCIES=" \
|
||||
ca-certificates \
|
||||
wget"
|
||||
|
||||
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core \
|
||||
set -ex \
|
||||
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache \
|
||||
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
||||
&& echo "no" | dpkg-reconfigure dash
|
||||
|
||||
WORKDIR /opt
|
||||
|
||||
ARG CHECK_VERSION=v1.0.2
|
||||
RUN set -ex \
|
||||
&& wget https://github.com/jumpserver-dev/healthcheck/releases/download/${CHECK_VERSION}/check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
|
||||
&& tar -xf check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
|
||||
&& mv check /usr/local/bin/ \
|
||||
&& chown root:root /usr/local/bin/check \
|
||||
&& chmod 755 /usr/local/bin/check \
|
||||
&& rm -f check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz
|
||||
|
||||
ARG RECEPTOR_VERSION=v1.4.5
|
||||
RUN set -ex \
|
||||
&& wget -O /opt/receptor.tar.gz https://github.com/ansible/receptor/releases/download/${RECEPTOR_VERSION}/receptor_${RECEPTOR_VERSION/v/}_linux_${TARGETARCH}.tar.gz \
|
||||
&& tar -xf /opt/receptor.tar.gz -C /usr/local/bin/ \
|
||||
&& chown root:root /usr/local/bin/receptor \
|
||||
&& chmod 755 /usr/local/bin/receptor \
|
||||
&& rm -f /opt/receptor.tar.gz
|
||||
FROM jumpserver/core-base:20240924_031841 AS stage-build
|
||||
|
||||
ARG VERSION
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
ADD . .
|
||||
|
||||
RUN echo > /opt/jumpserver/config.yml \
|
||||
&& \
|
||||
if [ -n "${VERSION}" ]; then \
|
||||
sed -i "s@VERSION = .*@VERSION = '${VERSION}'@g" apps/jumpserver/const.py; \
|
||||
fi
|
||||
|
||||
FROM python:3.11-slim-bullseye as stage-2
|
||||
ARG TARGETARCH
|
||||
|
||||
ARG BUILD_DEPENDENCIES=" \
|
||||
g++ \
|
||||
make \
|
||||
pkg-config"
|
||||
|
||||
ARG DEPENDENCIES=" \
|
||||
default-libmysqlclient-dev \
|
||||
freetds-dev \
|
||||
gettext \
|
||||
libkrb5-dev \
|
||||
libldap2-dev \
|
||||
libsasl2-dev"
|
||||
|
||||
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core \
|
||||
set -ex \
|
||||
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache \
|
||||
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
||||
&& echo "no" | dpkg-reconfigure dash
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
ARG PIP_MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
RUN --mount=type=cache,target=/root/.cache,sharing=locked,id=core \
|
||||
--mount=type=bind,source=poetry.lock,target=poetry.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
set -ex \
|
||||
&& python3 -m venv /opt/py3 \
|
||||
&& pip install poetry -i ${PIP_MIRROR} \
|
||||
&& poetry config virtualenvs.create false \
|
||||
&& . /opt/py3/bin/activate \
|
||||
&& poetry install --only main
|
||||
|
||||
COPY --from=stage-1 /opt/jumpserver /opt/jumpserver
|
||||
|
||||
RUN set -ex \
|
||||
&& export SECRET_KEY=$(head -c100 < /dev/urandom | base64 | tr -dc A-Za-z0-9 | head -c 48) \
|
||||
&& . /opt/py3/bin/activate \
|
||||
&& cd apps \
|
||||
&& python manage.py compilemessages
|
||||
|
||||
|
||||
FROM python:3.11-slim-bullseye
|
||||
ARG TARGETARCH
|
||||
ENV LANG=en_US.UTF-8 \
|
||||
PATH=/opt/py3/bin:$PATH
|
||||
|
||||
@@ -110,32 +34,27 @@ ARG TOOLS=" \
|
||||
sshpass \
|
||||
bubblewrap"
|
||||
|
||||
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core \
|
||||
set -ex \
|
||||
ARG APT_MIRROR=http://deb.debian.org
|
||||
RUN set -ex \
|
||||
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache \
|
||||
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& apt-get update \
|
||||
&& apt-get update > /dev/null \
|
||||
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${TOOLS} \
|
||||
&& apt-get clean \
|
||||
&& mkdir -p /root/.ssh/ \
|
||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \
|
||||
&& echo "no" | dpkg-reconfigure dash \
|
||||
&& sed -i "s@# export @export @g" ~/.bashrc \
|
||||
&& sed -i "s@# alias @alias @g" ~/.bashrc
|
||||
|
||||
COPY --from=stage-2 /opt /opt
|
||||
COPY --from=stage-1 /usr/local/bin /usr/local/bin
|
||||
COPY --from=stage-1 /opt/jumpserver/apps/libs/ansible/ansible.cfg /etc/ansible/
|
||||
COPY --from=stage-build /opt /opt
|
||||
COPY --from=stage-build /usr/local/bin /usr/local/bin
|
||||
COPY --from=stage-build /opt/jumpserver/apps/libs/ansible/ansible.cfg /etc/ansible/
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
VOLUME /opt/jumpserver/data
|
||||
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
|
||||
60
Dockerfile-base
Normal file
60
Dockerfile-base
Normal file
@@ -0,0 +1,60 @@
|
||||
FROM python:3.11-slim-bullseye
|
||||
ARG TARGETARCH
|
||||
|
||||
# Install APT dependencies
|
||||
ARG DEPENDENCIES=" \
|
||||
ca-certificates \
|
||||
wget \
|
||||
g++ \
|
||||
make \
|
||||
pkg-config \
|
||||
default-libmysqlclient-dev \
|
||||
freetds-dev \
|
||||
gettext \
|
||||
libkrb5-dev \
|
||||
libldap2-dev \
|
||||
libsasl2-dev"
|
||||
|
||||
|
||||
ARG APT_MIRROR=http://deb.debian.org
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core \
|
||||
set -ex \
|
||||
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
|
||||
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||
&& apt-get update > /dev/null \
|
||||
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
||||
&& echo "no" | dpkg-reconfigure dash
|
||||
|
||||
|
||||
# Install bin tools
|
||||
ARG CHECK_VERSION=v1.0.3
|
||||
RUN set -ex \
|
||||
&& wget https://github.com/jumpserver-dev/healthcheck/releases/download/${CHECK_VERSION}/check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
|
||||
&& tar -xf check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
|
||||
&& mv check /usr/local/bin/ \
|
||||
&& chown root:root /usr/local/bin/check \
|
||||
&& chmod 755 /usr/local/bin/check \
|
||||
&& rm -f check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz
|
||||
|
||||
|
||||
# Install Python dependencies
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
ARG PIP_MIRROR=https://pypi.org/simple
|
||||
ENV ANSIBLE_COLLECTIONS_PATHS=/opt/py3/lib/python3.11/site-packages/ansible_collections
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache,sharing=locked,id=core \
|
||||
--mount=type=bind,source=poetry.lock,target=poetry.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
--mount=type=bind,source=utils/clean_site_packages.sh,target=clean_site_packages.sh \
|
||||
--mount=type=bind,source=requirements/collections.yml,target=collections.yml \
|
||||
set -ex \
|
||||
&& python3 -m venv /opt/py3 \
|
||||
&& pip install poetry -i ${PIP_MIRROR} \
|
||||
&& poetry config virtualenvs.create false \
|
||||
&& . /opt/py3/bin/activate \
|
||||
&& poetry install --only main \
|
||||
&& ansible-galaxy collection install -r collections.yml --force --ignore-certs \
|
||||
&& bash clean_site_packages.sh
|
||||
@@ -1,38 +1,12 @@
|
||||
ARG VERSION
|
||||
ARG VERSION=dev
|
||||
|
||||
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} as build-xpack
|
||||
FROM python:3.11-slim-bullseye as build-core
|
||||
ARG BUILD_DEPENDENCIES=" \
|
||||
g++"
|
||||
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} AS build-xpack
|
||||
FROM jumpserver/core:${VERSION}-ce
|
||||
|
||||
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core \
|
||||
set -ex \
|
||||
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache \
|
||||
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
|
||||
&& echo "no" | dpkg-reconfigure dash
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
ARG PIP_MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
RUN --mount=type=cache,target=/root/.cache,sharing=locked,id=core \
|
||||
--mount=type=bind,source=poetry.lock,target=/opt/jumpserver/poetry.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=/opt/jumpserver/pyproject.toml \
|
||||
set -ex \
|
||||
&& python3 -m venv /opt/py3 \
|
||||
&& pip install poetry -i ${PIP_MIRROR} \
|
||||
&& poetry config virtualenvs.create false \
|
||||
&& . /opt/py3/bin/activate \
|
||||
&& poetry install --only xpack
|
||||
|
||||
FROM registry.fit2cloud.com/jumpserver/core:${VERSION}-ce
|
||||
ARG TARGETARCH
|
||||
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack
|
||||
|
||||
ARG TOOLS=" \
|
||||
g++ \
|
||||
curl \
|
||||
iputils-ping \
|
||||
netcat-openbsd \
|
||||
@@ -41,12 +15,21 @@ ARG TOOLS=" \
|
||||
vim \
|
||||
wget"
|
||||
|
||||
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core \
|
||||
set -ex \
|
||||
ARG APT_MIRROR=http://deb.debian.org
|
||||
RUN set -ex \
|
||||
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
|
||||
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install --no-install-recommends ${TOOLS}
|
||||
&& apt-get -y install --no-install-recommends ${TOOLS} \
|
||||
&& echo "no" | dpkg-reconfigure dash
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
ARG PIP_MIRROR=https://pypi.org/simple
|
||||
COPY poetry.lock pyproject.toml ./
|
||||
RUN set -ex \
|
||||
&& . /opt/py3/bin/activate \
|
||||
&& pip install poetry -i ${PIP_MIRROR} \
|
||||
&& poetry install --only xpack
|
||||
|
||||
COPY --from=build-core /opt/py3 /opt/py3
|
||||
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack
|
||||
43
README.md
43
README.md
@@ -10,15 +10,29 @@
|
||||
[![][github-release-shield]][github-release-link]
|
||||
[![][github-stars-shield]][github-stars-link]
|
||||
|
||||
**English** · [简体中文](./README.zh-CN.md) · [Documents][docs-link] · [Report Bug][github-issues-link] · [Request Feature][github-issues-link]
|
||||
**English** · [简体中文](./README.zh-CN.md)
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
## What is JumpServer?
|
||||
|
||||
JumpServer is an open-source Privileged Access Management (PAM) tool that provides DevOps and IT teams with on-demand and secure access to SSH, RDP, Kubernetes, Database and Remote App endpoints through a web browser.
|
||||
JumpServer is an open-source Privileged Access Management (PAM) tool that provides DevOps and IT teams with on-demand and secure access to SSH, RDP, Kubernetes, Database and RemoteApp endpoints through a web browser.
|
||||
|
||||

|
||||

|
||||
|
||||
## Quickstart
|
||||
|
||||
Prepare a clean Linux Server ( 64 bit, >= 4c8g )
|
||||
|
||||
```sh
|
||||
curl -sSL https://github.com/jumpserver/jumpserver/releases/latest/download/quick_start.sh | bash
|
||||
```
|
||||
|
||||
Access JumpServer in your browser at `http://your-jumpserver-ip/`
|
||||
- Username: `admin`
|
||||
- Password: `ChangeMe`
|
||||
|
||||
[](https://www.youtube.com/watch?v=UlGYRbKrpgY "JumpServer Quickstart")
|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -43,15 +57,6 @@ JumpServer is an open-source Privileged Access Management (PAM) tool that provid
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Quick Start
|
||||
Prepare a clean Linux Server ( 64bit, >= 4c8g )
|
||||
|
||||
```
|
||||
curl -sSL https://github.com/jumpserver/jumpserver/releases/latest/download/quick_start.sh | bash
|
||||
```
|
||||
|
||||
Access JumpServer in your browser at `http://your-ip/`, `admin`/`admin`
|
||||
|
||||
## Components
|
||||
|
||||
JumpServer consists of multiple key components, which collectively form the functional framework of JumpServer, providing users with comprehensive capabilities for operations management and security control.
|
||||
@@ -63,12 +68,10 @@ JumpServer consists of multiple key components, which collectively form the func
|
||||
| [KoKo](https://github.com/jumpserver/koko) | <a href="https://github.com/jumpserver/koko/releases"><img alt="Koko release" src="https://img.shields.io/github/release/jumpserver/koko.svg" /></a> | JumpServer Character Protocol Connector |
|
||||
| [Lion](https://github.com/jumpserver/lion) | <a href="https://github.com/jumpserver/lion/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion.svg" /></a> | JumpServer Graphical Protocol Connector |
|
||||
| [Chen](https://github.com/jumpserver/chen) | <a href="https://github.com/jumpserver/chen/releases"><img alt="Chen release" src="https://img.shields.io/github/release/jumpserver/chen.svg" /> | JumpServer Web DB |
|
||||
| [Razor](https://github.com/jumpserver/razor) | <img alt="Chen" src="https://img.shields.io/badge/release-private-red" /> | JumpServer RDP Proxy Connector |
|
||||
| [Tinker](https://github.com/jumpserver/tinker) | <img alt="Tinker" src="https://img.shields.io/badge/release-private-red" /> | JumpServer Remote Application Connector (Windows) |
|
||||
| [Panda](https://github.com/jumpserver/Panda) | <img alt="Panda" src="https://img.shields.io/badge/release-private-red" /> | JumpServer Remote Application Connector (Linux) |
|
||||
| [Magnus](https://github.com/jumpserver/magnus) | <img alt="Magnus" src="https://img.shields.io/badge/release-private-red" /> | JumpServer Database Proxy Connector |
|
||||
|
||||
|
||||
| [Razor](https://github.com/jumpserver/razor) | <img alt="Chen" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE RDP Proxy Connector |
|
||||
| [Tinker](https://github.com/jumpserver/tinker) | <img alt="Tinker" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Windows) |
|
||||
| [Panda](https://github.com/jumpserver/Panda) | <img alt="Panda" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Linux) |
|
||||
| [Magnus](https://github.com/jumpserver/magnus) | <img alt="Magnus" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Database Proxy Connector |
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -91,8 +94,8 @@ https://www.gnu.org/licenses/gpl-3.0.html
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an " AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
<!-- JumpServer official link -->
|
||||
[docs-link]: https://en-docs.jumpserver.org/
|
||||
[discord-link]: https://discord.com/invite/jcM5tKWJ
|
||||
[docs-link]: https://jumpserver.com/docs
|
||||
[discord-link]: https://discord.com/invite/W6vYXmAQG2
|
||||
[contributing-link]: https://github.com/jumpserver/jumpserver/blob/dev/CONTRIBUTING.md
|
||||
|
||||
<!-- JumpServer Other link-->
|
||||
|
||||
@@ -12,13 +12,13 @@
|
||||
|
||||
|
||||
<p align="center">
|
||||
9 年时间,倾情投入,用心做好一款开源堡垒机。
|
||||
10 年时间,倾情投入,用心做好一款开源堡垒机。
|
||||
</p>
|
||||
|
||||
------------------------------
|
||||
JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。
|
||||
## JumpServer 是什么?
|
||||
|
||||
JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型的资产,包括:
|
||||
JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型的资产,包括:
|
||||
|
||||
- **SSH**: Linux / Unix / 网络设备 等;
|
||||
- **Windows**: Web 方式连接 / 原生 RDP 连接;
|
||||
@@ -38,6 +38,13 @@ JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型
|
||||
- **多租户**: 一套系统,多个子公司或部门同时使用;
|
||||
- **云端存储**: 审计录像云端存储,永不丢失;
|
||||
|
||||
## 快速开始
|
||||
|
||||
- [快速入门](https://docs.jumpserver.org/zh/v3/quick_start/)
|
||||
- [产品文档](https://docs.jumpserver.org)
|
||||
- [在线学习](https://edu.fit2cloud.com/page/2635362)
|
||||
- [知识库](https://kb.fit2cloud.com/categories/jumpserver)
|
||||
|
||||
## UI 展示
|
||||
|
||||

|
||||
@@ -52,13 +59,6 @@ JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型
|
||||
| 请勿修改体验环境用户的密码! |
|
||||
| 请勿在环境中添加业务生产环境地址、用户名密码等敏感信息! |
|
||||
|
||||
## 快速开始
|
||||
|
||||
- [快速入门](https://docs.jumpserver.org/zh/v3/quick_start/)
|
||||
- [产品文档](https://docs.jumpserver.org)
|
||||
- [在线学习](https://edu.fit2cloud.com/page/2635362)
|
||||
- [知识库](https://kb.fit2cloud.com/categories/jumpserver)
|
||||
|
||||
## 案例研究
|
||||
|
||||
- [腾讯音乐娱乐集团:基于JumpServer的安全运维审计解决方案](https://blog.fit2cloud.com/?p=a04cdf0d-6704-4d18-9b40-9180baecd0e2)
|
||||
@@ -81,28 +81,24 @@ JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型
|
||||
|
||||
您也可以到我们的 [社区论坛](https://bbs.fit2cloud.com/c/js/5) 当中进行交流沟通。
|
||||
|
||||
### 参与贡献
|
||||
## 参与贡献
|
||||
|
||||
欢迎提交 PR 参与贡献。 参考 [CONTRIBUTING.md](https://github.com/jumpserver/jumpserver/blob/dev/CONTRIBUTING.md)
|
||||
|
||||
## 组件项目
|
||||
|
||||
| 项目 | 状态 | 描述 |
|
||||
|--------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|
|
||||
| [Lina](https://github.com/jumpserver/lina) | <a href="https://github.com/jumpserver/lina/releases"><img alt="Lina release" src="https://img.shields.io/github/release/jumpserver/lina.svg" /></a> | JumpServer Web UI 项目 |
|
||||
| [Luna](https://github.com/jumpserver/luna) | <a href="https://github.com/jumpserver/luna/releases"><img alt="Luna release" src="https://img.shields.io/github/release/jumpserver/luna.svg" /></a> | JumpServer Web Terminal 项目 |
|
||||
| [KoKo](https://github.com/jumpserver/koko) | <a href="https://github.com/jumpserver/koko/releases"><img alt="Koko release" src="https://img.shields.io/github/release/jumpserver/koko.svg" /></a> | JumpServer 字符协议 Connector 项目 |
|
||||
| [Lion](https://github.com/jumpserver/lion-release) | <a href="https://github.com/jumpserver/lion-release/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion-release.svg" /></a> | JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/) |
|
||||
| [Razor](https://github.com/jumpserver/razor) | <img alt="Chen" src="https://img.shields.io/badge/release-私有发布-red" /> | JumpServer RDP 代理 Connector 项目 |
|
||||
| [Tinker](https://github.com/jumpserver/tinker) | <img alt="Tinker" src="https://img.shields.io/badge/release-私有发布-red" /> | JumpServer 远程应用 Connector 项目 (Windows) |
|
||||
| [Panda](https://github.com/jumpserver/Panda) | <img alt="Panda" src="https://img.shields.io/badge/release-私有发布-red" /> | JumpServer 远程应用 Connector 项目 (Linux) |
|
||||
| [Magnus](https://github.com/jumpserver/magnus-release) | <a href="https://github.com/jumpserver/magnus-release/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/magnus-release.svg" /> | JumpServer 数据库代理 Connector 项目 |
|
||||
| [Chen](https://github.com/jumpserver/chen-release) | <a href="https://github.com/jumpserver/chen-release/releases"><img alt="Chen release" src="https://img.shields.io/github/release/jumpserver/chen-release.svg" /> | JumpServer Web DB 项目,替代原来的 OmniDB |
|
||||
| [Kael](https://github.com/jumpserver/kael) | <a href="https://github.com/jumpserver/kael/releases"><img alt="Kael release" src="https://img.shields.io/github/release/jumpserver/kael.svg" /> | JumpServer 连接 GPT 资产的组件项目 |
|
||||
| [Wisp](https://github.com/jumpserver/wisp) | <a href="https://github.com/jumpserver/wisp/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/wisp.svg" /> | JumpServer 各系统终端组件和 Core API 通信的组件项目 |
|
||||
| [Clients](https://github.com/jumpserver/clients) | <a href="https://github.com/jumpserver/clients/releases"><img alt="Clients release" src="https://img.shields.io/github/release/jumpserver/clients.svg" /> | JumpServer 客户端 项目 |
|
||||
| [Installer](https://github.com/jumpserver/installer) | <a href="https://github.com/jumpserver/installer/releases"><img alt="Installer release" src="https://img.shields.io/github/release/jumpserver/installer.svg" /> | JumpServer 安装包 项目 |
|
||||
|
||||
| Project | Status | Description |
|
||||
|--------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------|
|
||||
| [Lina](https://github.com/jumpserver/lina) | <a href="https://github.com/jumpserver/lina/releases"><img alt="Lina release" src="https://img.shields.io/github/release/jumpserver/lina.svg" /></a> | JumpServer Web UI |
|
||||
| [Luna](https://github.com/jumpserver/luna) | <a href="https://github.com/jumpserver/luna/releases"><img alt="Luna release" src="https://img.shields.io/github/release/jumpserver/luna.svg" /></a> | JumpServer Web Terminal |
|
||||
| [KoKo](https://github.com/jumpserver/koko) | <a href="https://github.com/jumpserver/koko/releases"><img alt="Koko release" src="https://img.shields.io/github/release/jumpserver/koko.svg" /></a> | JumpServer Character Protocol Connector |
|
||||
| [Lion](https://github.com/jumpserver/lion) | <a href="https://github.com/jumpserver/lion/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion.svg" /></a> | JumpServer Graphical Protocol Connector |
|
||||
| [Chen](https://github.com/jumpserver/chen) | <a href="https://github.com/jumpserver/chen/releases"><img alt="Chen release" src="https://img.shields.io/github/release/jumpserver/chen.svg" /> | JumpServer Web DB |
|
||||
| [Razor](https://github.com/jumpserver/razor) | <img alt="Chen" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE RDP Proxy Connector |
|
||||
| [Tinker](https://github.com/jumpserver/tinker) | <img alt="Tinker" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Windows) |
|
||||
| [Panda](https://github.com/jumpserver/Panda) | <img alt="Panda" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Linux) |
|
||||
| [Magnus](https://github.com/jumpserver/magnus) | <img alt="Magnus" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Database Proxy Connector |
|
||||
## 安全说明
|
||||
|
||||
JumpServer是一款安全产品,请参考 [基本安全建议](https://docs.jumpserver.org/zh/master/install/install_security/)
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
db_name: "{{ jms_asset.spec_info.db_name }}"
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Test MySQL connection
|
||||
@@ -13,9 +16,9 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
filter: version
|
||||
register: db_info
|
||||
|
||||
@@ -30,9 +33,9 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
host: "%"
|
||||
@@ -47,7 +50,7 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
filter: version
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Test PostgreSQL connection
|
||||
@@ -11,6 +15,10 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_db: "{{ jms_asset.spec_info.db_name }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
|
||||
@@ -28,6 +36,10 @@
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||
role_attr_flags: LOGIN
|
||||
ignore_errors: true
|
||||
when: result is succeeded
|
||||
@@ -39,3 +51,7 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||
|
||||
@@ -14,51 +14,15 @@
|
||||
- name: "Add {{ account.username }} user"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
shell: "{{ params.shell }}"
|
||||
home: "{{ params.home | default('/home/' + account.username, true) }}"
|
||||
groups: "{{ params.groups }}"
|
||||
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
|
||||
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
||||
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
||||
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
||||
append: yes
|
||||
expires: -1
|
||||
state: present
|
||||
when: user_info.failed
|
||||
|
||||
- name: "Add {{ account.username }} group"
|
||||
ansible.builtin.group:
|
||||
name: "{{ account.username }}"
|
||||
state: present
|
||||
when: user_info.failed
|
||||
|
||||
- name: "Add {{ account.username }} user to group"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
groups: "{{ params.groups }}"
|
||||
when:
|
||||
- user_info.failed
|
||||
- params.groups
|
||||
|
||||
- name: "Change {{ account.username }} password"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('des') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
|
||||
- name: "Change {{ account.username }} SSH key"
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: "Set {{ account.username }} sudo setting"
|
||||
ansible.builtin.lineinfile:
|
||||
dest: /etc/sudoers
|
||||
@@ -67,9 +31,59 @@
|
||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||
validate: visudo -cf %s
|
||||
when:
|
||||
- user_info.failed
|
||||
- user_info.failed or params.modify_sudo
|
||||
- params.sudo
|
||||
|
||||
- name: "Change {{ account.username }} password"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('des') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: "Get home directory for {{ account.username }}"
|
||||
ansible.builtin.shell: "getent passwd {{ account.username }} | cut -d: -f6"
|
||||
register: home_dir
|
||||
when: account.secret_type == "ssh_key"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Check if home directory exists for {{ account.username }}"
|
||||
ansible.builtin.stat:
|
||||
path: "{{ home_dir.stdout.strip() }}"
|
||||
register: home_dir_stat
|
||||
when: account.secret_type == "ssh_key"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Ensure {{ account.username }} home directory exists"
|
||||
ansible.builtin.file:
|
||||
path: "{{ home_dir.stdout.strip() }}"
|
||||
state: directory
|
||||
owner: "{{ account.username }}"
|
||||
group: "{{ account.username }}"
|
||||
mode: '0750'
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- home_dir_stat.stat.exists == false
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ home_dir.stdout.strip() }}/.ssh/authorized_keys"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Change {{ account.username }} SSH key"
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
@@ -79,7 +93,7 @@
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
become: "{{ account.become.ansible_become | default(False) }}"
|
||||
become_method: su
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
@@ -95,7 +109,7 @@
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -5,6 +5,12 @@ type:
|
||||
- AIX
|
||||
method: change_secret
|
||||
params:
|
||||
- name: modify_sudo
|
||||
type: bool
|
||||
label: "{{ 'Modify sudo label' | trans }}"
|
||||
default: False
|
||||
help_text: "{{ 'Modify params sudo help text' | trans }}"
|
||||
|
||||
- name: sudo
|
||||
type: str
|
||||
label: 'Sudo'
|
||||
@@ -28,12 +34,23 @@ params:
|
||||
default: ''
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
- name: uid
|
||||
type: str
|
||||
label: "{{ 'Params uid label' | trans }}"
|
||||
default: ''
|
||||
help_text: "{{ 'Params uid help text' | trans }}"
|
||||
|
||||
i18n:
|
||||
AIX account change secret:
|
||||
zh: '使用 Ansible 模块 user 执行账号改密 (DES)'
|
||||
ja: 'Ansible user モジュールを使用してアカウントのパスワード変更 (DES)'
|
||||
en: 'Using Ansible module user to change account secret (DES)'
|
||||
|
||||
Modify params sudo help text:
|
||||
zh: '如果用户存在,可以修改sudo权限'
|
||||
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
|
||||
en: 'If the user exists, sudo permissions can be modified'
|
||||
|
||||
Params sudo help text:
|
||||
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
|
||||
@@ -49,6 +66,16 @@ i18n:
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Params uid help text:
|
||||
zh: '请输入用户ID'
|
||||
ja: 'ユーザーIDを入力してください'
|
||||
en: 'Please enter the user ID'
|
||||
|
||||
Modify sudo label:
|
||||
zh: '修改 sudo 权限'
|
||||
ja: 'sudo 権限を変更'
|
||||
en: 'Modify sudo'
|
||||
|
||||
Params home label:
|
||||
zh: '家目录'
|
||||
ja: 'ホームディレクトリ'
|
||||
@@ -59,3 +86,7 @@ i18n:
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
|
||||
Params uid label:
|
||||
zh: '用户ID'
|
||||
ja: 'ユーザーID'
|
||||
en: 'User ID'
|
||||
|
||||
@@ -14,51 +14,15 @@
|
||||
- name: "Add {{ account.username }} user"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
shell: "{{ params.shell }}"
|
||||
home: "{{ params.home | default('/home/' + account.username, true) }}"
|
||||
groups: "{{ params.groups }}"
|
||||
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
|
||||
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
||||
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
||||
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
||||
append: yes
|
||||
expires: -1
|
||||
state: present
|
||||
when: user_info.failed
|
||||
|
||||
- name: "Add {{ account.username }} group"
|
||||
ansible.builtin.group:
|
||||
name: "{{ account.username }}"
|
||||
state: present
|
||||
when: user_info.failed
|
||||
|
||||
- name: "Add {{ account.username }} user to group"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
groups: "{{ params.groups }}"
|
||||
when:
|
||||
- user_info.failed
|
||||
- params.groups
|
||||
|
||||
- name: "Change {{ account.username }} password"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('sha512') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
|
||||
- name: "Change {{ account.username }} SSH key"
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: "Set {{ account.username }} sudo setting"
|
||||
ansible.builtin.lineinfile:
|
||||
dest: /etc/sudoers
|
||||
@@ -67,9 +31,59 @@
|
||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||
validate: visudo -cf %s
|
||||
when:
|
||||
- user_info.failed
|
||||
- user_info.failed or params.modify_sudo
|
||||
- params.sudo
|
||||
|
||||
- name: "Change {{ account.username }} password"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('sha512') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: "Get home directory for {{ account.username }}"
|
||||
ansible.builtin.shell: "getent passwd {{ account.username }} | cut -d: -f6"
|
||||
register: home_dir
|
||||
when: account.secret_type == "ssh_key"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Check if home directory exists for {{ account.username }}"
|
||||
ansible.builtin.stat:
|
||||
path: "{{ home_dir.stdout.strip() }}"
|
||||
register: home_dir_stat
|
||||
when: account.secret_type == "ssh_key"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Ensure {{ account.username }} home directory exists"
|
||||
ansible.builtin.file:
|
||||
path: "{{ home_dir.stdout.strip() }}"
|
||||
state: directory
|
||||
owner: "{{ account.username }}"
|
||||
group: "{{ account.username }}"
|
||||
mode: '0750'
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- home_dir_stat.stat.exists == false
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ home_dir.stdout.strip() }}/.ssh/authorized_keys"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Change {{ account.username }} SSH key"
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
@@ -79,7 +93,7 @@
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
become: "{{ account.become.ansible_become | default(False) }}"
|
||||
become_method: su
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
@@ -95,7 +109,7 @@
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -6,6 +6,12 @@ type:
|
||||
- linux
|
||||
method: change_secret
|
||||
params:
|
||||
- name: modify_sudo
|
||||
type: bool
|
||||
label: "{{ 'Modify sudo label' | trans }}"
|
||||
default: False
|
||||
help_text: "{{ 'Modify params sudo help text' | trans }}"
|
||||
|
||||
- name: sudo
|
||||
type: str
|
||||
label: 'Sudo'
|
||||
@@ -30,12 +36,23 @@ params:
|
||||
default: ''
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
- name: uid
|
||||
type: str
|
||||
label: "{{ 'Params uid label' | trans }}"
|
||||
default: ''
|
||||
help_text: "{{ 'Params uid help text' | trans }}"
|
||||
|
||||
i18n:
|
||||
Posix account change secret:
|
||||
zh: '使用 Ansible 模块 user 执行账号改密 (SHA512)'
|
||||
ja: 'Ansible user モジュールを使用して アカウントのパスワード変更 (SHA512)'
|
||||
en: 'Using Ansible module user to change account secret (SHA512)'
|
||||
|
||||
Modify params sudo help text:
|
||||
zh: '如果用户存在,可以修改sudo权限'
|
||||
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
|
||||
en: 'If the user exists, sudo permissions can be modified'
|
||||
|
||||
Params sudo help text:
|
||||
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
|
||||
@@ -51,6 +68,16 @@ i18n:
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Params uid help text:
|
||||
zh: '请输入用户ID'
|
||||
ja: 'ユーザーIDを入力してください'
|
||||
en: 'Please enter the user ID'
|
||||
|
||||
Modify sudo label:
|
||||
zh: '修改 sudo 权限'
|
||||
ja: 'sudo 権限を変更'
|
||||
en: 'Modify sudo'
|
||||
|
||||
Params home label:
|
||||
zh: '家目录'
|
||||
ja: 'ホームディレクトリ'
|
||||
@@ -61,3 +88,7 @@ i18n:
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
|
||||
Params uid label:
|
||||
zh: '用户ID'
|
||||
ja: 'ユーザーID'
|
||||
en: 'User ID'
|
||||
|
||||
@@ -25,11 +25,11 @@
|
||||
|
||||
- name: Verify password (pyfreerdp)
|
||||
rdp_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_host: "{{ jms_asset.origin_address }}"
|
||||
login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_secret_type: "{{ account.secret_type }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_gateway | default(None) }}"
|
||||
when: account.secret_type == "password"
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from xlsxwriter import Workbook
|
||||
|
||||
from accounts.const import AutomationTypes, SecretType, SSHKeyStrategy, SecretStrategy, ChangeSecretRecordStatusChoice
|
||||
from accounts.models import ChangeSecretRecord
|
||||
from accounts.models import ChangeSecretRecord, BaseAccountQuerySet
|
||||
from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretFailedMsg
|
||||
from accounts.serializers import ChangeSecretRecordBackUpSerializer
|
||||
from assets.const import HostTypes
|
||||
@@ -50,9 +50,6 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no'
|
||||
|
||||
if kwargs['strategy'] == SSHKeyStrategy.set_jms:
|
||||
username = account.username
|
||||
path = f'/{username}' if username == "root" else f'/home/{username}'
|
||||
kwargs['dest'] = f'{path}/.ssh/authorized_keys'
|
||||
kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip())
|
||||
return kwargs
|
||||
|
||||
@@ -68,10 +65,10 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
else:
|
||||
return self.secret_generator(secret_type).get_secret()
|
||||
|
||||
def get_accounts(self, privilege_account):
|
||||
def get_accounts(self, privilege_account) -> BaseAccountQuerySet | None:
|
||||
if not privilege_account:
|
||||
print(f'not privilege account')
|
||||
return []
|
||||
print('Not privilege account')
|
||||
return
|
||||
|
||||
asset = privilege_account.asset
|
||||
accounts = asset.accounts.all()
|
||||
@@ -108,6 +105,9 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
print(f'Windows {asset} does not support ssh key push')
|
||||
return inventory_hosts
|
||||
|
||||
if asset.type == HostTypes.WINDOWS:
|
||||
accounts = accounts.filter(secret_type=SecretType.PASSWORD)
|
||||
|
||||
host['ssh_params'] = {}
|
||||
for account in accounts:
|
||||
h = deepcopy(host)
|
||||
@@ -127,6 +127,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
recorder = ChangeSecretRecord(
|
||||
asset=asset, account=account, execution=self.execution,
|
||||
old_secret=account.secret, new_secret=new_secret,
|
||||
comment=f'{account.username}@{asset.address}'
|
||||
)
|
||||
records.append(recorder)
|
||||
else:
|
||||
@@ -226,6 +227,9 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
if self.secret_type and not self.check_secret():
|
||||
self.execution.status = 'success'
|
||||
self.execution.date_finished = timezone.now()
|
||||
self.execution.save()
|
||||
return
|
||||
super().run(*args, **kwargs)
|
||||
recorders = list(self.name_recorder_mapper.values())
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Get info
|
||||
@@ -12,9 +15,9 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
filter: users
|
||||
register: db_info
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Get info
|
||||
@@ -11,6 +15,10 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_db: "{{ jms_asset.spec_info.db_name }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||
filter: "roles"
|
||||
register: db_info
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class GatherAccountsFilter:
|
||||
def posix_filter(info):
|
||||
username_pattern = re.compile(r'^(\S+)')
|
||||
ip_pattern = re.compile(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})')
|
||||
login_time_pattern = re.compile(r'\w{3} \d{2} \d{2}:\d{2}:\d{2} \d{4}')
|
||||
login_time_pattern = re.compile(r'\w{3} \w{3}\s+\d{1,2} \d{2}:\d{2}:\d{2} \d{4}')
|
||||
result = {}
|
||||
for line in info:
|
||||
usernames = username_pattern.findall(line)
|
||||
@@ -46,7 +46,8 @@ class GatherAccountsFilter:
|
||||
result[username].update({'address': ip_addr})
|
||||
login_times = login_time_pattern.findall(line)
|
||||
if login_times:
|
||||
date = timezone.datetime.strptime(f'{login_times[0]} +0800', '%b %d %H:%M:%S %Y %z')
|
||||
datetime_str = login_times[0].split(' ', 1)[1] + " +0800"
|
||||
date = timezone.datetime.strptime(datetime_str, '%b %d %H:%M:%S %Y %z')
|
||||
result[username].update({'date': date})
|
||||
return result
|
||||
|
||||
|
||||
@@ -95,12 +95,14 @@ class GatherAccountsManager(AccountBasePlaybookManager):
|
||||
return None, None
|
||||
|
||||
users = User.objects.filter(id__in=recipients)
|
||||
if not users:
|
||||
if not users.exists():
|
||||
return users, None
|
||||
|
||||
asset_ids = self.asset_username_mapper.keys()
|
||||
assets = Asset.objects.filter(id__in=asset_ids)
|
||||
|
||||
assets = Asset.objects.filter(id__in=asset_ids).prefetch_related('accounts')
|
||||
gather_accounts = GatheredAccount.objects.filter(asset_id__in=asset_ids, present=True)
|
||||
|
||||
asset_id_map = {str(asset.id): asset for asset in assets}
|
||||
asset_id_username = list(assets.values_list('id', 'accounts__username'))
|
||||
asset_id_username.extend(list(gather_accounts.values_list('asset_id', 'username')))
|
||||
@@ -109,26 +111,24 @@ class GatherAccountsManager(AccountBasePlaybookManager):
|
||||
for asset_id, username in asset_id_username:
|
||||
system_asset_username_mapper[str(asset_id)].add(username)
|
||||
|
||||
change_info = {}
|
||||
change_info = defaultdict(dict)
|
||||
for asset_id, usernames in self.asset_username_mapper.items():
|
||||
system_usernames = system_asset_username_mapper.get(asset_id)
|
||||
|
||||
if not system_usernames:
|
||||
continue
|
||||
|
||||
add_usernames = usernames - system_usernames
|
||||
remove_usernames = system_usernames - usernames
|
||||
k = f'{asset_id_map[asset_id]}[{asset_id}]'
|
||||
|
||||
if not add_usernames and not remove_usernames:
|
||||
continue
|
||||
|
||||
change_info[k] = {
|
||||
'add_usernames': ', '.join(add_usernames),
|
||||
'remove_usernames': ', '.join(remove_usernames),
|
||||
change_info[str(asset_id_map[asset_id])] = {
|
||||
'add_usernames': add_usernames,
|
||||
'remove_usernames': remove_usernames
|
||||
}
|
||||
|
||||
return users, change_info
|
||||
return users, dict(change_info)
|
||||
|
||||
@staticmethod
|
||||
def send_email_if_need(users, change_info):
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
db_name: "{{ jms_asset.spec_info.db_name }}"
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Test MySQL connection
|
||||
@@ -13,9 +16,9 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
filter: version
|
||||
register: db_info
|
||||
|
||||
@@ -30,9 +33,9 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
host: "%"
|
||||
@@ -47,7 +50,7 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
filter: version
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Test PostgreSQL connection
|
||||
@@ -11,6 +15,10 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_db: "{{ jms_asset.spec_info.db_name }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
|
||||
@@ -28,6 +36,10 @@
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||
role_attr_flags: LOGIN
|
||||
ignore_errors: true
|
||||
when: result is succeeded
|
||||
@@ -40,6 +52,10 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||
when:
|
||||
- result is succeeded
|
||||
- change_info is succeeded
|
||||
|
||||
@@ -14,51 +14,15 @@
|
||||
- name: "Add {{ account.username }} user"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
shell: "{{ params.shell }}"
|
||||
home: "{{ params.home | default('/home/' + account.username, true) }}"
|
||||
groups: "{{ params.groups }}"
|
||||
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
|
||||
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
||||
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
||||
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
||||
append: yes
|
||||
expires: -1
|
||||
state: present
|
||||
when: user_info.failed
|
||||
|
||||
- name: "Add {{ account.username }} group"
|
||||
ansible.builtin.group:
|
||||
name: "{{ account.username }}"
|
||||
state: present
|
||||
when: user_info.failed
|
||||
|
||||
- name: "Add {{ account.username }} user to group"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
groups: "{{ params.groups }}"
|
||||
when:
|
||||
- user_info.failed
|
||||
- params.groups
|
||||
|
||||
- name: "Change {{ account.username }} password"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('des') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
|
||||
- name: "Change {{ account.username }} SSH key"
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: "Set {{ account.username }} sudo setting"
|
||||
ansible.builtin.lineinfile:
|
||||
dest: /etc/sudoers
|
||||
@@ -67,9 +31,59 @@
|
||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||
validate: visudo -cf %s
|
||||
when:
|
||||
- user_info.failed
|
||||
- user_info.failed or params.modify_sudo
|
||||
- params.sudo
|
||||
|
||||
- name: "Change {{ account.username }} password"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('des') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: "Get home directory for {{ account.username }}"
|
||||
ansible.builtin.shell: "getent passwd {{ account.username }} | cut -d: -f6"
|
||||
register: home_dir
|
||||
when: account.secret_type == "ssh_key"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Check if home directory exists for {{ account.username }}"
|
||||
ansible.builtin.stat:
|
||||
path: "{{ home_dir.stdout.strip() }}"
|
||||
register: home_dir_stat
|
||||
when: account.secret_type == "ssh_key"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Ensure {{ account.username }} home directory exists"
|
||||
ansible.builtin.file:
|
||||
path: "{{ home_dir.stdout.strip() }}"
|
||||
state: directory
|
||||
owner: "{{ account.username }}"
|
||||
group: "{{ account.username }}"
|
||||
mode: '0750'
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- home_dir_stat.stat.exists == false
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ home_dir.stdout.strip() }}/.ssh/authorized_keys"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Change {{ account.username }} SSH key"
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
@@ -79,7 +93,7 @@
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
become: "{{ account.become.ansible_become | default(False) }}"
|
||||
become_method: su
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
@@ -95,7 +109,7 @@
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -5,6 +5,12 @@ type:
|
||||
- AIX
|
||||
method: push_account
|
||||
params:
|
||||
- name: modify_sudo
|
||||
type: bool
|
||||
label: "{{ 'Modify sudo label' | trans }}"
|
||||
default: False
|
||||
help_text: "{{ 'Modify params sudo help text' | trans }}"
|
||||
|
||||
- name: sudo
|
||||
type: str
|
||||
label: 'Sudo'
|
||||
@@ -28,12 +34,23 @@ params:
|
||||
default: ''
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
- name: uid
|
||||
type: str
|
||||
label: "{{ 'Params uid label' | trans }}"
|
||||
default: ''
|
||||
help_text: "{{ 'Params uid help text' | trans }}"
|
||||
|
||||
i18n:
|
||||
Aix account push:
|
||||
zh: '使用 Ansible 模块 user 执行 Aix 账号推送 (DES)'
|
||||
ja: 'Ansible user モジュールを使用して Aix アカウントをプッシュする (DES)'
|
||||
en: 'Using Ansible module user to push account (DES)'
|
||||
|
||||
Modify params sudo help text:
|
||||
zh: '如果用户存在,可以修改sudo权限'
|
||||
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
|
||||
en: 'If the user exists, sudo permissions can be modified'
|
||||
|
||||
Params sudo help text:
|
||||
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
|
||||
@@ -49,6 +66,16 @@ i18n:
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Params uid help text:
|
||||
zh: '请输入用户ID'
|
||||
ja: 'ユーザーIDを入力してください'
|
||||
en: 'Please enter the user ID'
|
||||
|
||||
Modify sudo label:
|
||||
zh: '修改 sudo 权限'
|
||||
ja: 'sudo 権限を変更'
|
||||
en: 'Modify sudo'
|
||||
|
||||
Params home label:
|
||||
zh: '家目录'
|
||||
ja: 'ホームディレクトリ'
|
||||
@@ -59,3 +86,7 @@ i18n:
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
|
||||
Params uid label:
|
||||
zh: '用户ID'
|
||||
ja: 'ユーザーID'
|
||||
en: 'User ID'
|
||||
|
||||
@@ -14,51 +14,15 @@
|
||||
- name: "Add {{ account.username }} user"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
shell: "{{ params.shell }}"
|
||||
home: "{{ params.home | default('/home/' + account.username, true) }}"
|
||||
groups: "{{ params.groups }}"
|
||||
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
|
||||
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
||||
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
||||
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
||||
append: yes
|
||||
expires: -1
|
||||
state: present
|
||||
when: user_info.failed
|
||||
|
||||
- name: "Add {{ account.username }} group"
|
||||
ansible.builtin.group:
|
||||
name: "{{ account.username }}"
|
||||
state: present
|
||||
when: user_info.failed
|
||||
|
||||
- name: "Add {{ account.username }} user to group"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
groups: "{{ params.groups }}"
|
||||
when:
|
||||
- user_info.failed
|
||||
- params.groups
|
||||
|
||||
- name: "Change {{ account.username }} password"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('sha512') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
|
||||
- name: "Change {{ account.username }} SSH key"
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: "Set {{ account.username }} sudo setting"
|
||||
ansible.builtin.lineinfile:
|
||||
dest: /etc/sudoers
|
||||
@@ -67,9 +31,59 @@
|
||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||
validate: visudo -cf %s
|
||||
when:
|
||||
- user_info.failed
|
||||
- user_info.failed or params.modify_sudo
|
||||
- params.sudo
|
||||
|
||||
- name: "Change {{ account.username }} password"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('sha512') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: "Get home directory for {{ account.username }}"
|
||||
ansible.builtin.shell: "getent passwd {{ account.username }} | cut -d: -f6"
|
||||
register: home_dir
|
||||
when: account.secret_type == "ssh_key"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Check if home directory exists for {{ account.username }}"
|
||||
ansible.builtin.stat:
|
||||
path: "{{ home_dir.stdout.strip() }}"
|
||||
register: home_dir_stat
|
||||
when: account.secret_type == "ssh_key"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Ensure {{ account.username }} home directory exists"
|
||||
ansible.builtin.file:
|
||||
path: "{{ home_dir.stdout.strip() }}"
|
||||
state: directory
|
||||
owner: "{{ account.username }}"
|
||||
group: "{{ account.username }}"
|
||||
mode: '0750'
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- home_dir_stat.stat.exists == false
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ home_dir.stdout.strip() }}/.ssh/authorized_keys"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Change {{ account.username }} SSH key"
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
@@ -79,7 +93,7 @@
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
become: "{{ account.become.ansible_become | default(False) }}"
|
||||
become_method: su
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
@@ -95,7 +109,7 @@
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -6,6 +6,12 @@ type:
|
||||
- linux
|
||||
method: push_account
|
||||
params:
|
||||
- name: modify_sudo
|
||||
type: bool
|
||||
label: "{{ 'Modify sudo label' | trans }}"
|
||||
default: False
|
||||
help_text: "{{ 'Modify params sudo help text' | trans }}"
|
||||
|
||||
- name: sudo
|
||||
type: str
|
||||
label: 'Sudo'
|
||||
@@ -30,12 +36,23 @@ params:
|
||||
default: ''
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
- name: uid
|
||||
type: str
|
||||
label: "{{ 'Params uid label' | trans }}"
|
||||
default: ''
|
||||
help_text: "{{ 'Params uid help text' | trans }}"
|
||||
|
||||
i18n:
|
||||
Posix account push:
|
||||
zh: '使用 Ansible 模块 user 执行账号推送 (sha512)'
|
||||
ja: 'Ansible user モジュールを使用してアカウントをプッシュする (sha512)'
|
||||
en: 'Using Ansible module user to push account (sha512)'
|
||||
|
||||
Modify params sudo help text:
|
||||
zh: '如果用户存在,可以修改sudo权限'
|
||||
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
|
||||
en: 'If the user exists, sudo permissions can be modified'
|
||||
|
||||
Params sudo help text:
|
||||
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
|
||||
@@ -51,6 +68,16 @@ i18n:
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Params uid help text:
|
||||
zh: '请输入用户ID'
|
||||
ja: 'ユーザーIDを入力してください'
|
||||
en: 'Please enter the user ID'
|
||||
|
||||
Modify sudo label:
|
||||
zh: '修改 sudo 权限'
|
||||
ja: 'sudo 権限を変更'
|
||||
en: 'Modify sudo'
|
||||
|
||||
Params home label:
|
||||
zh: '家目录'
|
||||
ja: 'ホームディレクトリ'
|
||||
@@ -59,4 +86,9 @@ i18n:
|
||||
Params groups label:
|
||||
zh: '用户组'
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
en: 'Groups'
|
||||
|
||||
Params uid label:
|
||||
zh: '用户ID'
|
||||
ja: 'ユーザーID'
|
||||
en: 'User ID'
|
||||
@@ -25,11 +25,11 @@
|
||||
|
||||
- name: Verify password (pyfreerdp)
|
||||
rdp_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_host: "{{ jms_asset.origin_address }}"
|
||||
login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_secret_type: "{{ account.secret_type }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_gateway | default(None) }}"
|
||||
when: account.secret_type == "password"
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: "Remove account"
|
||||
@@ -12,8 +15,8 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
name: "{{ account.username }}"
|
||||
state: absent
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: "Remove account"
|
||||
@@ -12,4 +16,8 @@
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
name: "{{ account.username }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||
state: absent
|
||||
|
||||
@@ -13,4 +13,3 @@
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_secret_type: "{{ account.secret_type }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Verify account
|
||||
@@ -12,7 +15,7 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
filter: version
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Verify account
|
||||
@@ -11,5 +15,9 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from accounts.const import AutomationTypes
|
||||
from assets.automations.ping_gateway.manager import PingGatewayManager
|
||||
from common.utils import get_logger
|
||||
@@ -13,7 +15,7 @@ class VerifyGatewayAccountManager(PingGatewayManager):
|
||||
|
||||
@staticmethod
|
||||
def before_runner_start():
|
||||
logger.info(">>> 开始执行测试网关账号可连接性任务")
|
||||
logger.info(_(">>> Start executing the task to test gateway account connectivity"))
|
||||
|
||||
def get_accounts(self, gateway):
|
||||
account_ids = self.execution.snapshot['accounts']
|
||||
|
||||
@@ -10,7 +10,6 @@ import common.db.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
@@ -26,13 +25,19 @@ class Migration(migrations.Migration):
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('connectivity', models.CharField(choices=[('-', 'Unknown'), ('ok', 'OK'), ('err', 'Error')], default='-', max_length=16, verbose_name='Connectivity')),
|
||||
('org_id',
|
||||
models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('connectivity',
|
||||
models.CharField(choices=[('-', 'Unknown'), ('ok', 'OK'), ('err', 'Error')], default='-',
|
||||
max_length=16, verbose_name='Connectivity')),
|
||||
('date_verified', models.DateTimeField(null=True, verbose_name='Date verified')),
|
||||
('_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')),
|
||||
('secret_type', models.CharField(choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), ('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16, verbose_name='Secret type')),
|
||||
('secret_type', models.CharField(
|
||||
choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'),
|
||||
('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16,
|
||||
verbose_name='Secret type')),
|
||||
('privileged', models.BooleanField(default=False, verbose_name='Privileged')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
||||
('version', models.IntegerField(default=0, verbose_name='Version')),
|
||||
@@ -41,7 +46,11 @@ class Migration(migrations.Migration):
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Account',
|
||||
'permissions': [('view_accountsecret', 'Can view asset account secret'), ('view_historyaccount', 'Can view asset history account'), ('view_historyaccountsecret', 'Can view asset history account secret'), ('verify_account', 'Can verify account'), ('push_account', 'Can push account'), ('remove_account', 'Can remove account')],
|
||||
'permissions': [('view_accountsecret', 'Can view asset account secret'),
|
||||
('view_historyaccount', 'Can view asset history account'),
|
||||
('view_historyaccountsecret', 'Can view asset history account secret'),
|
||||
('verify_account', 'Can verify account'), ('push_account', 'Can push account'),
|
||||
('remove_account', 'Can remove account')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
@@ -53,16 +62,21 @@ class Migration(migrations.Migration):
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('org_id',
|
||||
models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('is_periodic', models.BooleanField(default=False, verbose_name='Periodic run')),
|
||||
('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Interval')),
|
||||
('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Crontab')),
|
||||
('types', models.JSONField(default=list)),
|
||||
('backup_type', models.CharField(choices=[('email', 'Email'), ('object_storage', 'SFTP')], default='email', max_length=128, verbose_name='Backup type')),
|
||||
('backup_type',
|
||||
models.CharField(choices=[('email', 'Email'), ('object_storage', 'SFTP')], default='email',
|
||||
max_length=128, verbose_name='Backup type')),
|
||||
('is_password_divided_by_email', models.BooleanField(default=True, verbose_name='Password divided')),
|
||||
('is_password_divided_by_obj_storage', models.BooleanField(default=True, verbose_name='Password divided')),
|
||||
('zip_encrypt_password', common.db.fields.EncryptCharField(blank=True, max_length=4096, null=True, verbose_name='Zip encrypt password')),
|
||||
('is_password_divided_by_obj_storage',
|
||||
models.BooleanField(default=True, verbose_name='Password divided')),
|
||||
('zip_encrypt_password', common.db.fields.EncryptCharField(blank=True, max_length=4096, null=True,
|
||||
verbose_name='Zip encrypt password')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Account backup plan',
|
||||
@@ -72,12 +86,16 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='AccountBackupExecution',
|
||||
fields=[
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('org_id',
|
||||
models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('date_start', models.DateTimeField(auto_now_add=True, verbose_name='Date start')),
|
||||
('timedelta', models.FloatField(default=0.0, null=True, verbose_name='Time')),
|
||||
('snapshot', models.JSONField(blank=True, default=dict, encoder=common.db.encoder.ModelJSONFieldEncoder, null=True, verbose_name='Account backup snapshot')),
|
||||
('trigger', models.CharField(choices=[('manual', 'Manual trigger'), ('timing', 'Timing trigger')], default='manual', max_length=128, verbose_name='Trigger mode')),
|
||||
('snapshot',
|
||||
models.JSONField(blank=True, default=dict, encoder=common.db.encoder.ModelJSONFieldEncoder, null=True,
|
||||
verbose_name='Account backup snapshot')),
|
||||
('trigger', models.CharField(choices=[('manual', 'Manual trigger'), ('timing', 'Timing trigger')],
|
||||
default='manual', max_length=128, verbose_name='Trigger mode')),
|
||||
('reason', models.CharField(blank=True, max_length=1024, null=True, verbose_name='Reason')),
|
||||
('is_success', models.BooleanField(default=False, verbose_name='Is success')),
|
||||
],
|
||||
@@ -95,13 +113,19 @@ class Migration(migrations.Migration):
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('org_id',
|
||||
models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
|
||||
('secret_strategy', models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy')),
|
||||
('secret_strategy',
|
||||
models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')],
|
||||
default='specific', max_length=16, verbose_name='Secret strategy')),
|
||||
('password_rules', models.JSONField(default=dict, verbose_name='Password rules')),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')),
|
||||
('secret_type', models.CharField(choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), ('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16, verbose_name='Secret type')),
|
||||
('secret_type', models.CharField(
|
||||
choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'),
|
||||
('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16,
|
||||
verbose_name='Secret type')),
|
||||
('privileged', models.BooleanField(default=False, verbose_name='Privileged')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
||||
('auto_push', models.BooleanField(default=False, verbose_name='Auto push')),
|
||||
@@ -142,7 +166,8 @@ class Migration(migrations.Migration):
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('org_id',
|
||||
models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('present', models.BooleanField(default=True, verbose_name='Present')),
|
||||
('date_last_login', models.DateTimeField(null=True, verbose_name='Date login')),
|
||||
('username', models.CharField(blank=True, db_index=True, max_length=32, verbose_name='Username')),
|
||||
@@ -158,12 +183,16 @@ class Migration(migrations.Migration):
|
||||
fields=[
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
|
||||
('_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
|
||||
('secret_type', models.CharField(choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), ('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16, verbose_name='Secret type')),
|
||||
('secret_type', models.CharField(
|
||||
choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'),
|
||||
('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16,
|
||||
verbose_name='Secret type')),
|
||||
('version', models.IntegerField(default=0, verbose_name='Version')),
|
||||
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('history_date', models.DateTimeField(db_index=True)),
|
||||
('history_change_reason', models.CharField(max_length=100, null=True)),
|
||||
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
|
||||
('history_type',
|
||||
models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'historical Account',
|
||||
@@ -181,9 +210,13 @@ class Migration(migrations.Migration):
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('alias', models.CharField(choices=[('@INPUT', 'Manual input'), ('@USER', 'Dynamic user'), ('@ANON', 'Anonymous account'), ('@SPEC', 'Specified account')], max_length=128, verbose_name='Alias')),
|
||||
('org_id',
|
||||
models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('alias', models.CharField(
|
||||
choices=[('@INPUT', 'Manual input'), ('@USER', 'Dynamic user'), ('@ANON', 'Anonymous account'),
|
||||
('@SPEC', 'Specified account')], max_length=128, verbose_name='Alias')),
|
||||
('secret_from_login', models.BooleanField(default=None, null=True, verbose_name='Secret from login')),
|
||||
],
|
||||
options={'verbose_name': 'Virtual account'},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 4.1.13 on 2024-08-26 09:05
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0005_myasset'),
|
||||
('accounts', '0003_automation'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='changesecretrecord',
|
||||
name='account',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.account'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='changesecretrecord',
|
||||
name='asset',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.asset'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='changesecretrecord',
|
||||
name='execution',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.automationexecution'),
|
||||
),
|
||||
]
|
||||
@@ -53,7 +53,8 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount):
|
||||
on_delete=models.SET_NULL, verbose_name=_("Su from")
|
||||
)
|
||||
version = models.IntegerField(default=0, verbose_name=_('Version'))
|
||||
history = AccountHistoricalRecords(included_fields=['id', '_secret', 'secret_type', 'version'])
|
||||
history = AccountHistoricalRecords(included_fields=['id', '_secret', 'secret_type', 'version'],
|
||||
verbose_name=_("historical Account"))
|
||||
source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source'))
|
||||
source_id = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Source ID'))
|
||||
|
||||
@@ -119,7 +120,8 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount):
|
||||
return auth
|
||||
|
||||
auth.update(self.make_account_ansible_vars(su_from))
|
||||
become_method = platform.su_method if platform.su_method else 'sudo'
|
||||
|
||||
become_method = platform.ansible_become_method
|
||||
password = su_from.secret if become_method == 'sudo' else self.secret
|
||||
auth['ansible_become'] = True
|
||||
auth['ansible_become_method'] = become_method
|
||||
|
||||
@@ -33,16 +33,15 @@ class ChangeSecretAutomation(ChangeSecretMixin, AccountBaseAutomation):
|
||||
|
||||
|
||||
class ChangeSecretRecord(JMSBaseModel):
|
||||
execution = models.ForeignKey('accounts.AutomationExecution', on_delete=models.CASCADE)
|
||||
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, null=True)
|
||||
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE, null=True)
|
||||
execution = models.ForeignKey('accounts.AutomationExecution', on_delete=models.SET_NULL, null=True)
|
||||
asset = models.ForeignKey('assets.Asset', on_delete=models.SET_NULL, null=True)
|
||||
account = models.ForeignKey('accounts.Account', on_delete=models.SET_NULL, null=True)
|
||||
old_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Old secret'))
|
||||
new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('New secret'))
|
||||
date_started = models.DateTimeField(blank=True, null=True, verbose_name=_('Date started'))
|
||||
date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('Date finished'))
|
||||
status = models.CharField(
|
||||
max_length=16, verbose_name=_('Status'),
|
||||
default=ChangeSecretRecordStatusChoice.pending.value
|
||||
max_length=16, verbose_name=_('Status'), default=ChangeSecretRecordStatusChoice.pending.value
|
||||
)
|
||||
error = models.TextField(blank=True, null=True, verbose_name=_('Error'))
|
||||
|
||||
@@ -51,4 +50,4 @@ class ChangeSecretRecord(JMSBaseModel):
|
||||
verbose_name = _("Change secret record")
|
||||
|
||||
def __str__(self):
|
||||
return self.account.__str__()
|
||||
return f'{self.account.username}@{self.asset}'
|
||||
|
||||
@@ -81,21 +81,28 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
|
||||
|
||||
@staticmethod
|
||||
def get_template_attr_for_account(template):
|
||||
# Set initial data from template
|
||||
field_names = [
|
||||
'name', 'username', 'secret', 'push_params',
|
||||
'secret_type', 'privileged', 'is_active'
|
||||
'name', 'username',
|
||||
'secret_type', 'secret',
|
||||
'privileged', 'is_active'
|
||||
]
|
||||
|
||||
field_map = {
|
||||
'push_params': 'params',
|
||||
'auto_push': 'push_now'
|
||||
}
|
||||
|
||||
field_names.extend(field_map.keys())
|
||||
|
||||
attrs = {}
|
||||
for name in field_names:
|
||||
value = getattr(template, name, None)
|
||||
if value is None:
|
||||
continue
|
||||
if name == 'push_params':
|
||||
attrs['params'] = value
|
||||
else:
|
||||
attrs[name] = value
|
||||
|
||||
attr_name = field_map.get(name, name)
|
||||
attrs[attr_name] = value
|
||||
|
||||
attrs['secret'] = template.get_secret()
|
||||
return attrs
|
||||
|
||||
@@ -171,14 +178,15 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
|
||||
instance.save()
|
||||
return instance, 'updated'
|
||||
else:
|
||||
raise serializers.ValidationError('Account already exists')
|
||||
raise serializers.ValidationError(_('Account already exists'))
|
||||
|
||||
def create(self, validated_data):
|
||||
push_now = validated_data.pop('push_now', None)
|
||||
params = validated_data.pop('params', None)
|
||||
self.clean_auth_fields(validated_data)
|
||||
instance, stat = self.do_create(validated_data)
|
||||
self.push_account_if_need(instance, push_now, params, stat)
|
||||
if instance.source == Source.LOCAL:
|
||||
self.push_account_if_need(instance, push_now, params, stat)
|
||||
return instance
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
@@ -239,6 +247,7 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
|
||||
'name': {'required': False},
|
||||
'source_id': {'required': False, 'allow_null': True},
|
||||
}
|
||||
fields_unimport_template = ['params']
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
@@ -280,8 +289,8 @@ class AssetAccountBulkSerializer(
|
||||
fields = [
|
||||
'name', 'username', 'secret', 'secret_type', 'passphrase',
|
||||
'privileged', 'is_active', 'comment', 'template',
|
||||
'on_invalid', 'push_now', 'assets', 'su_from_username',
|
||||
'source', 'source_id',
|
||||
'on_invalid', 'push_now', 'params', 'assets',
|
||||
'su_from_username', 'source', 'source_id',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'name': {'required': False},
|
||||
@@ -419,16 +428,23 @@ class AssetAccountBulkSerializer(
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def push_accounts_if_need(results, push_now):
|
||||
def push_accounts_if_need(results, push_now, params):
|
||||
if not push_now:
|
||||
return
|
||||
accounts = [str(v['instance']) for v in results if v.get('instance')]
|
||||
push_accounts_to_assets_task.delay(accounts)
|
||||
|
||||
account_ids = [v['instance'] for v in results if v.get('instance')]
|
||||
accounts = Account.objects.filter(id__in=account_ids, source=Source.LOCAL)
|
||||
if not accounts.exists():
|
||||
return
|
||||
|
||||
account_ids = [str(_id) for _id in accounts.values_list('id', flat=True)]
|
||||
push_accounts_to_assets_task.delay(account_ids, params)
|
||||
|
||||
def create(self, validated_data):
|
||||
params = validated_data.pop('params', None)
|
||||
push_now = validated_data.pop('push_now', False)
|
||||
results = self.perform_bulk_create(validated_data)
|
||||
self.push_accounts_if_need(results, push_now)
|
||||
self.push_accounts_if_need(results, push_now, params)
|
||||
for res in results:
|
||||
res['asset'] = str(res['asset'])
|
||||
return results
|
||||
|
||||
@@ -10,7 +10,7 @@ from .base import BaseAccountSerializer
|
||||
|
||||
|
||||
class PasswordRulesSerializer(serializers.Serializer):
|
||||
length = serializers.IntegerField(min_value=8, max_value=30, default=16, label=_('Password length'))
|
||||
length = serializers.IntegerField(min_value=8, max_value=36, default=16, label=_('Password length'))
|
||||
lowercase = serializers.BooleanField(default=True, label=_('Lowercase'))
|
||||
uppercase = serializers.BooleanField(default=True, label=_('Uppercase'))
|
||||
digit = serializers.BooleanField(default=True, label=_('Digit'))
|
||||
@@ -19,6 +19,16 @@ class PasswordRulesSerializer(serializers.Serializer):
|
||||
default='', allow_blank=True, max_length=16, label=_('Exclude symbol')
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_render_help_text():
|
||||
return _("""length is the length of the password, and the range is 8 to 30.
|
||||
lowercase indicates whether the password contains lowercase letters,
|
||||
uppercase indicates whether it contains uppercase letters,
|
||||
digit indicates whether it contains numbers, and symbol indicates whether it contains special symbols.
|
||||
exclude_symbols is used to exclude specific symbols. You can fill in the symbol characters to be excluded (up to 16).
|
||||
If you do not need to exclude symbols, you can leave it blank.
|
||||
default: {"length": 16, "lowercase": true, "uppercase": true, "digit": true, "symbol": true, "exclude_symbols": ""}""")
|
||||
|
||||
|
||||
class AccountTemplateSerializer(BaseAccountSerializer):
|
||||
password_rules = PasswordRulesSerializer(required=False, label=_('Password rules'))
|
||||
@@ -46,6 +56,7 @@ class AccountTemplateSerializer(BaseAccountSerializer):
|
||||
'required': False
|
||||
},
|
||||
}
|
||||
fields_unimport_template = ['push_params']
|
||||
|
||||
@staticmethod
|
||||
def generate_secret(attrs):
|
||||
|
||||
@@ -75,19 +75,6 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
|
||||
if self.initial_data.get('secret_strategy') == SecretStrategy.custom:
|
||||
return password_rules
|
||||
|
||||
length = password_rules.get('length')
|
||||
|
||||
try:
|
||||
length = int(length)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
msg = _("* Please enter the correct password length")
|
||||
raise serializers.ValidationError(msg)
|
||||
|
||||
if length < 6 or length > 30:
|
||||
msg = _('* Password length range 6-30 bits')
|
||||
raise serializers.ValidationError(msg)
|
||||
|
||||
return password_rules
|
||||
|
||||
def validate(self, attrs):
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.dispatch import receiver
|
||||
from django.utils.translation import gettext_noop
|
||||
|
||||
from accounts.backends import vault_client
|
||||
from accounts.const import Source
|
||||
from audits.const import ActivityChoices
|
||||
from audits.signal_handlers import create_activities
|
||||
from common.decorators import merge_delay_run
|
||||
@@ -32,7 +33,7 @@ def push_accounts_if_need(accounts=()):
|
||||
template_accounts = defaultdict(list)
|
||||
for ac in accounts:
|
||||
# 再强调一次吧
|
||||
if ac.source != 'template':
|
||||
if ac.source != Source.TEMPLATE:
|
||||
continue
|
||||
template_accounts[ac.source_id].append(ac)
|
||||
|
||||
@@ -61,7 +62,7 @@ def create_accounts_activities(account, action='create'):
|
||||
|
||||
@receiver(post_save, sender=Account)
|
||||
def on_account_create_by_template(sender, instance, created=False, **kwargs):
|
||||
if not created or instance.source != 'template':
|
||||
if not created or instance.source != Source.TEMPLATE:
|
||||
return
|
||||
push_accounts_if_need.delay(accounts=(instance,))
|
||||
create_accounts_activities(instance, action='create')
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import datetime
|
||||
|
||||
from celery import shared_task
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _, gettext_noop
|
||||
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.tasks.common import quickstart_automation_by_snapshot
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from common.const.crontab import CRONTAB_AT_AM_THREE
|
||||
from common.utils import get_logger, get_object_or_none, get_log_keep_day
|
||||
from ops.celery.decorator import register_as_period_task
|
||||
from orgs.utils import tmp_to_org, tmp_to_root_org
|
||||
|
||||
logger = get_logger(__file__)
|
||||
@@ -22,8 +28,14 @@ def task_activity_callback(self, pid, trigger, tp, *args, **kwargs):
|
||||
|
||||
|
||||
@shared_task(
|
||||
queue='ansible', verbose_name=_('Account execute automation'),
|
||||
activity_callback=task_activity_callback
|
||||
queue='ansible',
|
||||
verbose_name=_('Account execute automation'),
|
||||
activity_callback=task_activity_callback,
|
||||
description=_(
|
||||
"""Unified execution entry for account automation tasks: when the system performs tasks
|
||||
such as account push, password change, account verification, account collection,
|
||||
and gateway account verification, all tasks are executed through this unified entry"""
|
||||
)
|
||||
)
|
||||
def execute_account_automation_task(pid, trigger, tp):
|
||||
model = AutomationTypes.get_type_model(tp)
|
||||
@@ -48,8 +60,12 @@ def record_task_activity_callback(self, record_ids, *args, **kwargs):
|
||||
|
||||
|
||||
@shared_task(
|
||||
queue='ansible', verbose_name=_('Execute automation record'),
|
||||
activity_callback=record_task_activity_callback
|
||||
queue='ansible',
|
||||
verbose_name=_('Execute automation record'),
|
||||
activity_callback=record_task_activity_callback,
|
||||
description=_(
|
||||
"""When manually executing password change records, this task is used"""
|
||||
)
|
||||
)
|
||||
def execute_automation_record_task(record_ids, tp):
|
||||
from accounts.models import ChangeSecretRecord
|
||||
@@ -74,3 +90,33 @@ def execute_automation_record_task(record_ids, tp):
|
||||
}
|
||||
with tmp_to_org(record.execution.org_id):
|
||||
quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
|
||||
|
||||
|
||||
@shared_task(
|
||||
verbose_name=_('Clean change secret and push record period'),
|
||||
description=_(
|
||||
"""The system will periodically clean up unnecessary password change and push records,
|
||||
including their associated change tasks, execution logs, assets, and accounts. When any
|
||||
of these associated items are deleted, the corresponding password change and push records
|
||||
become invalid. Therefore, to maintain a clean and efficient database, the system will
|
||||
clean up expired records at 2 a.m daily, based on the interval specified by
|
||||
PERM_EXPIRED_CHECK_PERIODIC in the config.txt configuration file. This periodic cleanup
|
||||
mechanism helps free up storage space and enhances the security and overall performance
|
||||
of data management"""
|
||||
)
|
||||
)
|
||||
@register_as_period_task(crontab=CRONTAB_AT_AM_THREE)
|
||||
def clean_change_secret_and_push_record_period():
|
||||
from accounts.models import ChangeSecretRecord
|
||||
print('Start clean change secret and push record period')
|
||||
with tmp_to_root_org():
|
||||
now = timezone.now()
|
||||
days = get_log_keep_day('ACCOUNT_CHANGE_SECRET_RECORD_KEEP_DAYS')
|
||||
expired_day = now - datetime.timedelta(days=days)
|
||||
records = ChangeSecretRecord.objects.filter(
|
||||
date_updated__lt=expired_day
|
||||
).filter(
|
||||
Q(execution__isnull=True) | Q(asset__isnull=True) | Q(account__isnull=True)
|
||||
)
|
||||
|
||||
records.delete()
|
||||
|
||||
@@ -22,7 +22,13 @@ def task_activity_callback(self, pid, trigger, *args, **kwargs):
|
||||
return resource_ids, org_id
|
||||
|
||||
|
||||
@shared_task(verbose_name=_('Execute account backup plan'), activity_callback=task_activity_callback)
|
||||
@shared_task(
|
||||
verbose_name=_('Execute account backup plan'),
|
||||
activity_callback=task_activity_callback,
|
||||
description=_(
|
||||
"When performing scheduled or manual account backups, this task is used"
|
||||
)
|
||||
)
|
||||
def execute_account_backup_task(pid, trigger, **kwargs):
|
||||
from accounts.models import AccountBackupAutomation
|
||||
with tmp_to_root_org():
|
||||
|
||||
@@ -26,8 +26,10 @@ def gather_asset_accounts_util(nodes, task_name):
|
||||
|
||||
|
||||
@shared_task(
|
||||
queue="ansible", verbose_name=_('Gather asset accounts'),
|
||||
activity_callback=lambda self, node_ids, task_name=None, *args, **kwargs: (node_ids, None)
|
||||
queue="ansible",
|
||||
verbose_name=_('Gather asset accounts'),
|
||||
activity_callback=lambda self, node_ids, task_name=None, *args, **kwargs: (node_ids, None),
|
||||
description=_("Unused")
|
||||
)
|
||||
def gather_asset_accounts_task(node_ids, task_name=None):
|
||||
if task_name is None:
|
||||
|
||||
@@ -12,8 +12,12 @@ __all__ = [
|
||||
|
||||
|
||||
@shared_task(
|
||||
queue="ansible", verbose_name=_('Push accounts to assets'),
|
||||
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None)
|
||||
queue="ansible",
|
||||
verbose_name=_('Push accounts to assets'),
|
||||
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None),
|
||||
description=_(
|
||||
"When creating or modifying an account requires account push, this task is executed"
|
||||
)
|
||||
)
|
||||
def push_accounts_to_assets_task(account_ids, params=None):
|
||||
from accounts.models import PushAccountAutomation
|
||||
|
||||
@@ -21,8 +21,13 @@ __all__ = ['remove_accounts_task']
|
||||
|
||||
|
||||
@shared_task(
|
||||
queue="ansible", verbose_name=_('Remove account'),
|
||||
activity_callback=lambda self, gather_account_ids, *args, **kwargs: (gather_account_ids, None)
|
||||
queue="ansible",
|
||||
verbose_name=_('Remove account'),
|
||||
activity_callback=lambda self, gather_account_ids, *args, **kwargs: (gather_account_ids, None),
|
||||
description=_(
|
||||
"""When clicking "Sync deletion" in 'Console - Gather Account - Gathered accounts' this
|
||||
task will be executed"""
|
||||
)
|
||||
)
|
||||
def remove_accounts_task(gather_account_ids):
|
||||
from accounts.models import GatheredAccount
|
||||
@@ -41,7 +46,15 @@ def remove_accounts_task(gather_account_ids):
|
||||
quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
|
||||
|
||||
|
||||
@shared_task(verbose_name=_('Clean historical accounts'))
|
||||
@shared_task(
|
||||
verbose_name=_('Clean historical accounts'),
|
||||
description=_(
|
||||
"""Each time an asset account is updated, a historical account is generated, so it is
|
||||
necessary to clean up the asset account history. The system will clean up excess account
|
||||
records at 2 a.m. daily based on the configuration in the "System settings - Features -
|
||||
Account storage - Record limit"""
|
||||
)
|
||||
)
|
||||
@register_as_period_task(crontab=CRONTAB_AT_AM_TWO)
|
||||
@tmp_to_root_org()
|
||||
def clean_historical_accounts():
|
||||
|
||||
@@ -9,7 +9,11 @@ from orgs.utils import tmp_to_root_org, tmp_to_org
|
||||
|
||||
@shared_task(
|
||||
verbose_name=_('Template sync info to related accounts'),
|
||||
activity_callback=lambda self, template_id, *args, **kwargs: (template_id, None)
|
||||
activity_callback=lambda self, template_id, *args, **kwargs: (template_id, None),
|
||||
description=_(
|
||||
"""When clicking 'Sync new secret to accounts' in 'Console - Account - Templates -
|
||||
Accounts' this task will be executed"""
|
||||
)
|
||||
)
|
||||
def template_sync_related_accounts(template_id, user_id=None):
|
||||
from accounts.models import Account, AccountTemplate
|
||||
|
||||
@@ -28,7 +28,12 @@ def sync_instance(instance):
|
||||
return "succeeded", msg
|
||||
|
||||
|
||||
@shared_task(verbose_name=_('Sync secret to vault'))
|
||||
@shared_task(
|
||||
verbose_name=_('Sync secret to vault'),
|
||||
description=_(
|
||||
"When clicking 'Sync' in 'System Settings - Features - Account Storage' this task will be executed"
|
||||
)
|
||||
)
|
||||
def sync_secret_to_vault():
|
||||
if not vault_client.enabled:
|
||||
# 这里不能判断 settings.VAULT_ENABLED, 必须判断当前 vault_client 的类型
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.utils.translation import gettext_noop
|
||||
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.tasks.common import quickstart_automation_by_snapshot
|
||||
from assets.const import GATEWAY_NAME
|
||||
from common.utils import get_logger
|
||||
from orgs.utils import org_aware_func
|
||||
|
||||
@@ -32,13 +31,13 @@ def verify_accounts_connectivity_util(accounts, task_name):
|
||||
asset_ids = [a.asset_id for a in accounts]
|
||||
assets = Asset.objects.filter(id__in=asset_ids)
|
||||
|
||||
gateways = assets.filter(platform__name=GATEWAY_NAME)
|
||||
gateways = assets.gateways()
|
||||
verify_connectivity_util(
|
||||
gateways, AutomationTypes.verify_gateway_account,
|
||||
accounts, task_name
|
||||
)
|
||||
|
||||
common_assets = assets.exclude(platform__name=GATEWAY_NAME)
|
||||
common_assets = assets.gateways(0)
|
||||
verify_connectivity_util(
|
||||
common_assets, AutomationTypes.verify_account,
|
||||
accounts, task_name
|
||||
@@ -46,8 +45,12 @@ def verify_accounts_connectivity_util(accounts, task_name):
|
||||
|
||||
|
||||
@shared_task(
|
||||
queue="ansible", verbose_name=_('Verify asset account availability'),
|
||||
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None)
|
||||
queue="ansible",
|
||||
verbose_name=_('Verify asset account availability'),
|
||||
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None),
|
||||
description=_(
|
||||
"When clicking 'Test' in 'Console - Asset details - Accounts' this task will be executed"
|
||||
)
|
||||
)
|
||||
def verify_accounts_connectivity_task(account_ids):
|
||||
from accounts.models import Account, VerifyAccountAutomation
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
{% load i18n %}
|
||||
|
||||
<h3>{% trans 'Gather account change information' %}</h3>
|
||||
<table style="width: 100%; border-collapse: collapse; max-width: 100%; text-align: left; margin-top: 20px;">
|
||||
<h3></h3>
|
||||
<table style="width: 100%; border-collapse: collapse; table-layout: fixed; text-align: left; margin-top: 20px;">
|
||||
<caption></caption>
|
||||
<tr style="background-color: #f2f2f2;">
|
||||
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Asset' %}</th>
|
||||
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Added account' %}</th>
|
||||
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Deleted account' %}</th>
|
||||
<th style="border: 1px solid #ddd; padding: 15px; text-align: left; vertical-align: top; line-height: 1.5;">
|
||||
{% trans 'Asset' %}
|
||||
</th>
|
||||
<th style="border: 1px solid #ddd; padding: 15px; text-align: left; vertical-align: top; line-height: 1.5;">
|
||||
{% trans 'Added account' %}
|
||||
</th>
|
||||
<th style="border: 1px solid #ddd; padding: 15px; text-align: left; vertical-align: top; line-height: 1.5;">
|
||||
{% trans 'Deleted account' %}
|
||||
</th>
|
||||
</tr>
|
||||
{% for name, change in change_info.items %}
|
||||
<tr style="{% cycle 'background-color: #ebf5ff;' 'background-color: #fff;' %}">
|
||||
<td style="border: 1px solid #ddd; padding: 10px;">{{ name }}</td>
|
||||
<td style="border: 1px solid #ddd; padding: 10px;">{{ change.add_usernames }}</td>
|
||||
<td style="border: 1px solid #ddd; padding: 10px;">{{ change.remove_usernames }}</td>
|
||||
<td style="border: 1px solid #ddd; padding: 10px; text-align: left; vertical-align: top;">
|
||||
{{ name | safe }}
|
||||
</td>
|
||||
<td style="border: 1px solid #ddd; padding: 10px; text-align: left; vertical-align: top;">
|
||||
{{ change.add_usernames | join:" " | safe }}
|
||||
</td>
|
||||
<td style="border: 1px solid #ddd; padding: 10px; text-align: left; vertical-align: top;">
|
||||
{{ change.remove_usernames | join:" " | safe }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
@@ -8,3 +8,4 @@ class ActionChoices(models.TextChoices):
|
||||
review = 'review', _('Review')
|
||||
warning = 'warning', _('Warn')
|
||||
notice = 'notice', _('Notify')
|
||||
notify_and_warn = 'notify_and_warn', _('Notify and warn')
|
||||
|
||||
@@ -62,7 +62,7 @@ class ActionAclSerializer(serializers.Serializer):
|
||||
self.set_action_choices()
|
||||
|
||||
class Meta:
|
||||
action_choices_exclude = [ActionChoices.warning]
|
||||
action_choices_exclude = [ActionChoices.warning, ActionChoices.notify_and_warn]
|
||||
|
||||
def set_action_choices(self):
|
||||
field_action = self.fields.get("action")
|
||||
|
||||
@@ -7,3 +7,4 @@ from .node import *
|
||||
from .platform import *
|
||||
from .protocol import *
|
||||
from .tree import *
|
||||
from .my_asset import *
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
#
|
||||
from collections import defaultdict
|
||||
|
||||
import django_filters
|
||||
from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import gettext as _
|
||||
from django_filters import rest_framework as drf_filters
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
@@ -22,6 +22,7 @@ from common.drf.filters import BaseFilterSet, AttrRulesFilterBackend
|
||||
from common.utils import get_logger, is_uuid
|
||||
from orgs.mixins import generics
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from ...const import GATEWAY_NAME
|
||||
from ...notifications import BulkUpdatePlatformSkipAssetUserMsg
|
||||
|
||||
logger = get_logger(__file__)
|
||||
@@ -32,31 +33,32 @@ __all__ = [
|
||||
|
||||
|
||||
class AssetFilterSet(BaseFilterSet):
|
||||
platform = django_filters.CharFilter(method='filter_platform')
|
||||
exclude_platform = django_filters.CharFilter(field_name="platform__name", lookup_expr='exact', exclude=True)
|
||||
domain = django_filters.CharFilter(method='filter_domain')
|
||||
type = django_filters.CharFilter(field_name="platform__type", lookup_expr="exact")
|
||||
category = django_filters.CharFilter(field_name="platform__category", lookup_expr="exact")
|
||||
protocols = django_filters.CharFilter(method='filter_protocols')
|
||||
domain_enabled = django_filters.BooleanFilter(
|
||||
platform = drf_filters.CharFilter(method='filter_platform')
|
||||
is_gateway = drf_filters.BooleanFilter(method='filter_is_gateway')
|
||||
exclude_platform = drf_filters.CharFilter(field_name="platform__name", lookup_expr='exact', exclude=True)
|
||||
domain = drf_filters.CharFilter(method='filter_domain')
|
||||
type = drf_filters.CharFilter(field_name="platform__type", lookup_expr="exact")
|
||||
category = drf_filters.CharFilter(field_name="platform__category", lookup_expr="exact")
|
||||
protocols = drf_filters.CharFilter(method='filter_protocols')
|
||||
domain_enabled = drf_filters.BooleanFilter(
|
||||
field_name="platform__domain_enabled", lookup_expr="exact"
|
||||
)
|
||||
ping_enabled = django_filters.BooleanFilter(
|
||||
ping_enabled = drf_filters.BooleanFilter(
|
||||
field_name="platform__automation__ping_enabled", lookup_expr="exact"
|
||||
)
|
||||
gather_facts_enabled = django_filters.BooleanFilter(
|
||||
gather_facts_enabled = drf_filters.BooleanFilter(
|
||||
field_name="platform__automation__gather_facts_enabled", lookup_expr="exact"
|
||||
)
|
||||
change_secret_enabled = django_filters.BooleanFilter(
|
||||
change_secret_enabled = drf_filters.BooleanFilter(
|
||||
field_name="platform__automation__change_secret_enabled", lookup_expr="exact"
|
||||
)
|
||||
push_account_enabled = django_filters.BooleanFilter(
|
||||
push_account_enabled = drf_filters.BooleanFilter(
|
||||
field_name="platform__automation__push_account_enabled", lookup_expr="exact"
|
||||
)
|
||||
verify_account_enabled = django_filters.BooleanFilter(
|
||||
verify_account_enabled = drf_filters.BooleanFilter(
|
||||
field_name="platform__automation__verify_account_enabled", lookup_expr="exact"
|
||||
)
|
||||
gather_accounts_enabled = django_filters.BooleanFilter(
|
||||
gather_accounts_enabled = drf_filters.BooleanFilter(
|
||||
field_name="platform__automation__gather_accounts_enabled", lookup_expr="exact"
|
||||
)
|
||||
|
||||
@@ -71,9 +73,16 @@ class AssetFilterSet(BaseFilterSet):
|
||||
def filter_platform(queryset, name, value):
|
||||
if value.isdigit():
|
||||
return queryset.filter(platform_id=value)
|
||||
elif value == GATEWAY_NAME:
|
||||
return queryset.filter(platform__name__istartswith=GATEWAY_NAME)
|
||||
else:
|
||||
return queryset.filter(platform__name=value)
|
||||
|
||||
@staticmethod
|
||||
def filter_is_gateway(queryset, name, value):
|
||||
queryset = queryset.gateways(value)
|
||||
return queryset
|
||||
|
||||
@staticmethod
|
||||
def filter_domain(queryset, name, value):
|
||||
if is_uuid(value):
|
||||
@@ -298,6 +307,7 @@ class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
|
||||
def check_permissions(self, request):
|
||||
action_perm_require = {
|
||||
"refresh": "assets.refresh_assethardwareinfo",
|
||||
"test": "assets.test_assetconnectivity",
|
||||
}
|
||||
_action = request.data.get("action")
|
||||
perm_required = action_perm_require.get(_action)
|
||||
|
||||
@@ -2,7 +2,7 @@ from typing import List
|
||||
|
||||
from rest_framework.request import Request
|
||||
|
||||
from assets.models import Node, Platform, Protocol
|
||||
from assets.models import Node, Platform, Protocol, MyAsset
|
||||
from assets.utils import get_node_from_request, is_query_node_all_assets
|
||||
from common.utils import lazyproperty, timeit
|
||||
|
||||
@@ -82,6 +82,7 @@ class SerializeToTreeNodeMixin:
|
||||
|
||||
data = []
|
||||
root_assets_count = 0
|
||||
MyAsset.set_asset_custom_value(assets, self.request.user)
|
||||
for asset in assets:
|
||||
platform = platform_map.get(asset.platform_id)
|
||||
if not platform:
|
||||
|
||||
13
apps/assets/api/my_asset.py
Normal file
13
apps/assets/api/my_asset.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from common.api import GenericViewSet
|
||||
from rest_framework.mixins import CreateModelMixin
|
||||
from common.permissions import IsValidUser
|
||||
from ..serializers import MyAssetSerializer
|
||||
|
||||
__all__ = ['MyAssetViewSet']
|
||||
|
||||
|
||||
class MyAssetViewSet(CreateModelMixin, GenericViewSet):
|
||||
serializer_class = MyAssetSerializer
|
||||
permission_classes = (IsValidUser,)
|
||||
@@ -1,3 +1,5 @@
|
||||
from django.db.models import Count
|
||||
from django_filters import rest_framework as filters
|
||||
from rest_framework import generics
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
@@ -5,7 +7,7 @@ from rest_framework.response import Response
|
||||
|
||||
from assets.const import AllTypes
|
||||
from assets.models import Platform, Node, Asset, PlatformProtocol
|
||||
from assets.serializers import PlatformSerializer, PlatformProtocolSerializer
|
||||
from assets.serializers import PlatformSerializer, PlatformProtocolSerializer, PlatformListSerializer
|
||||
from common.api import JMSModelViewSet
|
||||
from common.permissions import IsValidUser
|
||||
from common.serializers import GroupedChoiceSerializer
|
||||
@@ -13,13 +15,22 @@ from common.serializers import GroupedChoiceSerializer
|
||||
__all__ = ['AssetPlatformViewSet', 'PlatformAutomationMethodsApi', 'PlatformProtocolViewSet']
|
||||
|
||||
|
||||
class PlatformFilter(filters.FilterSet):
|
||||
name__startswith = filters.CharFilter(field_name='name', lookup_expr='istartswith')
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = ['name', 'category', 'type']
|
||||
|
||||
|
||||
class AssetPlatformViewSet(JMSModelViewSet):
|
||||
queryset = Platform.objects.all()
|
||||
serializer_classes = {
|
||||
'default': PlatformSerializer,
|
||||
'list': PlatformListSerializer,
|
||||
'categories': GroupedChoiceSerializer,
|
||||
}
|
||||
filterset_fields = ['name', 'category', 'type']
|
||||
filterset_class = PlatformFilter
|
||||
search_fields = ['name']
|
||||
ordering = ['-internal', 'name']
|
||||
rbac_perms = {
|
||||
@@ -31,8 +42,8 @@ class AssetPlatformViewSet(JMSModelViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
# 因为没有走分页逻辑,所以需要这里 prefetch
|
||||
queryset = super().get_queryset().prefetch_related(
|
||||
'protocols', 'automation', 'labels', 'labels__label',
|
||||
queryset = super().get_queryset().annotate(assets_amount=Count('assets')).prefetch_related(
|
||||
'protocols', 'automation', 'labels', 'labels__label'
|
||||
)
|
||||
queryset = queryset.filter(type__in=AllTypes.get_types_values())
|
||||
return queryset
|
||||
|
||||
@@ -39,16 +39,16 @@ class NodeChildrenApi(generics.ListCreateAPIView):
|
||||
self.instance = self.get_object()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
data = serializer.validated_data
|
||||
_id = data.get("id")
|
||||
value = data.get("value")
|
||||
if value:
|
||||
children = self.instance.get_children()
|
||||
if children.filter(value=value).exists():
|
||||
raise JMSException(_('The same level node name cannot be the same'))
|
||||
else:
|
||||
value = self.instance.get_next_child_preset_name()
|
||||
with NodeAddChildrenLock(self.instance):
|
||||
data = serializer.validated_data
|
||||
_id = data.get("id")
|
||||
value = data.get("value")
|
||||
if value:
|
||||
children = self.instance.get_children()
|
||||
if children.filter(value=value).exists():
|
||||
raise JMSException(_('The same level node name cannot be the same'))
|
||||
else:
|
||||
value = self.instance.get_next_child_preset_name()
|
||||
node = self.instance.create_child(value=value, _id=_id)
|
||||
# 避免查询 full value
|
||||
node._full_value = node.value
|
||||
|
||||
@@ -113,11 +113,7 @@ class BasePlaybookManager:
|
||||
if not data:
|
||||
data = automation_params.get(method_id, {})
|
||||
params = serializer(data).data
|
||||
return {
|
||||
field_name: automation_params.get(field_name, '')
|
||||
if not params[field_name] else params[field_name]
|
||||
for field_name in params
|
||||
}
|
||||
return params
|
||||
|
||||
@property
|
||||
def platform_automation_methods(self):
|
||||
@@ -174,6 +170,7 @@ class BasePlaybookManager:
|
||||
result = self.write_cert_to_file(
|
||||
os.path.join(cert_dir, f), specific.get(f)
|
||||
)
|
||||
os.chmod(result, 0o600)
|
||||
host['jms_asset']['secret_info'][f] = result
|
||||
return host
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Get info
|
||||
@@ -12,9 +15,9 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
filter: version
|
||||
register: db_info
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Get info
|
||||
@@ -11,6 +15,10 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_db: "{{ jms_asset.spec_info.db_name }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||
register: db_info
|
||||
|
||||
- name: Define info by set_fact
|
||||
|
||||
@@ -12,7 +12,12 @@
|
||||
cpu_cores: "{{ ansible_processor_cores }}"
|
||||
cpu_vcpus: "{{ ansible_processor_vcpus }}"
|
||||
memory: "{{ ansible_memtotal_mb / 1024 | round(2) }}"
|
||||
disk_total: "{{ (ansible_mounts | map(attribute='size_total') | sum / 1024 / 1024 / 1024) | round(2) }}"
|
||||
disk_total: |-
|
||||
{% set ns = namespace(total=0) %}
|
||||
{%- for name, dev in ansible_devices.items() if dev.removable == '0' and dev.host != '' -%}
|
||||
{%- set ns.total = ns.total + ( dev.sectors | int * dev.sectorsize | int ) -%}
|
||||
{%- endfor -%}
|
||||
{{- (ns.total / 1024 / 1024 / 1024) | round(2) -}}
|
||||
distribution: "{{ ansible_distribution }}"
|
||||
distribution_version: "{{ ansible_distribution_version }}"
|
||||
arch: "{{ ansible_architecture }}"
|
||||
|
||||
@@ -11,8 +11,10 @@
|
||||
vendor: "{{ ansible_system_vendor }}"
|
||||
model: "{{ ansible_product_name }}"
|
||||
sn: "{{ ansible_product_serial }}"
|
||||
cpu_count: "{{ ansible_processor_count }}"
|
||||
cpu_cores: "{{ ansible_processor_cores }}"
|
||||
cpu_vcpus: "{{ ansible_processor_vcpus }}"
|
||||
memory: "{{ ansible_memtotal_mb }}"
|
||||
memory: "{{ (ansible_memtotal_mb / 1024) | round(2) }}"
|
||||
|
||||
- debug:
|
||||
var: info
|
||||
|
||||
@@ -13,4 +13,3 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_secret_type: "{{ jms_account.secret_type }}"
|
||||
login_private_key_path: "{{ jms_account.private_key_path }}"
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Test MySQL connection
|
||||
@@ -12,7 +15,7 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
filter: version
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Test PostgreSQL connection
|
||||
@@ -11,5 +15,9 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_db: "{{ jms_asset.spec_info.db_name }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
|
||||
@@ -115,7 +115,7 @@ class PingGatewayManager:
|
||||
|
||||
@staticmethod
|
||||
def before_runner_start():
|
||||
print(">>> 开始执行测试网关可连接性任务")
|
||||
print(_(">>> Start executing the task to test gateway connectivity"))
|
||||
|
||||
def get_accounts(self, gateway):
|
||||
account = gateway.select_account
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from .automation import *
|
||||
from .base import *
|
||||
from .category import *
|
||||
from .database import *
|
||||
from .host import *
|
||||
from .platform import *
|
||||
from .protocol import *
|
||||
from .types import *
|
||||
|
||||
@@ -112,7 +112,7 @@ class BaseType(TextChoices):
|
||||
|
||||
@classmethod
|
||||
def get_choices(cls):
|
||||
if not settings.XPACK_ENABLED:
|
||||
if not settings.XPACK_LICENSE_IS_VALID:
|
||||
choices = [(tp.value, tp.label) for tp in cls.get_community_types()]
|
||||
else:
|
||||
choices = cls.choices
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from django.db.models import TextChoices
|
||||
|
||||
from .base import BaseType
|
||||
|
||||
|
||||
@@ -117,5 +119,13 @@ class DatabaseTypes(BaseType):
|
||||
@classmethod
|
||||
def get_community_types(cls):
|
||||
return [
|
||||
cls.MYSQL, cls.MARIADB, cls.MONGODB, cls.REDIS
|
||||
cls.MYSQL, cls.MARIADB, cls.POSTGRESQL,
|
||||
cls.MONGODB, cls.REDIS,
|
||||
]
|
||||
|
||||
|
||||
class PostgresqlSSLMode(TextChoices):
|
||||
PREFER = 'prefer', 'Prefer'
|
||||
REQUIRE = 'require', 'Require'
|
||||
VERIFY_CA = 'verify-ca', 'Verify CA'
|
||||
VERIFY_FULL = 'verify-full', 'Verify Full'
|
||||
|
||||
@@ -19,7 +19,7 @@ class HostTypes(BaseType):
|
||||
'charset': 'utf-8', # default
|
||||
'domain_enabled': True,
|
||||
'su_enabled': True,
|
||||
'su_methods': ['sudo', 'su'],
|
||||
'su_methods': ['sudo', 'su', 'only_sudo', 'only_su'],
|
||||
},
|
||||
cls.WINDOWS: {
|
||||
'su_enabled': False,
|
||||
|
||||
11
apps/assets/const/platform.py
Normal file
11
apps/assets/const/platform.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.db.models import TextChoices
|
||||
|
||||
|
||||
class SuMethodChoices(TextChoices):
|
||||
sudo = "sudo", "sudo su -"
|
||||
su = "su", "su - "
|
||||
only_sudo = "only_sudo", "sudo su"
|
||||
only_su = "only_su", "su"
|
||||
enable = "enable", "enable"
|
||||
super = "super", "super 15"
|
||||
super_level = "super_level", "super level 15"
|
||||
@@ -45,6 +45,12 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
||||
'default': False,
|
||||
'label': _('Old SSH version'),
|
||||
'help_text': _('Old SSH version like openssh 5.x or 6.x')
|
||||
},
|
||||
'nc': {
|
||||
'type': 'bool',
|
||||
'default': False,
|
||||
'label': 'Netcat (nc)',
|
||||
'help_text': _('Netcat help text')
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -80,7 +86,18 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
||||
'choices': [('any', _('Any')), ('rdp', 'RDP'), ('tls', 'TLS'), ('nla', 'NLA')],
|
||||
'default': 'any',
|
||||
'label': _('Security'),
|
||||
'help_text': _("Security layer to use for the connection")
|
||||
'help_text': _("Security layer to use for the connection:<br>"
|
||||
"Any<br>"
|
||||
"Automatically select the security mode based on the security protocols "
|
||||
"supported by both the client and the server<br>"
|
||||
"RDP<br>"
|
||||
"Legacy RDP encryption. This mode is generally only used for older Windows "
|
||||
"servers or in cases where a standard Windows login screen is desired<br>"
|
||||
"TLS<br>"
|
||||
"RDP authentication and encryption implemented via TLS.<br>"
|
||||
"NLA<br>"
|
||||
"This mode uses TLS encryption and requires the username and password "
|
||||
"to be given in advance")
|
||||
},
|
||||
'ad_domain': {
|
||||
'type': 'str',
|
||||
@@ -208,6 +225,12 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
||||
'default': 'admin',
|
||||
'label': _('Auth source'),
|
||||
'help_text': _('The database to authenticate against')
|
||||
},
|
||||
'connection_options': {
|
||||
'type': 'str',
|
||||
'default': '',
|
||||
'label': _('Connect options'),
|
||||
'help_text': _('The connection specific options eg. retryWrites=false&retryReads=false')
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -289,23 +312,17 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
||||
'setting': {
|
||||
'api_mode': {
|
||||
'type': 'choice',
|
||||
'default': 'gpt-3.5-turbo',
|
||||
'default': 'gpt-4o-mini',
|
||||
'label': _('API mode'),
|
||||
'choices': [
|
||||
('gpt-3.5-turbo', 'GPT-3.5 Turbo'),
|
||||
('gpt-3.5-turbo-1106', 'GPT-3.5 Turbo 1106'),
|
||||
('gpt-4o-mini', 'GPT-4o-mini'),
|
||||
('gpt-4o', 'GPT-4o'),
|
||||
('gpt-4-turbo', 'GPT-4 Turbo'),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if settings.XPACK_LICENSE_IS_VALID:
|
||||
choices = protocols[cls.chatgpt]['setting']['api_mode']['choices']
|
||||
choices.extend([
|
||||
('gpt-4', 'GPT-4'),
|
||||
('gpt-4-turbo', 'GPT-4 Turbo'),
|
||||
('gpt-4o', 'GPT-4o'),
|
||||
])
|
||||
return protocols
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -3,6 +3,7 @@ from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from common.db.models import ChoicesMixin
|
||||
@@ -29,15 +30,15 @@ class AllTypes(ChoicesMixin):
|
||||
|
||||
@classmethod
|
||||
def choices(cls):
|
||||
return lazy(cls.get_choices, list)()
|
||||
|
||||
@classmethod
|
||||
def get_choices(cls):
|
||||
choices = []
|
||||
for tp in cls.includes:
|
||||
choices.extend(tp.get_choices())
|
||||
return choices
|
||||
|
||||
@classmethod
|
||||
def get_choices(cls):
|
||||
return cls.choices()
|
||||
|
||||
@classmethod
|
||||
def filter_choices(cls, category):
|
||||
choices = dict(cls.category_types()).get(category, cls).get_choices()
|
||||
@@ -171,12 +172,9 @@ class AllTypes(ChoicesMixin):
|
||||
(Category.DEVICE, DeviceTypes),
|
||||
(Category.DATABASE, DatabaseTypes),
|
||||
(Category.WEB, WebTypes),
|
||||
(Category.CLOUD, CloudTypes),
|
||||
(Category.CUSTOM, CustomTypes)
|
||||
]
|
||||
if settings.XPACK_ENABLED:
|
||||
types.extend([
|
||||
(Category.CLOUD, CloudTypes),
|
||||
(Category.CUSTOM, CustomTypes),
|
||||
])
|
||||
return types
|
||||
|
||||
@classmethod
|
||||
|
||||
28
apps/assets/migrations/0004_auto_20240709_1819.py
Normal file
28
apps/assets/migrations/0004_auto_20240709_1819.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 4.1.13 on 2024-07-09 10:19
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_platform_protocol_primary(apps, schema_editor):
|
||||
platform_model = apps.get_model('assets', 'Platform')
|
||||
platforms = platform_model.objects.all()
|
||||
|
||||
for platform in platforms:
|
||||
p = platform.protocols.filter(primary=True).first()
|
||||
if p:
|
||||
continue
|
||||
p = platform.protocols.first()
|
||||
if not p:
|
||||
continue
|
||||
p.primary = True
|
||||
p.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('assets', '0003_auto_20180109_2331'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_platform_protocol_primary)
|
||||
]
|
||||
35
apps/assets/migrations/0005_myasset.py
Normal file
35
apps/assets/migrations/0005_myasset.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# Generated by Django 4.1.13 on 2024-08-06 09:11
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('assets', '0004_auto_20240709_1819'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='MyAsset',
|
||||
fields=[
|
||||
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')),
|
||||
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(default='', max_length=128, verbose_name='Custom Name')),
|
||||
('comment', models.CharField(default='', max_length=512, verbose_name='Custom Comment')),
|
||||
('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='my_assets', to='assets.asset')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'My assets',
|
||||
'unique_together': {('user', 'asset')},
|
||||
},
|
||||
),
|
||||
]
|
||||
23
apps/assets/migrations/0006_database_pg_ssl_mode.py
Normal file
23
apps/assets/migrations/0006_database_pg_ssl_mode.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.1.13 on 2024-09-13 08:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('assets', '0005_myasset'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='database',
|
||||
name='pg_ssl_mode',
|
||||
field=models.CharField(choices=[
|
||||
('prefer', 'Prefer'),
|
||||
('require', 'Require'),
|
||||
('verify-ca', 'Verify CA'),
|
||||
('verify-full', 'Verify Full')
|
||||
], default='prefer',
|
||||
max_length=16, verbose_name='Postgresql SSL mode'),
|
||||
),
|
||||
]
|
||||
@@ -7,3 +7,4 @@ from .domain import *
|
||||
from .node import *
|
||||
from .favorite_asset import *
|
||||
from .automations import *
|
||||
from .my_asset import *
|
||||
|
||||
@@ -38,6 +38,13 @@ class AssetQuerySet(models.QuerySet):
|
||||
def valid(self):
|
||||
return self.active()
|
||||
|
||||
def gateways(self, is_gateway=1):
|
||||
kwargs = {'platform__name__startswith': 'Gateway'}
|
||||
if is_gateway:
|
||||
return self.filter(**kwargs)
|
||||
else:
|
||||
return self.exclude(**kwargs)
|
||||
|
||||
def has_protocol(self, name):
|
||||
return self.filter(protocols__contains=name)
|
||||
|
||||
@@ -158,10 +165,16 @@ class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin,
|
||||
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
address = models.CharField(max_length=767, verbose_name=_('Address'), db_index=True)
|
||||
platform = models.ForeignKey(Platform, on_delete=models.PROTECT, verbose_name=_("Platform"), related_name='assets')
|
||||
domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets',
|
||||
verbose_name=_("Zone"), on_delete=models.SET_NULL)
|
||||
nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes"))
|
||||
platform = models.ForeignKey(
|
||||
Platform, on_delete=models.PROTECT, verbose_name=_("Platform"), related_name='assets'
|
||||
)
|
||||
domain = models.ForeignKey(
|
||||
"assets.Domain", null=True, blank=True, related_name='assets',
|
||||
verbose_name=_("Zone"), on_delete=models.SET_NULL
|
||||
)
|
||||
nodes = models.ManyToManyField(
|
||||
'assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes")
|
||||
)
|
||||
is_active = models.BooleanField(default=True, verbose_name=_('Active'))
|
||||
gathered_info = models.JSONField(verbose_name=_('Gathered info'), default=dict, blank=True) # 资产的一些信息,如 硬件信息
|
||||
custom_info = models.JSONField(verbose_name=_('Custom info'), default=dict)
|
||||
@@ -173,7 +186,7 @@ class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin,
|
||||
|
||||
def get_labels(self):
|
||||
from labels.models import Label, LabeledResource
|
||||
res_type = ContentType.objects.get_for_model(self.__class__)
|
||||
res_type = ContentType.objects.get_for_model(self.__class__.label_model())
|
||||
label_ids = LabeledResource.objects.filter(res_type=res_type, res_id=self.id) \
|
||||
.values_list('label_id', flat=True)
|
||||
return Label.objects.filter(id__in=label_ids)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from assets.const import PostgresqlSSLMode
|
||||
from common.db.fields import EncryptTextField
|
||||
from .common import Asset
|
||||
|
||||
@@ -12,6 +13,10 @@ class Database(Asset):
|
||||
client_cert = EncryptTextField(verbose_name=_("Client cert"), blank=True)
|
||||
client_key = EncryptTextField(verbose_name=_("Client key"), blank=True)
|
||||
allow_invalid_cert = models.BooleanField(default=False, verbose_name=_('Allow invalid cert'))
|
||||
pg_ssl_mode = models.CharField(
|
||||
max_length=16, choices=PostgresqlSSLMode.choices,
|
||||
default=PostgresqlSSLMode.PREFER, verbose_name=_('Postgresql SSL mode')
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return '{}({}://{}/{})'.format(self.name, self.type, self.address, self.db_name)
|
||||
|
||||
@@ -31,7 +31,7 @@ class Domain(LabeledMixin, JMSOrgBaseModel):
|
||||
|
||||
@lazyproperty
|
||||
def assets_amount(self):
|
||||
return self.assets.count()
|
||||
return self.assets.gateways(0).count()
|
||||
|
||||
def random_gateway(self):
|
||||
gateways = [gw for gw in self.active_gateways if gw.is_connective]
|
||||
|
||||
@@ -16,7 +16,7 @@ __all__ = ['Gateway']
|
||||
class GatewayManager(OrgManager):
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.filter(platform__name=GATEWAY_NAME)
|
||||
queryset = queryset.filter(platform__name__startswith=GATEWAY_NAME)
|
||||
return queryset
|
||||
|
||||
def bulk_create(self, objs, batch_size=None, ignore_conflicts=False):
|
||||
@@ -33,10 +33,6 @@ class Gateway(Host):
|
||||
proxy = True
|
||||
verbose_name = _("Gateway")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.platform = self.default_platform()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def default_platform(cls):
|
||||
return Platform.objects.get(name=GATEWAY_NAME, internal=True)
|
||||
|
||||
43
apps/assets/models/my_asset.py
Normal file
43
apps/assets/models/my_asset.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.db.models import JMSBaseModel
|
||||
|
||||
__all__ = ['MyAsset']
|
||||
|
||||
|
||||
class MyAsset(JMSBaseModel):
|
||||
user = models.ForeignKey('users.User', on_delete=models.CASCADE)
|
||||
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, related_name='my_assets')
|
||||
name = models.CharField(verbose_name=_("Custom Name"), max_length=128, default='')
|
||||
comment = models.CharField(verbose_name=_("Custom Comment"), max_length=512, default='')
|
||||
custom_fields = ['name', 'comment']
|
||||
|
||||
class Meta:
|
||||
unique_together = ('user', 'asset')
|
||||
verbose_name = _("My assets")
|
||||
|
||||
def custom_to_dict(self):
|
||||
data = {}
|
||||
for field in self.custom_fields:
|
||||
value = getattr(self, field)
|
||||
if value == "":
|
||||
continue
|
||||
data.update({field: value})
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def set_asset_custom_value(assets, user):
|
||||
my_assets = MyAsset.objects.filter(asset__in=assets, user=user).all()
|
||||
customs = {my_asset.asset.id: my_asset.custom_to_dict() for my_asset in my_assets}
|
||||
for asset in assets:
|
||||
custom = customs.get(asset.id)
|
||||
if not custom:
|
||||
continue
|
||||
for field, value in custom.items():
|
||||
if not hasattr(asset, field):
|
||||
continue
|
||||
setattr(asset, field, value)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.user}-{self.asset}'
|
||||
@@ -1,7 +1,7 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from assets.const import AllTypes, Category, Protocol
|
||||
from assets.const import AllTypes, Category, Protocol, SuMethodChoices
|
||||
from common.db.fields import JsonDictTextField
|
||||
from common.db.models import JMSBaseModel
|
||||
|
||||
@@ -111,6 +111,10 @@ class Platform(LabeledMixin, JMSBaseModel):
|
||||
def type_constraints(self):
|
||||
return AllTypes.get_constraints(self.category, self.type)
|
||||
|
||||
@lazyproperty
|
||||
def assets_amount(self):
|
||||
return self.assets.count()
|
||||
|
||||
@classmethod
|
||||
def default(cls):
|
||||
linux, created = cls.objects.get_or_create(
|
||||
@@ -127,6 +131,17 @@ class Platform(LabeledMixin, JMSBaseModel):
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def ansible_become_method(self):
|
||||
su_method = self.su_method or SuMethodChoices.sudo
|
||||
if su_method in [SuMethodChoices.sudo, SuMethodChoices.only_sudo]:
|
||||
method = SuMethodChoices.sudo
|
||||
elif su_method in [SuMethodChoices.su, SuMethodChoices.only_su]:
|
||||
method = SuMethodChoices.su
|
||||
else:
|
||||
method = su_method
|
||||
return method
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
@@ -9,3 +9,4 @@ from .favorite_asset import *
|
||||
from .gateway import *
|
||||
from .node import *
|
||||
from .platform import *
|
||||
from .my_asset import *
|
||||
|
||||
@@ -18,7 +18,7 @@ from common.serializers.fields import LabeledChoiceField
|
||||
from labels.models import Label
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from ...const import Category, AllTypes
|
||||
from ...models import Asset, Node, Platform, Protocol
|
||||
from ...models import Asset, Node, Platform, Protocol, Host, Device, Database, Cloud, Web, Custom
|
||||
|
||||
__all__ = [
|
||||
'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer',
|
||||
@@ -31,6 +31,12 @@ __all__ = [
|
||||
class AssetProtocolsSerializer(serializers.ModelSerializer):
|
||||
port = serializers.IntegerField(required=False, allow_null=True, max_value=65535, min_value=0)
|
||||
|
||||
def get_render_help_text(self):
|
||||
if self.parent and self.parent.many:
|
||||
return _('Protocols, format is ["protocol/port"]')
|
||||
else:
|
||||
return _('Protocol, format is name/port')
|
||||
|
||||
def to_file_representation(self, data):
|
||||
return '{name}/{port}'.format(**data)
|
||||
|
||||
@@ -97,6 +103,9 @@ class AssetAccountSerializer(AccountSerializer):
|
||||
attrs = super().validate(attrs)
|
||||
return self.set_secret(attrs)
|
||||
|
||||
def get_render_help_text(self):
|
||||
return _('Accounts, format [{"name": "x", "username": "x", "secret": "x", "secret_type": "password"}]')
|
||||
|
||||
class Meta(AccountSerializer.Meta):
|
||||
fields = [
|
||||
f for f in AccountSerializer.Meta.fields
|
||||
@@ -121,19 +130,30 @@ class AccountSecretSerializer(SecretReadableMixin, CommonModelSerializer):
|
||||
}
|
||||
|
||||
|
||||
class NodeDisplaySerializer(serializers.ListField):
|
||||
def get_render_help_text(self):
|
||||
return _('Node path, format ["/org_name/node_name"], if node not exist, will create it')
|
||||
|
||||
def to_internal_value(self, data):
|
||||
return data
|
||||
|
||||
def to_representation(self, data):
|
||||
return data
|
||||
|
||||
|
||||
class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, WritableNestedModelSerializer):
|
||||
category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
|
||||
type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type'))
|
||||
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=())
|
||||
accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, write_only=True, label=_('Accounts'))
|
||||
nodes_display = serializers.ListField(read_only=False, required=False, label=_("Node path"))
|
||||
nodes_display = NodeDisplaySerializer(read_only=False, required=False, label=_("Node path"))
|
||||
_accounts = None
|
||||
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields_mini = ['id', 'name', 'address']
|
||||
fields_small = fields_mini + ['is_active', 'comment']
|
||||
fields_fk = ['domain', 'platform']
|
||||
fields_mini = ['id', 'name', 'address'] + fields_fk
|
||||
fields_small = fields_mini + ['is_active', 'comment']
|
||||
fields_m2m = [
|
||||
'nodes', 'labels', 'protocols',
|
||||
'nodes_display', 'accounts',
|
||||
@@ -289,6 +309,17 @@ class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, Writa
|
||||
})
|
||||
return protocols_data_map.values()
|
||||
|
||||
def validate_platform(self, platform_data):
|
||||
check_models = {Host, Device, Database, Cloud, Web, Custom}
|
||||
if self.Meta.model not in check_models:
|
||||
return platform_data
|
||||
model_name = self.Meta.model.__name__.lower()
|
||||
if model_name != platform_data.category:
|
||||
raise serializers.ValidationError({
|
||||
'platform': f"Platform does not match: {platform_data.name}"
|
||||
})
|
||||
return platform_data
|
||||
|
||||
@staticmethod
|
||||
def update_account_su_from(accounts, include_su_from_accounts):
|
||||
if not include_su_from_accounts:
|
||||
|
||||
@@ -16,6 +16,7 @@ class CustomSerializer(AssetSerializer):
|
||||
class Meta(AssetSerializer.Meta):
|
||||
model = Custom
|
||||
fields = AssetSerializer.Meta.fields + ['custom_info']
|
||||
fields_unimport_template = ['custom_info']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -16,9 +16,14 @@ class DatabaseSerializer(AssetSerializer):
|
||||
model = Database
|
||||
extra_fields = [
|
||||
'db_name', 'use_ssl', 'ca_cert', 'client_cert',
|
||||
'client_key', 'allow_invalid_cert'
|
||||
'client_key', 'allow_invalid_cert', 'pg_ssl_mode'
|
||||
]
|
||||
fields = AssetSerializer.Meta.fields + extra_fields
|
||||
extra_kwargs = {
|
||||
'ca_cert': {'help_text': _('CA cert help text')},
|
||||
'pg_ssl_mode': {'help_text': _('Postgresql ssl model help text')},
|
||||
}
|
||||
extra_kwargs.update(AssetSerializer.Meta.extra_kwargs)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -36,6 +36,7 @@ class BaseAutomationSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSe
|
||||
|
||||
class AutomationExecutionSerializer(serializers.ModelSerializer):
|
||||
snapshot = serializers.SerializerMethodField(label=_('Automation snapshot'))
|
||||
status = serializers.SerializerMethodField(label=_("Status"))
|
||||
trigger = LabeledChoiceField(choices=Trigger.choices, read_only=True, label=_("Trigger mode"))
|
||||
|
||||
class Meta:
|
||||
@@ -45,6 +46,14 @@ class AutomationExecutionSerializer(serializers.ModelSerializer):
|
||||
]
|
||||
fields = ['id', 'automation'] + read_only_fields
|
||||
|
||||
@staticmethod
|
||||
def get_status(obj):
|
||||
if obj.status == 'success':
|
||||
return _("Success")
|
||||
elif obj.status == 'pending':
|
||||
return _("Pending")
|
||||
return obj.status
|
||||
|
||||
@staticmethod
|
||||
def get_snapshot(obj):
|
||||
from assets.const import AutomationTypes as AssetTypes
|
||||
|
||||
@@ -57,9 +57,7 @@ class DomainSerializer(ResourceLabelsMixin, BulkOrgResourceModelSerializer):
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
queryset = queryset \
|
||||
.annotate(assets_amount=Count('assets')) \
|
||||
.prefetch_related('labels', 'labels__label')
|
||||
queryset = queryset.prefetch_related('labels', 'labels__label')
|
||||
return queryset
|
||||
|
||||
|
||||
@@ -70,7 +68,7 @@ class DomainListSerializer(DomainSerializer):
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
queryset = queryset.annotate(
|
||||
assets_amount=Count('assets', filter=~Q(assets__platform__name='Gateway'), distinct=True),
|
||||
assets_amount=Count('assets', filter=~Q(assets__platform__name__startswith='Gateway'), distinct=True),
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from common.serializers import BulkSerializerMixin
|
||||
from ..models import FavoriteAsset
|
||||
|
||||
|
||||
@@ -14,6 +14,11 @@ class GatewaySerializer(HostSerializer):
|
||||
class Meta(HostSerializer.Meta):
|
||||
model = Gateway
|
||||
|
||||
def validate_platform(self, p):
|
||||
if not p.name.startswith('Gateway'):
|
||||
raise serializers.ValidationError(_('The platform must start with Gateway'))
|
||||
return p
|
||||
|
||||
def validate_name(self, value):
|
||||
queryset = Asset.objects.filter(name=value)
|
||||
if self.instance:
|
||||
|
||||
38
apps/assets/serializers/my_asset.py
Normal file
38
apps/assets/serializers/my_asset.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from ..models import MyAsset
|
||||
|
||||
__all__ = ['MyAssetSerializer']
|
||||
|
||||
|
||||
class MyAssetSerializer(serializers.ModelSerializer):
|
||||
user = serializers.HiddenField(
|
||||
default=serializers.CurrentUserDefault()
|
||||
)
|
||||
name = serializers.CharField(label=_("Custom Name"), max_length=128, allow_blank=True, required=False)
|
||||
comment = serializers.CharField(label=_("Custom Comment"), max_length=512, allow_blank=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = MyAsset
|
||||
fields = ['user', 'asset', 'name', 'comment']
|
||||
validators = []
|
||||
|
||||
def create(self, data):
|
||||
custom_fields = MyAsset.custom_fields
|
||||
asset = data['asset']
|
||||
user = self.context['request'].user
|
||||
defaults = {field: data.get(field, '') for field in custom_fields}
|
||||
obj, created = MyAsset.objects.get_or_create(defaults=defaults, user=user, asset=asset)
|
||||
if created:
|
||||
return obj
|
||||
for field in custom_fields:
|
||||
value = data.get(field)
|
||||
if value is None:
|
||||
continue
|
||||
setattr(obj, field, value)
|
||||
obj.save()
|
||||
return obj
|
||||
@@ -3,16 +3,17 @@ from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueValidator
|
||||
|
||||
from assets.models import Asset
|
||||
from common.serializers import (
|
||||
WritableNestedModelSerializer, type_field_map, MethodSerializer,
|
||||
DictSerializer, create_serializer_class, ResourceLabelsMixin
|
||||
)
|
||||
from common.serializers.fields import LabeledChoiceField
|
||||
from common.serializers.fields import LabeledChoiceField, ObjectRelatedField
|
||||
from common.utils import lazyproperty
|
||||
from ..const import Category, AllTypes, Protocol
|
||||
from ..const import Category, AllTypes, Protocol, SuMethodChoices
|
||||
from ..models import Platform, PlatformProtocol, PlatformAutomation
|
||||
|
||||
__all__ = ["PlatformSerializer", "PlatformOpsMethodSerializer", "PlatformProtocolSerializer"]
|
||||
__all__ = ["PlatformSerializer", "PlatformOpsMethodSerializer", "PlatformProtocolSerializer", "PlatformListSerializer"]
|
||||
|
||||
|
||||
class PlatformAutomationSerializer(serializers.ModelSerializer):
|
||||
@@ -146,6 +147,10 @@ class PlatformProtocolSerializer(serializers.ModelSerializer):
|
||||
name, port = data.split('/')
|
||||
return {'name': name, 'port': port}
|
||||
|
||||
@staticmethod
|
||||
def get_render_help_text():
|
||||
return _('Protocols, format is ["protocol/port"]')
|
||||
|
||||
|
||||
class PlatformCustomField(serializers.Serializer):
|
||||
TYPE_CHOICES = [(t, t) for t, c in type_field_map.items()]
|
||||
@@ -158,13 +163,6 @@ class PlatformCustomField(serializers.Serializer):
|
||||
|
||||
|
||||
class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer):
|
||||
SU_METHOD_CHOICES = [
|
||||
("sudo", "sudo su -"),
|
||||
("su", "su - "),
|
||||
("enable", "enable"),
|
||||
("super", "super 15"),
|
||||
("super_level", "super level 15")
|
||||
]
|
||||
id = serializers.IntegerField(
|
||||
label='ID', required=False,
|
||||
validators=[UniqueValidator(queryset=Platform.objects.all())]
|
||||
@@ -175,10 +173,12 @@ class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer):
|
||||
protocols = PlatformProtocolSerializer(label=_("Protocols"), many=True, required=False)
|
||||
automation = PlatformAutomationSerializer(label=_("Automation"), required=False, default=dict)
|
||||
su_method = LabeledChoiceField(
|
||||
choices=SU_METHOD_CHOICES, label=_("Su method"),
|
||||
required=False, default="sudo", allow_null=True
|
||||
choices=SuMethodChoices.choices, label=_("Su method"),
|
||||
required=False, default=SuMethodChoices.sudo, allow_null=True
|
||||
)
|
||||
custom_fields = PlatformCustomField(label=_("Custom fields"), many=True, required=False)
|
||||
assets = ObjectRelatedField(queryset=Asset.objects, many=True, required=False, label=_('Assets'))
|
||||
assets_amount = serializers.IntegerField(label=_('Assets amount'), read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
@@ -191,7 +191,8 @@ class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer):
|
||||
'internal', 'date_created', 'date_updated',
|
||||
'created_by', 'updated_by'
|
||||
]
|
||||
fields = fields_small + [
|
||||
fields_m2m = ['assets', 'assets_amount']
|
||||
fields = fields_small + fields_m2m + [
|
||||
"protocols", "domain_enabled", "su_enabled", "su_method",
|
||||
"automation", "comment", "custom_fields", "labels"
|
||||
] + read_only_fields
|
||||
@@ -208,6 +209,7 @@ class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer):
|
||||
"help_text": _("Assets can be connected using a zone gateway")
|
||||
},
|
||||
"domain_default": {"label": _('Default Domain')},
|
||||
'assets': {'required': False, 'label': _('Assets')},
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -265,6 +267,11 @@ class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer):
|
||||
return automation
|
||||
|
||||
|
||||
class PlatformListSerializer(PlatformSerializer):
|
||||
class Meta(PlatformSerializer.Meta):
|
||||
fields = list(set(PlatformSerializer.Meta.fields + ['assets_amount']) - {'assets'})
|
||||
|
||||
|
||||
class PlatformOpsMethodSerializer(serializers.Serializer):
|
||||
id = serializers.CharField(read_only=True)
|
||||
name = serializers.CharField(max_length=50, label=_("Name"))
|
||||
|
||||
@@ -21,8 +21,10 @@ def task_activity_callback(self, pid, trigger, tp, *args, **kwargs):
|
||||
|
||||
|
||||
@shared_task(
|
||||
queue='ansible', verbose_name=_('Asset execute automation'),
|
||||
activity_callback=task_activity_callback
|
||||
queue='ansible',
|
||||
verbose_name=_('Asset execute automation'),
|
||||
activity_callback=task_activity_callback,
|
||||
description=_("Unused")
|
||||
)
|
||||
def execute_asset_automation_task(pid, trigger, tp):
|
||||
model = AutomationTypes.get_type_model(tp)
|
||||
|
||||
@@ -18,8 +18,13 @@ __all__ = [
|
||||
|
||||
|
||||
@shared_task(
|
||||
queue="ansible", verbose_name=_('Gather assets facts'),
|
||||
activity_callback=lambda self, asset_ids, org_id, *args, **kwargs: (asset_ids, org_id)
|
||||
queue="ansible",
|
||||
verbose_name=_('Gather assets facts'),
|
||||
activity_callback=lambda self, asset_ids, org_id, *args, **kwargs: (asset_ids, org_id),
|
||||
description=_(
|
||||
"""When clicking 'Refresh hardware info' in 'Console - Asset Details - Basic' this task
|
||||
will be executed"""
|
||||
)
|
||||
)
|
||||
def gather_assets_facts_task(asset_ids, org_id, task_name=None):
|
||||
from assets.models import GatherFactsAutomation
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
from celery import shared_task
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from assets.utils import check_node_assets_amount
|
||||
from common.const.crontab import CRONTAB_AT_AM_TWO
|
||||
from common.utils import get_logger
|
||||
from common.utils.lock import AcquireFailed
|
||||
from ops.celery.decorator import register_as_period_task
|
||||
from orgs.models import Organization
|
||||
from orgs.utils import tmp_to_org
|
||||
from ops.celery.decorator import register_as_period_task
|
||||
from assets.utils import check_node_assets_amount
|
||||
|
||||
from common.utils.lock import AcquireFailed
|
||||
from common.utils import get_logger
|
||||
from common.const.crontab import CRONTAB_AT_AM_TWO
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
@shared_task(verbose_name=_('Check the amount of assets under the node'))
|
||||
@shared_task(
|
||||
verbose_name=_('Check the amount of assets under the node'),
|
||||
description=_(
|
||||
"""Manually verifying asset quantities updates the asset count for nodes under the
|
||||
current organization. This task will be called in the following two cases: when updating
|
||||
nodes and when the number of nodes exceeds 100"""
|
||||
)
|
||||
)
|
||||
def check_node_assets_amount_task(org_id=None):
|
||||
if org_id is None:
|
||||
orgs = Organization.objects.all()
|
||||
@@ -30,7 +36,13 @@ def check_node_assets_amount_task(org_id=None):
|
||||
logger.error(error)
|
||||
|
||||
|
||||
@shared_task(verbose_name=_('Periodic check the amount of assets under the node'))
|
||||
@shared_task(
|
||||
verbose_name=_('Periodic check the amount of assets under the node'),
|
||||
description=_(
|
||||
"""Schedule the check_node_assets_amount_task to periodically update the asset count of
|
||||
all nodes under all organizations"""
|
||||
)
|
||||
)
|
||||
@register_as_period_task(crontab=CRONTAB_AT_AM_TWO)
|
||||
def check_node_assets_amount_period_task():
|
||||
check_node_assets_amount_task()
|
||||
|
||||
@@ -17,8 +17,12 @@ __all__ = [
|
||||
|
||||
|
||||
@shared_task(
|
||||
verbose_name=_('Test assets connectivity'), queue='ansible',
|
||||
activity_callback=lambda self, asset_ids, org_id, *args, **kwargs: (asset_ids, org_id)
|
||||
verbose_name=_('Test assets connectivity'),
|
||||
queue='ansible',
|
||||
activity_callback=lambda self, asset_ids, org_id, *args, **kwargs: (asset_ids, org_id),
|
||||
description=_(
|
||||
"When clicking 'Test Asset Connectivity' in 'Asset Details - Basic Settings' this task will be executed"
|
||||
)
|
||||
)
|
||||
def test_assets_connectivity_task(asset_ids, org_id, task_name=None):
|
||||
from assets.models import PingAutomation
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user