Merge branch 'dev' of github.com:jumpserver/jumpserver into pr@dev@perf_custom_change_pwd

pull/14359/head
jiangweidong 2024-10-24 17:11:31 +08:00
commit 3d1c5411f9
30 changed files with 3047 additions and 1996 deletions

View File

@ -1,4 +1,4 @@
FROM jumpserver/core-base:20240924_031841 AS stage-build
FROM jumpserver/core-base:20241022_070738 AS stage-build
ARG VERSION
@ -24,30 +24,27 @@ ENV LANG=en_US.UTF-8 \
PATH=/opt/py3/bin:$PATH
ARG DEPENDENCIES=" \
libldap2-dev \
libx11-dev"
ARG TOOLS=" \
ca-certificates \
default-libmysqlclient-dev \
openssh-client \
sshpass \
bubblewrap"
ARG APT_MIRROR=http://deb.debian.org
RUN set -ex \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& 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 > /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
&& apt-get clean all \
&& rm -rf /var/lib/apt/lists/*
COPY --from=stage-build /opt /opt
COPY --from=stage-build /usr/local/bin /usr/local/bin

View File

@ -5,18 +5,10 @@ ARG TARGETARCH
ARG DEPENDENCIES=" \
ca-certificates \
wget \
g++ \
make \
pkg-config \
default-libmysqlclient-dev \
freetds-dev \
gettext \
libkrb5-dev \
libldap2-dev \
libsasl2-dev"
gettext"
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 \
@ -27,9 +19,8 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& echo "no" | dpkg-reconfigure dash
# Install bin tools
ARG CHECK_VERSION=v1.0.3
ARG CHECK_VERSION=v1.0.4
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 \
@ -38,14 +29,13 @@ RUN set -ex \
&& 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 \
RUN --mount=type=cache,target=/root/.cache \
--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 \

View File

@ -15,14 +15,11 @@ ARG TOOLS=" \
vim \
wget"
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} \
&& echo "no" | dpkg-reconfigure dash
&& apt-get clean all \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /opt/jumpserver

View File

@ -13,20 +13,17 @@ from authentication.const import ConfirmType
from authentication.mixins import AuthMixin
from authentication.permissions import UserConfirmation
from common.sdk.im.wecom import URL
from common.sdk.im.wecom import WeCom
from common.sdk.im.wecom import WeCom, wecom_tool
from common.utils import get_logger
from common.utils.common import get_request_ip
from common.utils.django import reverse, get_object_or_none, safe_next_url
from common.utils.random import random_string
from common.views.mixins import UserConfirmRequiredExceptionMixin, PermissionsMixin
from users.models import User
from users.views import UserVerifyPasswordView
from .base import BaseLoginCallbackView, BaseBindCallbackView
from .mixins import METAMixin, FlashMessageMixin
logger = get_logger(__file__)
WECOM_STATE_SESSION_KEY = '_wecom_state'
logger = get_logger(__file__)
class WeComBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMessageMixin, View):
@ -45,7 +42,7 @@ class WeComBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashM
)
def verify_state(self):
return self.verify_state_with_session_key(WECOM_STATE_SESSION_KEY)
return wecom_tool.check_state(self.request.GET.get('state'), self.request)
def get_already_bound_response(self, redirect_url):
msg = _('WeCom is already bound')
@ -56,13 +53,10 @@ class WeComBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashM
class WeComQRMixin(WeComBaseMixin, View):
def get_qr_url(self, redirect_uri):
state = random_string(16)
self.request.session[WECOM_STATE_SESSION_KEY] = state
params = {
'appid': settings.WECOM_CORPID,
'agentid': settings.WECOM_AGENTID,
'state': state,
'state': wecom_tool.gen_state(request=self.request),
'redirect_uri': redirect_uri,
}
url = URL.QR_CONNECT + '?' + urlencode(params)
@ -74,13 +68,11 @@ class WeComOAuthMixin(WeComBaseMixin, View):
def get_oauth_url(self, redirect_uri):
if not settings.AUTH_WECOM:
return reverse('authentication:login')
state = random_string(16)
self.request.session[WECOM_STATE_SESSION_KEY] = state
params = {
'appid': settings.WECOM_CORPID,
'agentid': settings.WECOM_AGENTID,
'state': state,
'state': wecom_tool.gen_state(request=self.request),
'redirect_uri': redirect_uri,
'response_type': 'code',
'scope': 'snsapi_base',

View File

@ -16,12 +16,6 @@ def digest(corp_id, corp_secret):
return dist
def update_values(default: dict, others: dict):
for key in default.keys():
if key in others:
default[key] = others[key]
def set_default(data: dict, default: dict):
for key in default.keys():
if key not in data:

View File

@ -1,12 +1,14 @@
from typing import Iterable, AnyStr
from urllib.parse import urlencode
from django.conf import settings
from django.core.cache import cache
from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import APIException
from common.sdk.im.mixin import RequestMixin, BaseRequest
from common.sdk.im.utils import digest, update_values
from common.utils.common import get_logger
from common.sdk.im.utils import digest
from common.utils import reverse, random_string, get_logger, lazyproperty
from users.utils import construct_user_email, flatten_dict, map_attributes
logger = get_logger(__name__)
@ -107,15 +109,6 @@ class WeCom(RequestMixin):
对于业务代码只需要关心由 用户id 消息不对 导致的错误其他错误不予理会
"""
users = tuple(users)
extra_params = {
"safe": 0,
"enable_id_trans": 0,
"enable_duplicate_check": 0,
"duplicate_check_interval": 1800
}
update_values(extra_params, kwargs)
body = {
"touser": '|'.join(users),
"msgtype": "text",
@ -123,7 +116,7 @@ class WeCom(RequestMixin):
"text": {
"content": msg
},
**extra_params
**kwargs
}
if markdown:
body['msgtype'] = 'markdown'
@ -144,15 +137,15 @@ class WeCom(RequestMixin):
if 'invaliduser' not in data:
return ()
invaliduser = data['invaliduser']
if not invaliduser:
invalid_user = data['invaliduser']
if not invalid_user:
return ()
if isinstance(invaliduser, str):
logger.error(f'WeCom send text 200, but invaliduser is not str: invaliduser={invaliduser}')
if isinstance(invalid_user, str):
logger.error(f'WeCom send text 200, but invaliduser is not str: invaliduser={invalid_user}')
raise WeComError
invalid_users = invaliduser.split('|')
invalid_users = invalid_user.split('|')
return invalid_users
def get_user_id_by_code(self, code):
@ -167,13 +160,12 @@ class WeCom(RequestMixin):
self._requests.check_errcode_is_0(data)
USER_ID = 'UserId'
OPEN_ID = 'OpenId'
if USER_ID in data:
return data[USER_ID], USER_ID
elif OPEN_ID in data:
return data[OPEN_ID], OPEN_ID
user_id = 'UserId'
open_id = 'OpenId'
if user_id in data:
return data[user_id], user_id
elif open_id in data:
return data[open_id], open_id
else:
logger.error(f'WeCom response 200 but get field from json error: fields=UserId|OpenId')
raise WeComError
@ -195,3 +187,37 @@ class WeCom(RequestMixin):
default_detail = self.default_user_detail(data, user_id)
detail = map_attributes(default_detail, info, self.attributes)
return detail
class WeComTool(object):
WECOM_STATE_SESSION_KEY = '_wecom_state'
WECOM_STATE_VALUE = 'wecom'
@lazyproperty
def qr_cb_url(self):
return reverse('authentication:wecom-qr-login-callback', external=True)
def gen_state(self, request=None):
state = random_string(16)
if not request:
cache.set(state, self.WECOM_STATE_VALUE, timeout=60 * 60 * 24)
else:
request.session[self.WECOM_STATE_SESSION_KEY] = state
return state
def check_state(self, state, request=None):
return cache.get(state) == self.WECOM_STATE_VALUE or \
request.session[self.WECOM_STATE_SESSION_KEY] == state
def wrap_redirect_url(self, next_url):
params = {
'appid': settings.WECOM_CORPID,
'agentid': settings.WECOM_AGENTID,
'state': self.gen_state(),
'redirect_uri': f'{self.qr_cb_url}?next={next_url}',
'response_type': 'code', 'scope': 'snsapi_base',
}
return URL.OAUTH_CONNECT + '?' + urlencode(params) + '#wechat_redirect'
wecom_tool = WeComTool()

View File

@ -1,9 +1,9 @@
import os
import jms_storage
from django.conf import settings
from django.core.files.storage import default_storage
from common.storage import jms_storage
from common.utils import get_logger, make_dirs
from terminal.models import ReplayStorage

View File

@ -0,0 +1,41 @@
#!/usr/bin/env python
# coding: utf-8
# Copyright (c) 2018
#
__version__ = '0.0.59'
from .ftp import FTPStorage
from .oss import OSSStorage
from .obs import OBSStorage
from .s3 import S3Storage
from .azure import AzureStorage
from .ceph import CEPHStorage
from .jms import JMSReplayStorage, JMSCommandStorage
from .multi import MultiObjectStorage
from .sftp import SFTPStorage
def get_object_storage(config):
if config.get("TYPE") in ["s3", "ceph", "swift", "cos"]:
return S3Storage(config)
elif config.get("TYPE") == "oss":
return OSSStorage(config)
elif config.get("TYPE") == "server":
return JMSReplayStorage(config)
elif config.get("TYPE") == "azure":
return AzureStorage(config)
elif config.get("TYPE") == "ceph":
return CEPHStorage(config)
elif config.get("TYPE") == "ftp":
return FTPStorage(config)
elif config.get("TYPE") == "obs":
return OBSStorage(config)
elif config.get("TYPE") == "sftp":
return SFTPStorage(config)
else:
return JMSReplayStorage(config)
def get_multi_object_storage(configs):
return MultiObjectStorage(configs)

View File

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
#
import os
from azure.storage.blob import BlobServiceClient
from .base import ObjectStorage
class AzureStorage(ObjectStorage):
def __init__(self, config):
self.account_name = config.get("ACCOUNT_NAME", None)
self.account_key = config.get("ACCOUNT_KEY", None)
self.container_name = config.get("CONTAINER_NAME", None)
self.endpoint_suffix = config.get("ENDPOINT_SUFFIX", 'core.chinacloudapi.cn')
if self.account_name and self.account_key:
self.service_client = BlobServiceClient(
account_url=f'https://{self.account_name}.blob.{self.endpoint_suffix}',
credential={'account_name': self.account_name, 'account_key': self.account_key}
)
self.client = self.service_client.get_container_client(self.container_name)
else:
self.client = None
def upload(self, src, target):
try:
self.client.upload_blob(target, src)
return True, None
except Exception as e:
return False, e
def download(self, src, target):
try:
blob_data = self.client.download_blob(blob=src)
os.makedirs(os.path.dirname(target), 0o755, exist_ok=True)
with open(target, 'wb') as writer:
writer.write(blob_data.readall())
return True, None
except Exception as e:
return False, e
def delete(self, path):
try:
self.client.delete_blob(path)
return True, False
except Exception as e:
return False, e
def exists(self, path):
resp = self.client.list_blobs(name_starts_with=path)
return len(list(resp)) != 0
def list_buckets(self):
return list(self.service_client.list_containers())
@property
def type(self):
return 'azure'

View File

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
#
import abc
class ObjectStorage(metaclass=abc.ABCMeta):
@abc.abstractmethod
def upload(self, src, target):
return None, None
@abc.abstractmethod
def download(self, src, target):
pass
@abc.abstractmethod
def delete(self, path):
pass
@abc.abstractmethod
def exists(self, path):
pass
def is_valid(self, src, target):
ok, msg = self.upload(src=src, target=target)
if not ok:
return False
self.delete(path=target)
return True
class LogStorage(metaclass=abc.ABCMeta):
@abc.abstractmethod
def save(self, command):
pass
@abc.abstractmethod
def bulk_save(self, command_set, raise_on_error=True):
pass
@abc.abstractmethod
def filter(self, date_from=None, date_to=None,
user=None, asset=None, account=None,
input=None, session=None):
pass
@abc.abstractmethod
def count(self, date_from=None, date_to=None,
user=None, asset=None, account=None,
input=None, session=None):
pass

View File

@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
#
import os
import boto
import boto.s3.connection
from .base import ObjectStorage
class CEPHStorage(ObjectStorage):
def __init__(self, config):
self.bucket = config.get("BUCKET", None)
self.region = config.get("REGION", None)
self.access_key = config.get("ACCESS_KEY", None)
self.secret_key = config.get("SECRET_KEY", None)
self.hostname = config.get("HOSTNAME", None)
self.port = config.get("PORT", 7480)
if self.hostname and self.access_key and self.secret_key:
self.conn = boto.connect_s3(
aws_access_key_id=self.access_key,
aws_secret_access_key=self.secret_key,
host=self.hostname,
port=self.port,
is_secure=False,
calling_format=boto.s3.connection.OrdinaryCallingFormat(),
)
try:
self.client = self.conn.get_bucket(bucket_name=self.bucket)
except Exception:
self.client = None
def upload(self, src, target):
try:
key = self.client.new_key(target)
key.set_contents_from_filename(src)
return True, None
except Exception as e:
return False, e
def download(self, src, target):
try:
os.makedirs(os.path.dirname(target), 0o755, exist_ok=True)
key = self.client.get_key(src)
key.get_contents_to_filename(target)
return True, None
except Exception as e:
return False, e
def delete(self, path):
try:
self.client.delete_key(path)
return True, None
except Exception as e:
return False, e
def exists(self, path):
try:
return self.client.get_key(path)
except Exception:
return False
@property
def type(self):
return 'ceph'

View File

@ -0,0 +1,116 @@
# -*- coding: utf-8 -*-
#
import os
from ftplib import FTP, error_perm
from .base import ObjectStorage
class FTPStorage(ObjectStorage):
def __init__(self, config):
self.host = config.get("HOST", None)
self.port = int(config.get("PORT", 21))
self.username = config.get("USERNAME", None)
self.password = config.get("PASSWORD", None)
self.pasv = bool(config.get("PASV", False))
self.dir = config.get("DIR", "replay")
self.client = FTP()
self.client.encoding = 'utf-8'
self.client.set_pasv(self.pasv)
self.pwd = '.'
self.connect()
def connect(self, timeout=-999, source_address=None):
self.client.connect(self.host, self.port, timeout, source_address)
self.client.login(self.username, self.password)
if not self.check_dir_exist(self.dir):
self.mkdir(self.dir)
self.client.cwd(self.dir)
self.pwd = self.client.pwd()
def confirm_connected(self):
try:
self.client.pwd()
except Exception:
self.connect()
def upload(self, src, target):
self.confirm_connected()
target_dir = os.path.dirname(target)
exist = self.check_dir_exist(target_dir)
if not exist:
ok = self.mkdir(target_dir)
if not ok:
raise PermissionError('Dir create error: %s' % target)
try:
with open(src, 'rb') as f:
self.client.storbinary('STOR '+target, f)
return True, None
except Exception as e:
return False, e
def download(self, src, target):
self.confirm_connected()
try:
os.makedirs(os.path.dirname(target), 0o755, exist_ok=True)
with open(target, 'wb') as f:
self.client.retrbinary('RETR ' + src, f.write)
return True, None
except Exception as e:
return False, e
def delete(self, path):
self.confirm_connected()
if not self.exists(path):
raise FileNotFoundError('File not exist error(%s)' % path)
try:
self.client.delete(path)
return True, None
except Exception as e:
return False, e
def check_dir_exist(self, d):
pwd = self.client.pwd()
try:
self.client.cwd(d)
self.client.cwd(pwd)
return True
except error_perm:
return False
def mkdir(self, dirs):
self.confirm_connected()
# 创建多级目录ftplib不支持一次创建多级目录
dir_list = dirs.split('/')
pwd = self.client.pwd()
try:
for d in dir_list:
if not d or d in ['.']:
continue
# 尝试切换目录
try:
self.client.cwd(d)
continue
except:
pass
# 切换失败创建这个目录,再切换
try:
self.client.mkd(d)
self.client.cwd(d)
except:
return False
return True
finally:
self.client.cwd(pwd)
def exists(self, target):
self.confirm_connected()
try:
self.client.size(target)
return True
except:
return False
def close(self):
self.client.close()

View File

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
#
import os
from .base import ObjectStorage, LogStorage
class JMSReplayStorage(ObjectStorage):
def __init__(self, config):
self.client = config.get("SERVICE")
def upload(self, src, target):
session_id = os.path.basename(target).split('.')[0]
ok = self.client.push_session_replay(src, session_id)
return ok, None
def delete(self, path):
return False, Exception("Not support not")
def exists(self, path):
return False
def download(self, src, target):
return False, Exception("Not support not")
@property
def type(self):
return 'jms'
class JMSCommandStorage(LogStorage):
def __init__(self, config):
self.client = config.get("SERVICE")
if not self.client:
raise Exception("Not found app service")
def save(self, command):
return self.client.push_session_command([command])
def bulk_save(self, command_set, raise_on_error=True):
return self.client.push_session_command(command_set)
def filter(self, date_from=None, date_to=None,
user=None, asset=None, account=None,
input=None, session=None):
pass
def count(self, date_from=None, date_to=None,
user=None, asset=None, account=None,
input=None, session=None):
pass

View File

@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
#
from .base import ObjectStorage, LogStorage
class MultiObjectStorage(ObjectStorage):
def __init__(self, configs):
self.configs = configs
self.storage_list = []
self.init_storage_list()
def init_storage_list(self):
from . import get_object_storage
if isinstance(self.configs, dict):
configs = self.configs.values()
else:
configs = self.configs
for config in configs:
try:
storage = get_object_storage(config)
self.storage_list.append(storage)
except Exception:
pass
def upload(self, src, target):
success = []
msg = []
for storage in self.storage_list:
ok, err = storage.upload(src, target)
success.append(ok)
msg.append(err)
return success, msg
def download(self, src, target):
success = False
msg = None
for storage in self.storage_list:
try:
if not storage.exists(src):
continue
ok, msg = storage.download(src, target)
if ok:
success = True
msg = ''
break
except:
pass
return success, msg
def delete(self, path):
success = True
msg = None
for storage in self.storage_list:
try:
if storage.exists(path):
ok, msg = storage.delete(path)
if not ok:
success = False
except:
pass
return success, msg
def exists(self, path):
for storage in self.storage_list:
try:
if storage.exists(path):
return True
except:
pass
return False

View File

@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
#
import os
from obs.client import ObsClient
from .base import ObjectStorage
class OBSStorage(ObjectStorage):
def __init__(self, config):
self.endpoint = config.get("ENDPOINT", None)
self.bucket = config.get("BUCKET", None)
self.access_key = config.get("ACCESS_KEY", None)
self.secret_key = config.get("SECRET_KEY", None)
if self.access_key and self.secret_key and self.endpoint:
proxy_host = os.getenv("proxy_host")
proxy_port = os.getenv("proxy_port")
proxy_username = os.getenv("proxy_username")
proxy_password = os.getenv("proxy_password")
self.obsClient = ObsClient(access_key_id=self.access_key, secret_access_key=self.secret_key, server=self.endpoint, proxy_host=proxy_host, proxy_port=proxy_port, proxy_username=proxy_username, proxy_password=proxy_password)
else:
self.obsClient = None
def upload(self, src, target):
try:
resp = self.obsClient.putFile(self.bucket, target, src)
if resp.status < 300:
return True, None
else:
return False, resp.reason
except Exception as e:
return False, e
def exists(self, path):
resp = self.obsClient.getObjectMetadata(self.bucket, path)
if resp.status < 300:
return True
return False
def delete(self, path):
try:
resp = self.obsClient.deleteObject(self.bucket, path)
if resp.status < 300:
return True, None
else:
return False, resp.reason
except Exception as e:
return False, e
def download(self, src, target):
try:
os.makedirs(os.path.dirname(target), 0o755, exist_ok=True)
resp = self.obsClient.getObject(self.bucket, src, target)
if resp.status < 300:
return True, None
else:
return False, resp.reason
except Exception as e:
return False, e
def list_buckets(self):
resp = self.obsClient.listBuckets()
if resp.status < 300:
return [b.name for b in resp.body.buckets]
else:
raise RuntimeError(resp.status, str(resp.reason))
@property
def type(self):
return 'obs'

View File

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
#
import os
import time
import oss2
from .base import ObjectStorage
class OSSStorage(ObjectStorage):
def __init__(self, config):
self.endpoint = config.get("ENDPOINT", None)
self.bucket = config.get("BUCKET", None)
self.access_key = config.get("ACCESS_KEY", None)
self.secret_key = config.get("SECRET_KEY", None)
if self.access_key and self.secret_key:
self.auth = oss2.Auth(self.access_key, self.secret_key)
else:
self.auth = None
if self.auth and self.endpoint and self.bucket:
self.client = oss2.Bucket(self.auth, self.endpoint, self.bucket)
else:
self.client = None
def upload(self, src, target):
try:
self.client.put_object_from_file(target, src)
return True, None
except Exception as e:
return False, e
def exists(self, path):
try:
return self.client.object_exists(path)
except Exception as e:
return False
def delete(self, path):
try:
self.client.delete_object(path)
return True, None
except Exception as e:
return False, e
def restore(self, path):
meta = self.client.head_object(path)
if meta.resp.headers['x-oss-storage-class'] == oss2.BUCKET_STORAGE_CLASS_ARCHIVE:
self.client.restore_object(path)
while True:
meta = self.client.head_object(path)
if meta.resp.headers['x-oss-restore'] == 'ongoing-request="true"':
time.sleep(5)
else:
break
def download(self, src, target):
try:
os.makedirs(os.path.dirname(target), 0o755, exist_ok=True)
self.restore(src)
self.client.get_object_to_file(src, target)
return True, None
except Exception as e:
return False, e
def list_buckets(self):
service = oss2.Service(self.auth,self.endpoint)
return ([b.name for b in oss2.BucketIterator(service)])
@property
def type(self):
return 'oss'

View File

@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
#
import boto3
import os
from .base import ObjectStorage
class S3Storage(ObjectStorage):
def __init__(self, config):
self.bucket = config.get("BUCKET", "jumpserver")
self.region = config.get("REGION", None)
self.access_key = config.get("ACCESS_KEY", None)
self.secret_key = config.get("SECRET_KEY", None)
self.endpoint = config.get("ENDPOINT", None)
try:
self.client = boto3.client(
's3', region_name=self.region,
aws_access_key_id=self.access_key,
aws_secret_access_key=self.secret_key,
endpoint_url=self.endpoint
)
except ValueError:
pass
def upload(self, src, target):
try:
self.client.upload_file(Filename=src, Bucket=self.bucket, Key=target)
return True, None
except Exception as e:
return False, e
def exists(self, path):
try:
self.client.head_object(Bucket=self.bucket, Key=path)
return True
except Exception as e:
return False
def download(self, src, target):
try:
os.makedirs(os.path.dirname(target), 0o755, exist_ok=True)
self.client.download_file(self.bucket, src, target)
return True, None
except Exception as e:
return False, e
def delete(self, path):
try:
self.client.delete_object(Bucket=self.bucket, Key=path)
return True, None
except Exception as e:
return False, e
def generate_presigned_url(self, path, expire=3600):
try:
return self.client.generate_presigned_url(
ClientMethod='get_object',
Params={'Bucket': self.bucket, 'Key': path},
ExpiresIn=expire,
HttpMethod='GET'), None
except Exception as e:
return False, e
def list_buckets(self):
response = self.client.list_buckets()
buckets = response.get('Buckets', [])
result = [b['Name'] for b in buckets if b.get('Name')]
return result
@property
def type(self):
return 's3'

View File

@ -0,0 +1,107 @@
# -*- coding: utf-8 -*-
import os
import io
import paramiko
from jms_storage.base import ObjectStorage
class SFTPStorage(ObjectStorage):
def __init__(self, config):
self.sftp = None
self.sftp_host = config.get('SFTP_HOST', None)
self.sftp_port = int(config.get('SFTP_PORT', 22))
self.sftp_username = config.get('SFTP_USERNAME', '')
self.sftp_secret_type = config.get('STP_SECRET_TYPE', 'password')
self.sftp_password = config.get('SFTP_PASSWORD', '')
self.sftp_private_key = config.get('STP_PRIVATE_KEY', '')
self.sftp_passphrase = config.get('STP_PASSPHRASE', '')
self.sftp_root_path = config.get('SFTP_ROOT_PATH', '/tmp')
self.ssh = paramiko.SSHClient()
self.connect()
def connect(self):
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
if self.sftp_secret_type == 'password':
self.ssh.connect(self.sftp_host, self.sftp_port, self.sftp_username, self.sftp_password)
elif self.sftp_secret_type == 'ssh_key':
pkey = paramiko.RSAKey.from_private_key(io.StringIO(self.sftp_private_key))
self.ssh.connect(self.sftp_host, self.sftp_port, self.sftp_username, pkey=pkey,
passphrase=self.sftp_passphrase)
self.sftp = self.ssh.open_sftp()
def confirm_connected(self):
try:
self.sftp.getcwd()
except Exception as e:
self.connect()
def upload(self, src, target):
local_file = src
remote_file = os.path.join(self.sftp_root_path, target)
try:
self.confirm_connected()
mode = os.stat(local_file).st_mode
remote_dir = os.path.dirname(remote_file)
if not self.exists(remote_dir):
self.sftp.mkdir(remote_dir)
self.sftp.put(local_file, remote_file)
self.sftp.chmod(remote_file, mode)
return True, None
except Exception as e:
return False, e
def download(self, src, target):
remote_file = src
local_file = target
self.confirm_connected()
try:
local_dir = os.path.dirname(local_file)
if not os.path.exists(local_dir):
os.makedirs(local_dir)
mode = self.sftp.stat(remote_file).st_mode
self.sftp.get(remote_file, local_file)
os.chmod(local_file, mode)
return True, None
except Exception as e:
return False, e
def delete(self, path):
path = os.path.join(self.sftp_root_path, path)
self.confirm_connected()
if not self.exists(path):
raise FileNotFoundError('File not exist error(%s)' % path)
try:
self.sftp.remove(path)
return True, None
except Exception as e:
return False, e
def check_dir_exist(self, d):
self.confirm_connected()
try:
self.sftp.stat(d)
return True
except Exception:
return False
def mkdir(self, dirs):
self.confirm_connected()
try:
if not self.exists(dirs):
self.sftp.mkdir(dirs)
return True, None
except Exception as e:
return False, e
def exists(self, target):
self.confirm_connected()
try:
self.sftp.stat(target)
return True
except:
return False
def close(self):
self.sftp.close()
self.ssh.close()

View File

@ -1,11 +1,11 @@
import os
import jms_storage
from celery import shared_task
from django.conf import settings
from django.core.mail import send_mail, EmailMultiAlternatives, get_connection
from django.utils.translation import gettext_lazy as _
from common.storage import jms_storage
from .utils import get_logger
logger = get_logger(__file__)

View File

@ -127,13 +127,16 @@ class Message(metaclass=MessageType):
def get_html_msg(self) -> dict:
return self.get_common_msg()
def get_markdown_msg(self) -> dict:
@staticmethod
def html_to_markdown(html_msg):
h = HTML2Text()
h.body_width = 300
msg = self.get_html_msg()
content = msg['message']
msg['message'] = h.handle(content)
return msg
h.body_width = 0
content = html_msg['message']
html_msg['message'] = h.handle(content)
return html_msg
def get_markdown_msg(self) -> dict:
return self.html_to_markdown(self.get_html_msg())
def get_text_msg(self) -> dict:
h = HTML2Text()

View File

@ -9,4 +9,11 @@
ansible.windows.win_powershell:
script: |
tinkerd install --name {{ applet_name }}
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
Write-Host "Failed to install {{ applet_name }}"
Write-Host "Exit code: $exitCode"
$Ansible.Failed = $true
exit 1
}
when: applet_name != 'all'

View File

@ -18,7 +18,7 @@
PYTHON_VERSION: 3.11.10
CHROME_VERSION: 118.0.5993.118
CHROME_DRIVER_VERSION: 118.0.5993.70
TINKER_VERSION: v0.1.9
TINKER_VERSION: v0.2.0
tasks:
- block:
@ -268,6 +268,13 @@
ansible.windows.win_powershell:
script: |
tinkerd install all
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
Write-Host "Failed to install applets"
Write-Host "Exit code: $exitCode"
$Ansible.Failed = $true
exit 1
}
register: sync_remote_applets
when: INSTALL_APPLETS

View File

@ -9,3 +9,10 @@
ansible.windows.win_powershell:
script: |
tinkerd uninstall --name {{ applet_name }}
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
Write-Host "Failed to uninstall {{ applet_name }}"
Write-Host "Exit code: $exitCode"
$Ansible.Failed = $true
exit 1
}

View File

@ -4,7 +4,6 @@ import copy
import os
from importlib import import_module
import jms_storage
from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
@ -12,6 +11,7 @@ from django.utils.translation import gettext_lazy as _
from common.db.fields import EncryptJsonDictTextField
from common.db.models import JMSBaseModel
from common.plugins.es import QuerySet as ESQuerySet
from common.storage import jms_storage
from common.utils import get_logger
from common.utils.timezone import local_now_date_display
from terminal import const

View File

@ -4,6 +4,7 @@ from django.conf import settings
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _
from common.sdk.im.wecom import wecom_tool
from common.utils import get_logger, reverse
from common.utils import lazyproperty
from common.utils.timezone import local_now_display
@ -75,53 +76,50 @@ class CommandWarningMessage(CommandAlertMixin, UserMessage):
super().__init__(user)
self.command = command
def get_html_msg(self) -> dict:
command = self.command
command_input = command['input']
user = command['user']
asset = command['asset']
account = command.get('_account', '')
cmd_acl = command.get('_cmd_filter_acl')
cmd_group = command.get('_cmd_group')
session_id = command.get('session', '')
risk_level = command['risk_level']
org_id = command['org_id']
org_name = command.get('_org_name') or org_id
def get_session_url(self, external=True):
session_id = self.command.get('session', '')
org_id = self.command['org_id']
session_url = ''
if session_id:
session_url = reverse(
'api-terminal:session-detail', kwargs={'pk': session_id},
external=True, api_to_ui=True
external=external, api_to_ui=True
) + '?oid={}'.format(org_id)
session_url = session_url.replace('/terminal/sessions/', '/audit/sessions/sessions/')
else:
session_url = ''
return session_url
# Command ACL
cmd_acl_name = cmd_group_name = ''
if cmd_acl:
cmd_acl_name = cmd_acl.name
if cmd_group:
cmd_group_name = cmd_group.name
def gen_html_string(self, **other_context):
command = self.command
cmd_acl = command.get('_cmd_filter_acl')
cmd_group = command.get('_cmd_group')
org_id = command['org_id']
org_name = command.get('_org_name') or org_id
cmd_acl_name = cmd_acl.name if cmd_acl else ''
cmd_group_name = cmd_group.name if cmd_group else ''
context = {
'command': command_input,
'user': user,
'asset': asset,
'account': account,
'command': command['input'],
'user': command['user'],
'asset': command['asset'],
'account': command.get('_account', ''),
'cmd_filter_acl': cmd_acl_name,
'cmd_group': cmd_group_name,
'session_url': session_url,
'risk_level': RiskLevelChoices.get_label(risk_level),
'risk_level': RiskLevelChoices.get_label(command['risk_level']),
'org': org_name,
}
context.update(other_context)
message = render_to_string('terminal/_msg_command_warning.html', context)
return {
'subject': self.subject,
'message': message
}
return {'subject': self.subject, 'message': message}
def get_wecom_msg(self):
session_url = wecom_tool.wrap_redirect_url(
self.get_session_url(external=False)
)
message = self.gen_html_string(session_url=session_url)
return self.html_to_markdown(message)
def get_html_msg(self) -> dict:
return self.gen_html_string(session_url=self.get_session_url())
class CommandAlertMessage(CommandAlertMixin, SystemMessage):
@ -141,15 +139,18 @@ class CommandAlertMessage(CommandAlertMixin, SystemMessage):
command['session'] = Session.objects.first().id
return cls(command)
def get_html_msg(self) -> dict:
command = self.command
def get_session_url(self, external=True):
session_detail_url = reverse(
'api-terminal:session-detail', kwargs={'pk': command['session']},
external=True, api_to_ui=True
'api-terminal:session-detail', api_to_ui=True,
kwargs={'pk': self.command['session']}, external=external,
) + '?oid={}'.format(self.command['org_id'])
session_detail_url = session_detail_url.replace(
'/terminal/sessions/', '/audit/sessions/sessions/'
)
return session_detail_url
def gen_html_string(self, **other_context) -> dict:
command = self.command
level = RiskLevelChoices.get_label(command['risk_level'])
items = {
_("Asset"): command['asset'],
@ -159,14 +160,21 @@ class CommandAlertMessage(CommandAlertMixin, SystemMessage):
}
context = {
'items': items,
'session_url': session_detail_url,
"command": command['input'],
}
context.update(other_context)
message = render_to_string('terminal/_msg_command_alert.html', context)
return {
'subject': self.subject,
'message': message
}
return {'subject': self.subject, 'message': message}
def get_wecom_msg(self):
session_url = wecom_tool.wrap_redirect_url(
self.get_session_url(external=False)
)
message = self.gen_html_string(session_url=session_url)
return self.html_to_markdown(message)
def get_html_msg(self) -> dict:
return self.gen_html_string(session_url=self.get_session_url())
class CommandExecutionAlert(CommandAlertMixin, SystemMessage):
@ -189,16 +197,20 @@ class CommandExecutionAlert(CommandAlertMixin, SystemMessage):
}
return cls(cmd)
def get_html_msg(self) -> dict:
command = self.command
def get_asset_urls(self, external=True, tran_func=None):
assets_with_url = []
for asset in command['assets']:
for asset in self.command['assets']:
url = reverse(
'assets:asset-detail', kwargs={'pk': asset.id},
api_to_ui=True, external=True, is_console=True
api_to_ui=True, external=external, is_console=True
) + '?oid={}'.format(asset.org_id)
if tran_func:
url = tran_func(url)
assets_with_url.append([asset, url])
return assets_with_url
def gen_html_string(self, **other_context):
command = self.command
level = RiskLevelChoices.get_label(command['risk_level'])
items = {
@ -206,17 +218,23 @@ class CommandExecutionAlert(CommandAlertMixin, SystemMessage):
_("Level"): level,
_("Date"): local_now_display(),
}
context = {
'items': items,
'assets_with_url': assets_with_url,
'command': command['input'],
}
context.update(other_context)
message = render_to_string('terminal/_msg_command_execute_alert.html', context)
return {
'subject': self.subject,
'message': message
}
return {'subject': self.subject, 'message': message}
def get_wecom_msg(self):
assets_with_url = self.get_asset_urls(
external=False, tran_func=wecom_tool.wrap_redirect_url
)
message = self.gen_html_string(assets_with_url=assets_with_url)
return self.html_to_markdown(message)
def get_html_msg(self) -> dict:
return self.gen_html_string(assets_with_url=self.get_asset_urls())
class StorageConnectivityMessage(SystemMessage):

View File

@ -4,12 +4,12 @@ from urllib.parse import urljoin
from django.conf import settings
from django.core.cache import cache
from django.forms import model_to_dict
from django.shortcuts import reverse
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _
from common.db.encoder import ModelJSONFieldEncoder
from common.utils import get_logger, random_string
from common.sdk.im.wecom import wecom_tool
from common.utils import get_logger, random_string, reverse
from notifications.notifications import UserMessage
from . import const
from .models import Ticket
@ -22,16 +22,13 @@ class BaseTicketMessage(UserMessage):
ticket: Ticket
content_title: str
@property
def ticket_detail_url(self):
tp = self.ticket.type
return urljoin(
settings.SITE_URL,
const.TICKET_DETAIL_URL.format(
id=str(self.ticket.id),
type=tp
)
def get_ticket_detail_url(self, external=True):
detail_url = const.TICKET_DETAIL_URL.format(
id=str(self.ticket.id), type=self.ticket.type
)
if not external:
return detail_url
return urljoin(settings.SITE_URL, detail_url)
@property
def content_title(self):
@ -41,17 +38,31 @@ class BaseTicketMessage(UserMessage):
def subject(self):
raise NotImplementedError
def get_html_msg(self) -> dict:
context = dict(
title=self.content_title,
content=self.content,
ticket_detail_url=self.ticket_detail_url
)
message = render_to_string('tickets/_msg_ticket.html', context)
return {
'subject': self.subject,
'message': message
def get_html_context(self):
return {'ticket_detail_url': self.get_ticket_detail_url()}
def get_wecom_context(self):
ticket_detail_url = wecom_tool.wrap_redirect_url(
[self.get_ticket_detail_url(external=False)]
)[0]
return {'ticket_detail_url': ticket_detail_url}
def gen_html_string(self, **other_context):
context = {
'title': self.content_title, 'content': self.content,
}
context.update(other_context)
message = render_to_string(
'tickets/_msg_ticket.html', context
)
return {'subject': self.subject, 'message': message}
def get_html_msg(self) -> dict:
return self.gen_html_string(**self.get_html_context())
def get_wecom_msg(self):
message = self.gen_html_string(**self.get_wecom_context())
return self.html_to_markdown(message)
@classmethod
def gen_test_msg(cls):
@ -113,27 +124,21 @@ class TicketAppliedToAssigneeMessage(BaseTicketMessage):
)
return title
def get_ticket_approval_url(self):
def get_ticket_approval_url(self, external=True):
url = reverse('tickets:direct-approve', kwargs={'token': self.token})
if not external:
return url
return urljoin(settings.SITE_URL, url)
def get_html_msg(self) -> dict:
context = dict(
title=self.content_title,
content=self.content,
ticket_detail_url=self.ticket_detail_url
)
ticket_approval_url = self.get_ticket_approval_url()
context.update({'ticket_approval_url': ticket_approval_url})
message = render_to_string('tickets/_msg_ticket.html', context)
cache.set(self.token, {
'ticket_id': self.ticket.id, 'approver_id': self.user.id,
'content': self.content,
}, 3600)
return {
'subject': self.subject, 'message': message
def get_html_context(self):
context = super().get_html_context()
context['ticket_approval_url'] = self.get_ticket_approval_url()
data = {
'ticket_id': self.ticket.id,
'approver_id': self.user.id, 'content': self.content,
}
cache.set(self.token, data, 3600)
return context
@classmethod
def gen_test_msg(cls):

3786
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -16,60 +16,32 @@ documentation = "https://docs.jumpserver.org"
[tool.poetry.dependencies]
python = "^3.11"
# cython = "3.0.0"
aiofiles = "23.1.0"
amqp = "5.1.1"
ansible-core = { url = "https://github.com/jumpserver-dev/ansible/archive/refs/tags/v2.14.1.7.zip" }
ansible = "7.1.0"
ansible-runner = { url = "https://github.com/jumpserver-dev/ansible-runner/archive/refs/tags/2.4.0.1.zip" }
asn1crypto = "1.5.1"
bcrypt = "4.0.1"
billiard = "4.1.0"
# certifi = "2023.7.22"
# cffi = "1.15.1"
chardet = "5.1.0"
configparser = "6.0.0"
decorator = "5.1.1"
docutils = "0.20.1"
ecdsa = "0.18.0"
enum-compat = "0.0.3"
ephem = "4.1.4"
future = "0.18.3"
# idna = "3.4"
itypes = "1.2.0"
jinja2 = "3.1.2"
markupsafe = "2.1.3"
olefile = "0.46"
paramiko = "3.2.0"
passlib = "1.7.4"
pyasn1 = "0.5.0"
pycparser = "2.21"
# cryptography = "41.0.2"
pycryptodome = "3.18.0"
pycryptodomex = "3.18.0"
phonenumbers = "8.13.17"
gmssl = "3.2.2"
itsdangerous = "1.1.0"
pyotp = "2.8.0"
pynacl = "1.5.0"
python-dateutil = "2.8.2"
pyyaml = "6.0.1"
requests = "2.31.0"
jms-storage = "^0.0.59"
simplejson = "3.19.1"
six = "1.16.0"
sshtunnel = "0.4.0"
sshpubkeys = "3.3.1"
uritemplate = "4.1.1"
# urllib3 = "1.26.16"
vine = "5.0.0"
werkzeug = "2.3.6"
unicodecsv = "0.14.1"
httpsig = "1.3.0"
treelib = "1.6.4"
psutil = "5.9.5"
psutil = "6.0.0"
msrestazure = "0.6.4"
adal = "1.2.7"
# Requires fixed version number, ImportError: cannot import name 'save_virtual_workbook' from 'openpyxl.writer.excel'
openpyxl = "3.0.10"
pyexcel = "0.7.0"
pyexcel-xlsx = "0.6.0"
@ -90,10 +62,7 @@ django = "4.1.13"
django-bootstrap3 = "23.4"
django-filter = "23.2"
django-formtools = "2.4.1"
django-ranged-response = "0.2.0"
django-simple-captcha = "0.5.18"
django-timezone-field = "5.1"
djangorestframework = "3.14.0"
djangorestframework-bulk = "0.2.1"
django-simple-history = "3.3.0"
django-private-storage = "3.1"
@ -101,33 +70,29 @@ drf-nested-routers = "0.93.4"
drf-writable-nested = "0.7.0"
rest-condition = "1.0.3"
drf-yasg = "1.21.7"
coreapi = "2.3.3"
coreschema = "0.0.4"
openapi-codec = "1.3.2"
pillow = "10.0.1"
pytz = "2023.3"
django-proxy = "1.2.2"
python-daemon = "3.0.1"
eventlet = "0.33.3"
greenlet = "3.0.1"
gunicorn = "21.2.0"
celery = "5.3.1"
flower = "2.0.1"
django-celery-beat = "2.6.0"
kombu = "5.3.1"
uvicorn = "0.22.0"
websockets = "11.0.3"
python-ldap = "3.4.3"
python-ldap = [
{ url = "https://github.com/jumpserver-dev/core-package/releases/download/v1.0.0/python_ldap-3.4.3-cp311-cp311-manylinux_2_28_x86_64.whl", markers = "sys_platform == 'linux' and platform_machine == 'x86_64'" },
{ url = "https://github.com/jumpserver-dev/core-package/releases/download/v1.0.0/python_ldap-3.4.3-cp311-cp311-manylinux_2_28_aarch64.whl", markers = "sys_platform == 'linux' and platform_machine == 'aarch64'" },
{ version = "3.4.3", source = "aliyun", markers = "sys_platform != 'linux'" }
]
ldap3 = "2.9.1"
django-radius = { url = "https://github.com/ibuler/django-radius/archive/refs/tags/1.5.0.zip" }
django-cas-ng = { url = "https://github.com/ibuler/django-cas-ng/releases/download/v4.3.2/django-cas-ng-4.3.2.zip" }
python-cas = "1.6.0"
django-auth-ldap = "4.4.0"
boto3 = "1.28.9"
botocore = "1.31.9"
s3transfer = "0.6.1"
mysqlclient = "2.2.4"
pymssql = "2.2.8"
mysqlclient = [
{ url = "https://github.com/jumpserver-dev/core-package/releases/download/v1.0.0/mysqlclient-2.2.4-cp311-cp311-manylinux_2_28_x86_64.whl", markers = "sys_platform == 'linux' and platform_machine == 'x86_64'" },
{ url = "https://github.com/jumpserver-dev/core-package/releases/download/v1.0.0/mysqlclient-2.2.4-cp311-cp311-manylinux_2_28_aarch64.whl", markers = "sys_platform == 'linux' and platform_machine == 'aarch64'" },
{ version = "2.2.4", source = "aliyun", markers = "sys_platform != 'linux'" }
]
pymssql = "2.2.11"
django-redis = "5.3.0"
python-redis-lock = "4.0.0"
pyopenssl = "23.2.0"
@ -138,33 +103,29 @@ forgerypy3 = "0.3.1"
django-debug-toolbar = "4.1.0"
pympler = "1.0.1"
hvac = "1.1.1"
pyhcl = "0.4.4"
ipy = "1.1"
netifaces = "^0.11.0"
netifaces = [
{ url = "https://github.com/jumpserver-dev/core-package/releases/download/v1.0.0/netifaces-0.11.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", markers = "sys_platform == 'linux' and platform_machine == 'x86_64'" },
{ url = "https://github.com/jumpserver-dev/core-package/releases/download/v1.0.0/netifaces-0.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", markers = "sys_platform == 'linux' and platform_machine == 'aarch64'" },
{ version = "^0.11.0", source = "aliyun", markers = "sys_platform != 'linux'" }
]
daphne = "4.0.0"
channels = "^4.0.0"
channels-redis = "4.1.0"
fido2 = "^1.1.2"
ua-parser = "^0.18.0"
user-agents = "^2.2.0"
django-cors-headers = "^4.3.0"
mistune = "2.0.3"
openai = "^1.29.0"
xlsxwriter = "^3.1.9"
exchangelib = "^5.1.0"
xmlsec = "^1.3.13"
lxml = "5.2.1"
pydantic = "^2.7.4"
annotated-types = "^0.6.0"
httpx = "^0.27.0"
distro = "1.9.0"
tqdm = "4.66.4"
elasticsearch7 = "7.17.9"
elasticsearch8 = "8.13.2"
polib = "^1.2.0"
# psycopg2 = "2.9.6"
psycopg2-binary = "2.9.6"
pycountry = "^24.6.1"
boto = "2.49.0"
azure-storage-blob = "12.17.0"
esdk-obs-python = "3.21.4"
[tool.poetry.group.xpack]
optional = true
@ -183,11 +144,9 @@ bce-python-sdk = "0.8.87"
tencentcloud-sdk-python = "3.0.941"
aliyun-python-sdk-core-v3 = "2.13.33"
aliyun-python-sdk-ecs = "4.24.64"
keystoneauth1 = "5.2.1"
oracledb = "1.4.0"
ucloud-sdk-python3 = "0.11.50"
huaweicloudsdkecs = "3.1.52"
huaweicloudsdkcore = "3.1.52"
volcengine-python-sdk = "1.0.71"
[[tool.poetry.source]]

View File

@ -18,7 +18,7 @@ for i in $need_clean; do
done
# 清理缓存文件
cd lib_path
cd ${lib_path} || exit 1
find . -name "*.pyc" -exec rm -f {} \;
# 清理不需要的国际化文件

View File

@ -1,6 +1,6 @@
#!/bin/bash
command -v poetry >/dev/null 2>&1 || {
command -v poetry >/dev/null 2>&1 || {
echo "poetry not found. Aborting."
exit 1
}