Merge pull request #1 from openspug/3.0

sync from upstream
pull/509/head
sf 2022-06-23 14:03:30 +08:00 committed by GitHub
commit 0061fc3c62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 118 additions and 30 deletions

View File

@ -10,11 +10,16 @@ Spug是面向中小型企业设计的轻量级无Agent的自动化运维平台
- 使用文档https://spug.cc/docs/about-spug/
- 更新日志https://spug.cc/docs/change-log/
- 常见问题https://spug.cc/docs/faq/
- 推送助手https://push.spug.cc
## 演示环境
演示地址https://demo.spug.cc
## 🔥推送助手
推送助手是一个集成了电话、短信、邮件、飞书、钉钉、微信、企业微信等多通道的消息推送平台用户只需要调用一个简单的URL就可以完成多通道的消息推送点击体验[https://push.spug.cc](https://push.spug.cc)
## 特性
@ -39,9 +44,10 @@ Spug是面向中小型企业设计的轻量级无Agent的自动化运维平台
## 安装
[官方文档](https://spug.cc/docs/install/)
[官方文档](https://spug.cc/docs/install-docker)
更多使用帮助请参考: [使用文档](https://spug.cc/docs/host-manage/)
更多使用帮助请参考 [使用文档](https://spug.cc/docs/host-manage/)。
## 推荐项目
[Yearning — MYSQL 开源SQL语句审核平台](https://github.com/cookieY/Yearning)

View File

@ -52,10 +52,9 @@ class Command(BaseCommand):
self.echo_error('缺少参数')
self.print_help()
user = User.objects.filter(username=options['u'], deleted_by_id__isnull=True).first()
if not user:
return self.echo_error(f'未找到登录名为【{options["u"]}】的账户')
user.is_active = True
user.save()
if user:
user.is_active = True
user.save()
cache.delete(user.username)
self.echo_success('账户已启用')
elif action == 'reset':

View File

@ -41,13 +41,12 @@ class UserView(AdminView):
return json_response(error=f'已存在登录名为【{form.username}】的用户')
role_ids, password = form.pop('role_ids'), form.pop('password')
if not verify_password(password):
return json_response(error='请设置至少8位包含数字、小写和大写字母的新密码')
if form.id:
user = User.objects.get(pk=form.id)
user.update_by_dict(form)
else:
if not verify_password(password):
return json_response(error='请设置至少8位包含数字、小写和大写字母的新密码')
user = User.objects.create(
password_hash=User.make_password(password),
created_by=request.user,

View File

@ -6,6 +6,7 @@ from apps.setting.utils import AppSetting
from apps.deploy.models import Deploy, DeployRequest
from apps.repository.models import Repository
from apps.deploy.utils import dispatch as deploy_dispatch
from libs.utils import human_datetime
from threading import Thread
import hashlib
import hmac
@ -119,4 +120,7 @@ def _dispatch(deploy_id, ref, commit_id=None, message=None):
req.save()
if req.status == '2':
req.do_at = human_datetime()
req.do_by = deploy.created_by
req.save()
deploy_dispatch(req)

View File

@ -130,6 +130,10 @@ class RequestDetailView(View):
outputs[item['key']]['status'] = item['status']
data = rds.lrange(key, counter, counter + 9)
response['index'] = counter
if counter == 0:
for item in outputs:
outputs[item]['data'] += '\r\n\r\n未读取到数据Spug 仅保存最近2周的日志信息。'
if req.is_quick_deploy:
if outputs['local']['data']:
outputs['local']['data'] = f'{human_time()} 读取数据... ' + outputs['local']['data']

View File

@ -84,7 +84,7 @@ class HostExtend(models.Model, ModelMixin):
class Group(models.Model, ModelMixin):
name = models.CharField(max_length=20)
name = models.CharField(max_length=50)
parent_id = models.IntegerField(default=0)
sort_id = models.IntegerField(default=0)
hosts = models.ManyToManyField(Host, related_name='groups')

View File

@ -202,6 +202,8 @@ def fetch_host_extend(ssh):
code, out = ssh.exec_command_raw('hostname -I')
if code == 0:
for ip in out.strip().split():
if len(ip) > 15: # ignore ipv6
continue
if ipaddress.ip_address(ip).is_global:
if len(public_ip_address) < 10:
public_ip_address.add(ip)

View File

@ -114,6 +114,7 @@ def get_overview(request):
for item in Detection.objects.all():
data = {}
for key in json.loads(item.targets):
key = str(key)
data[key] = {
'id': f'{item.id}_{key}',
'group': item.group,

View File

@ -6,6 +6,7 @@ from apps.account.models import History
from apps.alarm.models import Alarm
from apps.schedule.models import Task, History as TaskHistory
from apps.deploy.models import DeployRequest
from apps.app.models import DeployExtend1
from apps.exec.models import ExecHistory
from apps.notify.models import Notify
from apps.deploy.utils import dispatch
@ -21,6 +22,12 @@ def auto_run_by_day():
History.objects.filter(created_at__lt=date_30).delete()
Notify.objects.filter(created_at__lt=date_7, unread=False).delete()
Alarm.objects.filter(created_at__lt=date_30).delete()
for item in DeployExtend1.objects.all():
index = 0
for req in DeployRequest.objects.filter(deploy_id=item.deploy_id, repository_id__isnull=False):
if index > item.versions and req.repository_id:
req.repository.delete()
index += 1
try:
record = ExecHistory.objects.all()[50]
ExecHistory.objects.filter(id__lt=record.id).delete()

View File

@ -131,7 +131,7 @@ AUTHENTICATION_EXCLUDES = (
re.compile('/apis/.*'),
)
SPUG_VERSION = 'v3.1.5'
SPUG_VERSION = 'v3.1.7'
# override default config
try:

View File

@ -10,4 +10,4 @@ export * from './functools';
export * from './router';
export const http = _http;
export const history = _history;
export const VERSION = 'v3.1.5';
export const VERSION = 'v3.1.7';

View File

@ -46,7 +46,7 @@ export default observer(function () {
</Form.Item>
</Col>
<Col span={10}>
<Form.Item required label="版本数量" tooltip="早于指定数量的历史版本会被删除,以释放磁盘空间。">
<Form.Item required label="版本数量" tooltip="早于指定数量的构建纪录及历史版本会被删除,以释放磁盘空间。">
<Input value={info['versions']} onChange={e => info['versions'] = e.target.value} placeholder="请输入保存的版本数量"/>
</Form.Item>
</Col>

View File

@ -55,12 +55,12 @@ export default observer(function () {
<Form.Item required name="name" label="申请标题">
<Input placeholder="请输入申请标题"/>
</Form.Item>
<Form.Item required name="request_id" label="选择版本">
<Form.Item required name="request_id" label="选择版本" tooltip="可选择回滚版本与发布配置中的版本数量配置相关。">
<Select
showSearch
placeholder="请选择回滚至哪个版本"
filterOption={(input, option) => includes(option.props.children, input)}>
{store.records.filter(x => x.deploy_id === deploy_id && ['3', '-3'].includes(x.status)).map((item, index) => (
{store.records.filter(x => x.repository_id && x.deploy_id === deploy_id && ['3', '-3'].includes(x.status)).map((item, index) => (
<Select.Option key={item.id} value={item.id} record={item} disabled={index === 0}>
<div style={{display: 'flex', justifyContent: 'space-between'}}>
<span>{`${item.name} (${item.version})`}</span>

View File

@ -11,6 +11,7 @@ import { http, hasPermission } from 'libs';
import { Action, AuthButton, TableCard } from 'components';
import S from './index.module.less';
import store from './store';
import moment from 'moment';
function DeployConfirm() {
return (
@ -72,7 +73,7 @@ function ComTable() {
className: S.min120,
dataIndex: 'created_at',
sorter: (a, b) => a['created_at'].localeCompare(b['created_at']),
render: v => <Tooltip title={v}>{v ? v.substring(0, 10) : null}</Tooltip>,
render: v => <Tooltip title={v}>{v ? moment(v).fromNow() : null}</Tooltip>,
hide: true
}, {
title: '审核人',
@ -83,7 +84,7 @@ function ComTable() {
title: '审核时间',
className: S.min120,
dataIndex: 'approve_at',
render: v => <Tooltip title={v}>{v ? v.substring(0, 10) : null}</Tooltip>,
render: v => <Tooltip title={v}>{v ? moment(v).fromNow() : null}</Tooltip>,
}, {
title: '发布人',
className: S.min120,
@ -93,7 +94,7 @@ function ComTable() {
title: '发布时间',
className: S.min120,
dataIndex: 'do_at',
render: v => <Tooltip title={v}>{v ? v.substring(0, 10) : null}</Tooltip>,
render: v => <Tooltip title={v}>{v ? moment(v).fromNow() : null}</Tooltip>,
hide: true
}, {
title: '备注',

View File

@ -4,14 +4,15 @@
* Released under the AGPL-3.0 License.
*/
import React from 'react';
import { Breadcrumb, Table, Switch, Progress, Modal, message } from 'antd';
import { Breadcrumb, Table, Switch, Progress, Modal, Input, message } from 'antd';
import {
DeleteOutlined,
DownloadOutlined,
FileOutlined,
FolderOutlined,
HomeOutlined,
UploadOutlined
UploadOutlined,
EditOutlined
} from '@ant-design/icons';
import { AuthButton, Action } from 'components';
import { http, uniqueId, X_TOKEN } from 'libs';
@ -23,10 +24,12 @@ class FileManager extends React.Component {
constructor(props) {
super(props);
this.input = null;
this.input2 = null
this.state = {
fetching: false,
showDot: false,
uploading: false,
inputPath: null,
uploadStatus: 'active',
pwd: [],
objects: [],
@ -97,10 +100,11 @@ class FileManager extends React.Component {
this.setState({fetching: true});
pwd = pwd || this.state.pwd;
const path = '/' + pwd.join('/');
http.get('/api/file/', {params: {id: this.props.id, path}})
return http.get('/api/file/', {params: {id: this.props.id, path}})
.then(res => {
const objects = lds.orderBy(res, [this._kindSort, 'name'], ['desc', 'asc']);
this.setState({objects, pwd})
this.state.inputPath !== null && this.setState({inputPath: path})
})
.finally(() => this.setState({fetching: false}))
};
@ -109,6 +113,7 @@ class FileManager extends React.Component {
let pwd = this.state.pwd.map(x => x);
if (action === '1') {
pwd.push(name)
this.setState({inputPath: null})
} else if (action === '2') {
const index = pwd.indexOf(name);
pwd = pwd.splice(0, index + 1)
@ -118,6 +123,28 @@ class FileManager extends React.Component {
this.fetchFiles(pwd)
};
handlePathInput = (e) => {
const value = e.target.value;
const pwd = value.substring(1).split('/')
this.setState({pwd})
};
handleInputEnter = () => {
if (this.state.inputPath === null) {
if (this.state.pwd.length > 0) {
this.setState({inputPath: `/${this.state.pwd.join('/')}/`})
} else {
this.setState({inputPath: '/'})
}
setTimeout(() => this.input2.focus(), 100)
} else {
let pwdStr = this.state.inputPath.replace(/^\/+/, '')
pwdStr = pwdStr.replace(/\/+$/, '')
this.fetchFiles(pwdStr.split('/'))
.then(() => this.setState({inputPath: null}))
}
}
handleUpload = () => {
this.input.click();
this.input.onchange = e => {
@ -198,16 +225,27 @@ class FileManager extends React.Component {
<React.Fragment>
<input style={{display: 'none'}} type="file" ref={ref => this.input = ref}/>
<div className={styles.drawerHeader}>
<Breadcrumb>
<Breadcrumb.Item href="#" onClick={() => this.handleChdir('', '0')}>
<HomeOutlined/>
</Breadcrumb.Item>
{this.state.pwd.map(item => (
<Breadcrumb.Item key={item} href="#" onClick={() => this.handleChdir(item, '2')}>
<span>{item}</span>
{this.state.inputPath !== null ? (
<Input ref={ref => this.input2 = ref} size="small" className={styles.input}
suffix={<div style={{color: '#999', fontSize: 12}}>回车确认</div>}
value={this.state.inputPath} onChange={e => this.setState({inputPath: e.target.value})}
onPressEnter={this.handleInputEnter}/>
) : (
<Breadcrumb className={styles.bread}>
<Breadcrumb.Item href="#" onClick={() => this.handleChdir('', '0')}>
<HomeOutlined style={{fontSize: 16}}/>
</Breadcrumb.Item>
))}
</Breadcrumb>
{this.state.pwd.map(item => (
<Breadcrumb.Item key={item} href="#" onClick={() => this.handleChdir(item, '2')}>
<span>{item}</span>
</Breadcrumb.Item>
))}
<Breadcrumb.Item onClick={this.handleInputEnter}>
<EditOutlined className={styles.edit}/>
</Breadcrumb.Item>
</Breadcrumb>
)}
<div className={styles.action}>
<span>显示隐藏文件</span>
<Switch

View File

@ -162,6 +162,24 @@
height: 24px;
}
.bread:hover {
.edit {
display: inline-block;
}
}
.input {
width: 60%;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
}
.edit {
display: none;
color: #2563fcbb;
margin-left: 24px;
cursor: pointer;
}
.progress {
width: 94px;
margin-left: 12px;
@ -174,6 +192,15 @@
white-space: nowrap;
margin-right: 24px;
}
:global(.ant-breadcrumb-separator) {
margin: 0 4px;
color: rgba(0, 0, 0, 0.85);
}
:global(.ant-breadcrumb-link) {
color: rgba(0, 0, 0, 0.85);
}
}
.drawerBtn {