Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 1 addition & 119 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,119 +1 @@
<h1 align="center">Spug</h1>

<div align="center">

Spug是面向中小型企业设计的轻量级无Agent的自动化运维平台,整合了主机管理、主机批量执行、主机在线终端、应用发布部署、在线任务计划、配置中心、监控、报警等一系列功能。

</div>

- 公司官网:https://www.spug.cc
- 项目官网:https://ops.spug.cc
- 使用文档:https://ops.spug.cc/docs/about-spug/

## 演示环境

演示地址:https://demo.spug.cc

## 🔐免费通配符SSL证书
免费通配符,付费证书价格亲民,性价比超高,低于市场其他平台价格,免费专家一对一配置服务,购买流程简单快速,且支持7天无理由退款和开具发票。提供一键下载和SSL过期通知配置,免费申请:[https://ssl.spug.cc](https://ssl.spug.cc)


## 🔥推送助手

推送助手是一个集成了电话、短信、邮件、飞书、钉钉、微信、企业微信等多通道的消息推送平台,可以3分钟实现个人电话短信推送,点击体验:[https://push.spug.cc](https://push.spug.cc)


## 特性

- **批量执行**: 主机命令在线批量执行
- **在线终端**: 主机支持浏览器在线终端登录
- **文件管理**: 主机文件在线上传下载
- **任务计划**: 灵活的在线任务计划
- **发布部署**: 支持自定义发布部署流程
- **配置中心**: 支持KV、文本、json等格式的配置
- **监控中心**: 支持站点、端口、进程、自定义等监控
- **报警中心**: 支持短信、邮件、钉钉、微信等报警方式
- **优雅美观**: 基于 Ant Design 的UI界面
- **开源免费**: 前后端代码完全开源


## 环境

* Python 3.6+
* Django 2.2
* Node 12.14
* React 16.11

## 安装

[官方文档](https://ops.spug.cc/docs/install-docker)

更多使用帮助请参考: [使用文档](https://ops.spug.cc/docs/host-manage/)


## 推荐项目
[Yearning — MYSQL 开源SQL语句审核平台](https://github.com/cookieY/Yearning)


## 预览

### 主机管理
![image](https://cdn.spug.cc/img/3.0/host.jpg)

#### 主机在线终端
![image](https://cdn.spug.cc/img/3.0/web-terminal.jpg)

#### 文件在线上传下载
![image](https://cdn.spug.cc/img/3.0/file-manager.jpg)

#### 主机批量执行
![image](https://cdn.spug.cc/img/3.0/host-exec.jpg)
![image](https://cdn.spug.cc/img/3.0/host-exec2.jpg)

#### 应用发布
![image](https://cdn.spug.cc/img/3.0/deploy.jpg)

#### 监控报警
![image](https://cdn.spug.cc/img/3.0/monitor.jpg)

#### 角色权限
![image](https://cdn.spug.cc/img/3.0/user-role.jpg)


## 赞助
<table>
<thead>
<tr>
<th align="center" style="width: 115px;">
<a href="https://www.ucloud.cn/site/active/kuaijie.html?invitation_code=C1xD0E5678FBA77">
<img src="https://cdn.spug.cc/img/ucloud.png" width="115px"><br>
<sub>UCloud</sub><br>
<sub>5 元/月云主机</sub>
</a>
</th>
<th align="center" style="width: 115px;">
<a href="https://www.aliyun.com/minisite/goods?userCode=bkj6b9tn">
<img src="https://cdn.spug.cc/img/aliyun-logo.png" width="115px"><br>
<sub>阿里云</sub><br>
<sub>2核心2G低至99元/年</sub>
</a>
</th>
<th align="center" style="width: 125px;">
<a href="http://www.magedu.com">
<img src="https://cdn.spug.cc/img/magedu-logo.jpeg" width="115px"><br>
<sub>马哥教育</sub><br>
<sub>IT人高薪职业学院</sub>
</a>
</th>
</tr>
</thead>
</table>

## 开发者群
#### 关注Spug运维公众号加微信群、QQ群、获取最新产品动态
<div >
<img src="https://cdn.spug.cc/img/spug-club.jpg" width = "300" height = "300" alt="spug-qq" align=center />
<div>

## License & Copyright
[AGPL-3.0](https://opensource.org/licenses/AGPL-3.0)
整合主机管理、主机批量执行、主机在线终端、应用发布部署、在线任务计划、配置中心、监控、报警等功能
4 changes: 0 additions & 4 deletions spug_api/apps/account/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,4 @@ def has_host_perm(user, target):


def verify_password(password):
if len(password) < 8:
return False
if not all(map(lambda x: re.findall(x, password), ['[0-9]', '[a-z]', '[A-Z]'])):
return False
return True
165 changes: 118 additions & 47 deletions spug_api/apps/host/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,63 +186,134 @@ def fetch_host_extend(ssh):
public_ip_address = set()
private_ip_address = set()
response = {'disk': []}
code, out = ssh.exec_command_raw('nproc')
if code != 0:
code, out = ssh.exec_command_raw("grep -c '^processor' /proc/cpuinfo")
if code == 0:
response['cpu'] = int(out.strip())

# 先检测操作系统类型
code, os_type = ssh.exec_command_raw('uname -s')
is_macos = code == 0 and 'Darwin' in os_type

# 获取 CPU 核心数
if is_macos:
code, out = ssh.exec_command_raw('sysctl -n hw.ncpu')
if code == 0:
response['cpu'] = int(out.strip())
else:
code, out = ssh.exec_command_raw('nproc')
if code != 0:
code, out = ssh.exec_command_raw("grep -c '^processor' /proc/cpuinfo")
if code == 0:
response['cpu'] = int(out.strip())

code, out = ssh.exec_command_raw("cat /etc/os-release | grep PRETTY_NAME | awk -F \\\" '{print $2}'")
if '/etc/os-release' in out:
code, out = ssh.exec_command_raw("cat /etc/issue | head -1 | awk '{print $1,$2,$3}'")
if code == 0:
response['os_name'] = out.strip()[:50]
# 获取操作系统名称
if is_macos:
code, out = ssh.exec_command_raw('sw_vers -productName && sw_vers -productVersion')
if code == 0:
response['os_name'] = 'macOS ' + out.strip().replace('\n', ' ')
else:
code, out = ssh.exec_command_raw("cat /etc/os-release | grep PRETTY_NAME | awk -F \\\" '{print $2}'")
if '/etc/os-release' in out:
code, out = ssh.exec_command_raw("cat /etc/issue | head -1 | awk '{print $1,$2,$3}'")
if code == 0:
response['os_name'] = out.strip()[:50]

code, out = ssh.exec_command_raw('hostname -I')
# 获取 IP 地址
if is_macos:
code, out = ssh.exec_command_raw("ifconfig | grep 'inet ' | grep -v 127.0.0.1 | awk '{print $2}'")
else:
code, out = ssh.exec_command_raw('hostname -I')

if code == 0:
for ip in out.strip().split():
if len(ip) > 15: # ignore ipv6
# 过滤掉 IPv6 和空字符串
if not ip or ':' in ip or len(ip) > 15:
continue
try:
if ipaddress.ip_address(ip).is_global:
if len(public_ip_address) < 10:
public_ip_address.add(ip)
elif len(private_ip_address) < 10:
private_ip_address.add(ip)
except:
continue
if ipaddress.ip_address(ip).is_global:
if len(public_ip_address) < 10:
public_ip_address.add(ip)
elif len(private_ip_address) < 10:
private_ip_address.add(ip)

ssh_hostname = ssh.arguments.get('hostname')
if ip_validator(ssh_hostname):
if ipaddress.ip_address(ssh_hostname).is_global:
if ssh_hostname in public_ip_address:
public_ip_address.remove(ssh_hostname)
public_ip_address = [ssh_hostname] + list(public_ip_address)
else:
if ssh_hostname in private_ip_address:
private_ip_address.remove(ssh_hostname)
private_ip_address = [ssh_hostname] + list(private_ip_address)

code, out = ssh.exec_command_raw('lsblk -dbn -o SIZE -e 11 2> /dev/null')
if code == 0:
disks = []
for item in out.strip().splitlines():
item = item.strip()
size = math.ceil(int(item) / 1024 / 1024 / 1024)
if size > 10:
disks.append(size)
response['disk'] = disks[:10]

code, out = ssh.exec_command_raw("dmidecode -t 17 | grep -E 'Size: [0-9]+' | awk '{s+=$2} END {print s,$3}'")
if code == 0:
fields = out.strip().split()
if len(fields) == 2 and fields[1] in ('GB', 'MB'):
size, unit = out.strip().split()
if unit == 'GB':
response['memory'] = size
try:
if ipaddress.ip_address(ssh_hostname).is_global:
if ssh_hostname in public_ip_address:
public_ip_address.remove(ssh_hostname)
public_ip_address = [ssh_hostname] + list(public_ip_address)
else:
response['memory'] = round(int(size) / 1024, 0)
if 'memory' not in response:
code, out = ssh.exec_command_raw("cat /proc/meminfo | grep 'MemTotal' | awk '{print $2}'")
if ssh_hostname in private_ip_address:
private_ip_address.remove(ssh_hostname)
private_ip_address = [ssh_hostname] + list(private_ip_address)
except:
pass

# 获取磁盘信息
if is_macos:
code, out = ssh.exec_command_raw("diskutil list | grep -E '^[[:space:]]+[0-9]+:' | grep -v 'Apple_APFS' | awk '{print $3}'")
if code == 0:
response['memory'] = math.ceil(int(out) / 1024 / 1024)
disks = []
for size_str in out.strip().splitlines():
try:
# macOS 的 diskutil 输出格式是 "10.5 GB" 这样的
size_parts = size_str.strip().split()
if len(size_parts) == 2:
size_num = float(size_parts[0])
size_unit = size_parts[1]
if size_unit in ('GB', 'GiB'):
size_gb = int(size_num)
elif size_unit in ('MB', 'MiB'):
size_gb = int(size_num / 1024)
else:
size_gb = int(size_num)

if size_gb > 10:
disks.append(size_gb)
except:
continue
response['disk'] = disks[:10]
else:
# 如果 diskutil 失败,尝试用 df 粗略估计
code, out = ssh.exec_command_raw("df -k / | tail -1 | awk '{print $2}'")
if code == 0:
total_kb = int(out.strip())
response['disk'] = [math.ceil(total_kb / 1024 / 1024)]
else:
code, out = ssh.exec_command_raw('lsblk -dbn -o SIZE -e 11 2> /dev/null')
if code == 0:
disks = []
for item in out.strip().splitlines():
item = item.strip()
size = math.ceil(int(item) / 1024 / 1024 / 1024)
if size > 10:
disks.append(size)
response['disk'] = disks[:10]

# 获取内存信息
if is_macos:
code, out = ssh.exec_command_raw('sysctl -n hw.memsize')
if code == 0:
memory_bytes = int(out.strip())
response['memory'] = math.ceil(memory_bytes / 1024 / 1024 / 1024) # 转换为 GB
else:
code, out = ssh.exec_command_raw("dmidecode -t 17 | grep -E 'Size: [0-9]+' | awk '{s+=$2} END {print s,$3}'")
if code == 0:
fields = out.strip().split()
if len(fields) == 2 and fields[1] in ('GB', 'MB'):
size, unit = out.strip().split()
if unit == 'GB':
response['memory'] = size
else:
response['memory'] = round(int(size) / 1024, 0)
if 'memory' not in response:
code, out = ssh.exec_command_raw("cat /proc/meminfo | grep 'MemTotal' | awk '{print $2}'")
if code == 0:
response['memory'] = math.ceil(int(out) / 1024 / 1024)

# 如果还是没获取到内存,给个默认值
if 'memory' not in response:
response['memory'] = 4 # 默认 4GB

response['public_ip_address'] = list(public_ip_address)
response['private_ip_address'] = list(private_ip_address)
Expand Down
9 changes: 2 additions & 7 deletions spug_web/config-overrides.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License.
*/
const {override, addDecoratorsLegacy, addLessLoader} = require('customize-cra');

module.exports = override(
Expand All @@ -11,8 +6,8 @@ module.exports = override(
lessOptions: {
javascriptEnabled: true,
modifyVars: {
'@primary-color': '#2563fc'
}
'@primary-color': '#13c2c2'
}
}
}),
);
Loading