├── .github └── workflows │ ├── auto_deploy.yaml │ ├── auto_restart.yaml │ └── manual_start.yaml ├── README.md ├── cloudf.py ├── conf └── __init__.py ├── dates.py ├── default_config.json ├── github_opt.png ├── logger.py ├── login_auto_deploy.py ├── requirements.txt ├── serv.py ├── serv00-autodeploy.iml ├── ssh.py ├── sshs.py └── user_info_example.json /.github/workflows/auto_deploy.yaml: -------------------------------------------------------------------------------- 1 | name: auto deploy 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - release 8 | paths-ignore: 9 | - 'README.md' 10 | - 'user_info_example.json' 11 | - 'default_config.json' 12 | 13 | jobs: 14 | auto_deploy: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout 仓库代码 19 | uses: actions/checkout@v2 20 | 21 | - name: 设置 Python 环境 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: '3.12.3' # 设置你希望使用的 Python 版本,建议使用稳定版本 25 | 26 | - name: Create user_info.json from environment variable 27 | run: echo "$USER_INFO" > user_info.json 28 | env: 29 | USER_INFO: ${{ secrets.USER_INFO}} # 从GitHub Secrets中获取环境变量 30 | - name: Create user_info_manual.json from environment variable 31 | run: echo "$USER_INFO_MANUAL" > user_info_manual.json 32 | env: 33 | USER_INFO_MANUAL: ${{ secrets.USER_INFO_MANUAL}} # 从GitHub Secrets中获取环境变量 34 | 35 | - name: Create env_config from environment variable 36 | run: echo "$ENV_CONFIG" > env_config.json 37 | env: 38 | ENV_CONFIG: ${{ secrets.ENV_CONFIG}} # 从GitHub variables中获取环境变量 39 | 40 | - name: 安装依赖 41 | run: | 42 | python -m pip install --upgrade pip 43 | pip install -r requirements.txt 44 | 45 | - name: 运行自动化部署 46 | env: 47 | USER_INFO: ${{ secrets.USER_INFO }} 48 | ENV_CONFIG: ${{ secrets.ENV_CONFIG }} 49 | USER_INFO_MANUAL: ${{ secrets.USER_INFO_MANUAL }} 50 | run: | 51 | python login_auto_deploy.py -------------------------------------------------------------------------------- /.github/workflows/auto_restart.yaml: -------------------------------------------------------------------------------- 1 | name: auto restart 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 */8 * * *" # 每6小时运行一次,可以根据需求调整时间 7 | push: 8 | branches: 9 | - release 10 | jobs: 11 | auto_restart: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout 仓库代码 16 | uses: actions/checkout@v2 17 | 18 | - name: 设置 Python 环境 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: '3.12.3' # 设置你希望使用的 Python 版本,建议使用稳定版本 22 | 23 | - name: Create user_info.json from environment variable 24 | run: echo "$USER_INFO" > user_info.json 25 | env: 26 | USER_INFO: ${{ secrets.USER_INFO}} # 从GitHub Secrets中获取环境变量 27 | 28 | - name: Create env_config.json from environment variable 29 | run: echo "$ENV_CONFIG" > env_config.json 30 | env: 31 | ENV_CONFIG: ${{ secrets.ENV_CONFIG}} # 从GitHub variables中获取环境变量 32 | 33 | - name: 安装依赖 34 | run: | 35 | python -m pip install --upgrade pip 36 | pip install -r requirements.txt 37 | 38 | - name: 运行登录脚本 39 | env: 40 | USER_INFO: ${{ secrets.USER_INFO }} 41 | ENV_CONFIG: ${{ secrets.ENV_CONFIG }} 42 | ENV_CMD: ${{ secrets.ENV_CMD }} 43 | run: | 44 | python login_auto_deploy.py -------------------------------------------------------------------------------- /.github/workflows/manual_start.yaml: -------------------------------------------------------------------------------- 1 | name: manual start 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | start_type: 7 | description: '请选择启动方式' 8 | required: true 9 | default: 'restart' 10 | type: choice 11 | options: 12 | - restart 13 | - reset 14 | - keepalive 15 | interval: 16 | description: '请选择保活间隔时间(分钟)' 17 | required: true 18 | default: '20' 19 | type: choice 20 | options: 21 | - 0 22 | - 20 23 | - 30 24 | - 40 25 | - 50 26 | - 60 27 | 28 | jobs: 29 | manual_restart: 30 | runs-on: ubuntu-latest 31 | 32 | steps: 33 | - name: Checkout 仓库代码 34 | uses: actions/checkout@v2 35 | 36 | - name: 设置 Python 环境 37 | uses: actions/setup-python@v2 38 | with: 39 | python-version: '3.12.3' # 设置你希望使用的 Python 版本,建议使用稳定版本 40 | 41 | - name: Create user_info.json from environment variable 42 | run: echo "$USER_INFO" > user_info.json 43 | env: 44 | USER_INFO: ${{ secrets.USER_INFO}} # 从GitHub Secrets中获取环境变量 45 | 46 | - name: Create env_config.json from environment variable 47 | run: echo "$ENV_CONFIG" > env_config.json 48 | env: 49 | ENV_CONFIG: ${{ secrets.ENV_CONFIG}} # 从GitHub variables中获取环境变量 50 | 51 | - name: 安装依赖 52 | run: | 53 | python -m pip install --upgrade pip 54 | pip install -r requirements.txt 55 | - name: 设置运行命令 56 | run: | 57 | echo "start_type: $START_TYPE" 58 | echo "INTERVAL: $INTERVAL" 59 | echo "ENV_CMD: py $START_TYPE $INTERVAL" 60 | env: 61 | INTERVAL: ${{ inputs.interval }} 62 | START_TYPE: ${{ inputs.start_type }} 63 | - name: 运行登录脚本 64 | env: 65 | USER_INFO: ${{ secrets.USER_INFO }} 66 | ENV_CONFIG: ${{ secrets.ENV_CONFIG }} 67 | ENV_CMD: py ${{ inputs.start_type }} ${{ inputs.interval }} 68 | run: | 69 | python login_auto_deploy.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serv00与CT8自动化部署启动,保号续期保活,节点被删自动重新部署,发送节点消息到Telegram 2 | 3 | ## 利用github Action以及python脚本实现,支持多个账号批量部署,CF CDN自动配置 4 | 5 | ## 🙏🙏🙏点个Star!!Star!!Star!! 6 | 7 | ### 将代码fork到你的仓库并运行的操作步骤 8 | 9 | ## (一). Fork 仓库 10 | 11 | ``` 12 | 1、访问原始仓库页面: 打开你想要 fork 的 GitHub 仓库页面。 13 | 2、Fork 仓库 点击页面右上角的 "Fork" 按钮,将仓库 fork 到你的 GitHub 账户下。 14 | 3、若没有Serv00服务器 请参照(六)服务器Serv00免费申请教程,从教程【搭建vless节点】步骤开始就可使用本项目进行自动化部署: 15 | 包括自动申请随机端口、自动化配置服务器运行环境、自建节点启动,节点保活节点下线自动重启,支持node和PM2两种启动方式 16 | ``` 17 | 18 | ## (二). 设置 Telegram token 19 | 20 | ``` 21 | 1、创建 Telegram Bot 【非必须】,如果没有配置此项 tg无法收到节点信息 22 | 在 Telegram 中找到 @BotFather,创建一个新 Bot,并获取 API Token。 23 | 获取到你的 Chat ID 方法一,在一休技术交流群里发送/id@KinhRoBot获取,返回用户信息中的ID就是Chat ID 24 | 获取到你的 Chat ID 方法二,可以通过向 Bot 发送一条消息,然后访问 https://api.telegram.org/bot/getUpdates 找到 Chat ID 25 | ``` 26 | 27 | ## (三). 配置 GitHub Secrets 28 | 29 | **1、Setting->Secrets->actions 添加secrets名称:ENV_CONFIG**
30 | (注:此项可直接复制下面的内容配置即可) 31 | 32 | ```json 33 | { 34 | "env_config": { 35 | "reset": 1, 36 | "outo_npm_install": 1, 37 | "code_source_url": "git clone http://github.com/zjxde/serv00-ws", 38 | "kill_pid_path": "serv00", 39 | "nodejs_name": "index" 40 | } 41 | } 42 | ``` 43 | 44 | **2、Setting->Secrets->actions 添加secrets名称:USER_INFO** 45 | 检验配置内容是否json格式有效地址:https://www.bejson.com/explore/index_new/ 46 | 47 | ``` 48 | (注:此项参数中括号只是配置说明,需要修改成你自己的) 49 | 配置格式为json格式:{"tg_config"{你的配置},accounts:[{账号一配置},{账号二配置},......]} 50 | ``` 51 | 52 | ```json 53 | { 54 | "tg_config": { 55 | "tg_bot_token": "tg token", 56 | "tg_chat_id": "tg chat id", 57 | "send_tg": 1, 58 | "node_num": 2, 59 | "usepm2": 0, 60 | "cf_token": "xxx", 61 | "cf_username": "xxx" 62 | 63 | }, 64 | "accounts": [ 65 | { 66 | "username": "【用户名】", 67 | "password": "【密码】", 68 | "domain": "【域名】", 69 | "pannelnum": 6, 70 | "cmd":"python reset 20", 71 | "server_type": 1, 72 | "use_cf": 1, 73 | "ssl_domains": "xxx.username.us.kg,xxx2.username.us.kg" 74 | }, 75 | { 76 | "username": "【用户名】", 77 | "password": "【密码】", 78 | "domain": "【域名】", 79 | "pannelnum": 6, 80 | "cmd":"python reset 20", 81 | "server_type": 1, 82 | "use_cf": 0 83 | } 84 | ] 85 | } 86 | 87 | ``` 88 | 89 | **3、Setting->Secrets->actions 添加secrets名称:ENV_CMD,值复制下面内容** 90 | 91 | ``` 92 | py restart 30 93 | ``` 94 | 95 | **4、ENV_CONFIG 配置项参数说明** 96 | 97 | |参数名称|参数说明| 98 | |--|--| 99 | |port|节点端口号| 100 | |reset|是否需要重装节点 0:不重装 1:重装| 101 | |outo_npm_install|是否自动安装 0:手动安装 1 : 自动安装 此项默认即可| 102 | |code_source_url|节点代码地址 默认即可| 103 | |kill_pid_path|默认即可| 104 | |nodejs_name|节点布置文件node.js的名称 默认即可| 105 | |uuid|节点uuid值,可以不修改| 106 | 107 | **3、USER_INFO 配置项参数说明** 108 | 109 | |参数名称| 参数说明 | 110 | |--|----------------------------------------------------------------------------------------------------------------------------------| 111 | |tg_chat_id| Chat ID 当send_tg=0此项默认即可 | 112 | |send_tg| 是否需要发送节点信息到telegram ,1:开启 0:不开启 | 113 | |node_num| 开启节点个数,由于节点serv00端口限制,最多可设3个,默认2个 | 114 | |usepm2| 是否开启pm2 1:开启 0:不开启 ,默认不开启, 比较耗资源建议不开启 | 115 | |username| **用户名** | 116 | |password| **密码** | 117 | |domain| **你申请的域名,如CF绑定的域名,或serv00默认域名** | 118 | |pannelnum| 你申请机器号,如panel6.serv00.com就设置为6, CT8此项默认即可 | 119 | |server_type| 服务器类型 1: Serv00 2: CT8 ,默认为1 | 120 | |cmd| **reset:重新安装节点 ,keepalive:保活 ,restart:只重启 ,三种模式后面参数都可跟保活间隔时间 ,单位为分钟 如: python restart 20 ,就表示重启并保活 时间设置为20分钟,若不加时间参数,则不会进行节点保活** | 121 | |basepath| 节点部署目录:默认 /home/XXX[用户名]/domains/XXX[域名]/app2/serv00-ws/ | 122 | |tg_bot_token| 申请tg机器人的token | 123 | |cf_username| cloudflare 用户名 当use_cf=0 此项默认即可 | 124 | |cf_token| cloudflare API 密钥 (**Global API Key**) https://dash.cloudflare.com/profile/api-tokens ,当use_cf=0此项默认即可 | 125 | |use_cf| 是否开启CF CDN 1:开启 0:不开启 | 126 | |ssl_domains| 当use_cf =1时 此项必填,CF CDN记录配置域名如:test.junx066.us.kg 多个域名以英文逗号隔开 | 127 | ## (四). 启动 GitHub Actions 128 | 129 | ``` 130 | 1、配置 GitHub Actions 131 | 》在你的 fork 仓库中,进入 Actions 页面。 132 | 》如果 Actions 没有自动启用,点击 Enable GitHub Actions 按钮以激活它。 133 | 2、运行工作流 134 | 》GitHub Actions 将会根据你设置自动运行脚本。 135 | 》如果需要手动触发,可以在 Actions 页面手动运行工作流。 136 | 3、工作流文件有 137 | auto_deploy.yaml:一开始部署使用此任务 138 | auto_restart.yaml : 可自定义命令重启服务器 注使用该使用需要手动配置 secrets 名称为 ENV_CMD,值为 py restart 10【推荐】 ,后面 139 | 的数值可按自己需求自行修改( 参数说明:reset:重新初始化环境 keepalive:保活 restart:只重启 三种模式后面参数都可跟保活间隔时间,单位为分钟) 140 | ``` 141 | 142 | ### 操作步骤说明 143 | 144 | ![执行action](github_opt.png) 145 | 146 |
147 | 148 | ## (五).注意事项 149 | 150 | ``` 151 | 1、保密性: Secrets 是敏感信息,请确保不要将它们泄露到公共代码库或未授权的人员。 152 | 2、更新和删除: 如果需要更新或删除 Secrets,可以通过仓库的 Secrets 页面进行管理。 153 | 3、通过以上步骤,你就可以成功将代码 fork 到你的仓库下并运行了。如果需要进一步的帮助或有其他问题,请随时告知! 154 | ``` 155 | 156 | ## (六).相关教程 157 | 158 | 1、[服务器Serv00免费申请教程](https://blog.yixiu.us.kg/posts/gratis/freevpsandvless/) 159 | 2、[Serv00与ct8自动化批量保号](https://github.com/yixiu001/serv00-login) 160 | 3、[TG技术交流群](https://t.me/yxjsjl) 161 | 4、[校验json地址](https://www.bejson.com/explore/index_new/) 162 | -------------------------------------------------------------------------------- /cloudf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import requests 3 | import jsonpath 4 | import json 5 | import logging 6 | from logger import Mylogger 7 | 8 | """ 9 | CF api处理类 10 | """ 11 | class CFServer(object): 12 | 13 | def __init__(self, username, token): 14 | self.logger = Mylogger.getCommonLogger("cfserver.log", logging.INFO, 1) 15 | self.headers = { 16 | 'Accept': '*/*', 17 | 'Accept-Language': 'en-US,en;q=0.8', 18 | 'Cache-Control': 'max-age=0', 19 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36', 20 | 'Connection': 'keep-alive', 21 | 'Content-Type': 'application/json', 22 | 'X-Auth-Email': username, 23 | 'X-Auth-Key': token, 24 | } 25 | self.LIST_ZONES = 'https://api.cloudflare.com/client/v4/zones' 26 | self.LIST_DNS_RECONDS = 'https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records' 27 | self.LIST_ORIGIN_RULES = 'https://api.cloudflare.com/client/v4/zones/{zone_id}/rulesets/phases/http_request_origin/entrypoint' 28 | self.DELETE_DNS_RECORD = 'https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records/{dns_record_id}' 29 | self.session = requests.session() 30 | """ 31 | 获取域名列表 32 | zoneId:域名id 33 | """ 34 | def listZones(self): 35 | rep = self.session.get(self.LIST_ZONES, headers=self.headers) 36 | if rep and rep.status_code == 200: 37 | data = json.loads(rep.content) 38 | zoneIds = jsonpath.jsonpath(data, "$.result[*].id") 39 | zoneNames = jsonpath.jsonpath(data, "$.result[*].name") 40 | return zoneIds,zoneNames 41 | """ 42 | 获取DNS记录列表 43 | zoneId:域名id 44 | """ 45 | def getDNSByZoneId(self, zoneId): 46 | if zoneId: 47 | url = self.LIST_DNS_RECONDS.format(zone_id=zoneId) 48 | rep = self.session.get(url, headers=self.headers) 49 | if rep and rep.status_code == 200: 50 | data = json.loads(rep.content) 51 | dnsRecords = jsonpath.jsonpath(data, "$.result[*]") 52 | 53 | return dnsRecords 54 | return None 55 | def deleteDNSByZoneId(self, zoneId,recordId): 56 | if zoneId: 57 | url = self.DELETE_DNS_RECORD.format(zone_id=zoneId,dns_record_id=recordId) 58 | rep = self.session.delete(url, headers=self.headers) 59 | result={} 60 | data = json.loads(rep.content) 61 | result['success'] = data['success'] 62 | result['errors'] = data['errors'] 63 | result['messages'] = data['messages'] 64 | self.logger.info(f"{result}") 65 | if rep and rep.status_code == 200: 66 | dnsRecordId = jsonpath.jsonpath(data, "$.result.id") 67 | return dnsRecordId 68 | return None 69 | def createDNSByZoneId(self,zoneId,ownDomain,servDomain,proxied): 70 | dnsRecordId = None 71 | if zoneId: 72 | url = self.LIST_DNS_RECONDS.format(zone_id=zoneId) 73 | dnsRecord = { 74 | "content": servDomain, 75 | "name": ownDomain, 76 | "proxied": proxied, 77 | "proxiable": proxied, 78 | "type": "CNAME", 79 | "ttl": 1 80 | } 81 | dd = json.dumps(dnsRecord) 82 | rep = self.session.post(url, data=dd,headers=self.headers) 83 | data = json.loads(rep.content) 84 | result={} 85 | result['success'] = data['success'] 86 | result['errors'] = data['errors'] 87 | result['messages'] = data['messages'] 88 | self.logger.info(f"{ownDomain} ::{result}") 89 | if rep and rep.status_code == 200: 90 | dnsRecordId = jsonpath.jsonpath(data, "$.result.id") 91 | 92 | return dnsRecordId 93 | 94 | """ 95 | 获取规则列表 96 | zoneId:域名id 97 | """ 98 | def listOriginRules(self, zoneId): 99 | if zoneId: 100 | url = self.LIST_ORIGIN_RULES.format(zone_id=zoneId) 101 | rep = self.session.get(url, headers=self.headers) 102 | if rep and rep.status_code == 200: 103 | data = json.loads(rep.content) 104 | rules = jsonpath.jsonpath(data, "$.result[*][*]") 105 | return rules 106 | return None 107 | 108 | 109 | """ 110 | 更新Origin Rules 111 | zoneId:域名id 112 | 113 | domain:域名 114 | redirectPorts:待更新端口 115 | des:描述规则 116 | ruleId:规则id 117 | """ 118 | def updateRuleV2(self, zoneId, updateDomains): 119 | if updateDomains and len(updateDomains)>0: 120 | rules = [] 121 | for domain in updateDomains: 122 | p = updateDomains[domain] 123 | result = {} 124 | des = domain.split(".")[0] 125 | if zoneId: 126 | url = self.LIST_ORIGIN_RULES.format(zone_id=zoneId) 127 | 128 | rule = { 129 | "action": "route", 130 | "action_parameters": { 131 | "origin": { 132 | "port": int(p) 133 | } 134 | }, 135 | "enabled": True, 136 | "description": des, 137 | "expression": "(http.host eq \""+domain+"\")" 138 | } 139 | rules.append(rule) 140 | data = {"description": "domain", "rules": rules} 141 | dd = json.dumps(data) 142 | rep = self.session.put(url, data=dd, headers=self.headers) 143 | #self.logger.info(rep.content) 144 | if rep: 145 | data = json.loads(rep.content) 146 | result['success'] = data['success'] 147 | result['errors'] = data['errors'] 148 | result['messages'] = data['messages'] 149 | return result 150 | 151 | """ 152 | 更新Origin Rules 153 | zoneId:域名id 154 | 155 | domain:域名 156 | redirectPorts:待更新端口 157 | des:描述规则 158 | ruleId:规则id 159 | """ 160 | def updateRule(self, zoneId, updateDomains): 161 | if updateDomains and len(updateDomains)>0: 162 | rules = [] 163 | for domain in updateDomains: 164 | p = updateDomains[domain] 165 | result = {} 166 | des = domain.split(".")[0] 167 | if zoneId: 168 | url = self.LIST_ORIGIN_RULES.format(zone_id=zoneId) 169 | 170 | rule = { 171 | "action": "route", 172 | "action_parameters": { 173 | "origin": { 174 | "port": p 175 | } 176 | }, 177 | "enabled": True, 178 | "description": des, 179 | "expression": "(http.host eq \""+domain+"\")" 180 | } 181 | rules.append(rule) 182 | data = {"description": "domain", "rules": rules} 183 | dd = json.dumps(data) 184 | rep = self.session.put(url, data=dd, headers=self.headers) 185 | #self.logger.info(rep.content) 186 | if rep: 187 | data = json.loads(rep.content) 188 | result['success'] = data['success'] 189 | result['errors'] = data['errors'] 190 | result['messages'] = data['messages'] 191 | return result 192 | """ 193 | 更新Origin Rules 主方法 194 | domain:域名 195 | ports:待更新端口 196 | """ 197 | def runMain(self, ownDomains,servDomain, ports): 198 | 199 | 200 | updateDomains = {} 201 | zones,zoneNames = self.listZones() 202 | domain_zoneNames = dict(zip(zoneNames,zones)) 203 | normalZoneId = 0 204 | #判断是否存在域名 205 | if ownDomains and len(ownDomains) >0: 206 | for ownDomain in ownDomains: 207 | if ownDomain: 208 | for zoneName in domain_zoneNames: 209 | if zoneName and zoneName in ownDomain: 210 | normalZoneId = domain_zoneNames[zoneName] 211 | break 212 | if normalZoneId: 213 | for index,ownDomain in enumerate(ownDomains): 214 | isNormal = 0 215 | dnsRecords = self.getDNSByZoneId(normalZoneId) 216 | if dnsRecords and len(dnsRecords) > 0: 217 | # 查找当前域名是否开启dns 218 | for record in dnsRecords: 219 | recordName = record['name'] 220 | domain = recordName 221 | if ownDomain == recordName: 222 | recordContext = record['content'] 223 | if servDomain==recordContext: 224 | isNormal = 1 225 | updateDomains[recordName] = ports[index] 226 | if record['proxied']: 227 | self.logger.info(domain + "::已经开启dns代理") 228 | else: 229 | self.logger.info(domain + "::未开启dns代理,请务必先配置") 230 | break 231 | else: 232 | self.deleteDNSByZoneId(normalZoneId,record['id']) 233 | if not isNormal: 234 | updateDomains[ownDomain] = ports[index] 235 | self.logger.info(recordName + "::未配置域名dns记录,自动帮配置") 236 | self.createDNSByZoneId(normalZoneId,ownDomain,servDomain,True) 237 | else: 238 | self.logger.info(ownDomains[index] + "::未配置域名dns记录,自动帮配置") 239 | updateDomains[ownDomain] = ports[index] 240 | self.createDNSByZoneId(normalZoneId,ownDomain,servDomain,True) 241 | 242 | #else: 243 | #self.logger.info(recordName + "::未配置域名dns记录,请务必先配置") 244 | 245 | 246 | if updateDomains and len(updateDomains)>0: 247 | rules = self.listOriginRules(normalZoneId) 248 | oldPorts = [] 249 | key = "" 250 | oldkey = "" 251 | for domain in updateDomains: 252 | port = updateDomains[domain] 253 | key += domain 254 | key += "_"+str(port)+"_" 255 | if rules and len(rules) > 0: 256 | 257 | for rule in rules: 258 | # 检查端口是否配置 259 | expression = rule['expression'] 260 | rid = rule['id'] 261 | # 该域名已经配置规则,更新 262 | if expression and domain in expression: 263 | port = rule['action_parameters']['origin']['port'] 264 | oldkey += domain 265 | oldkey += "_"+str(port)+"_" 266 | if key == oldkey: 267 | self.logger.info(f"{domain}:提供的新端口与旧端口一致,不操作") 268 | else: 269 | # 创建规则 270 | res = self.updateRuleV2(normalZoneId, updateDomains) 271 | self.logger.info(f"{domain}:创建规则结果为:{res}") 272 | else: 273 | self.logger.info(f"{ownDomain}:未找到相关域名") 274 | """ 275 | 更新Origin Rules 主方法 276 | domain:域名 277 | ports:待更新端口 278 | """ 279 | def runNewMain(self, domains, ports): 280 | domain_ports = dict(zip(domains,ports)) 281 | updateDomains = {} 282 | zones = self.listZones() 283 | isNormal = 0 284 | normalZoneId = None 285 | if zones and len(zones) > 0: 286 | for zoneId in zones: 287 | dnsRecords = self.getDNSByZoneId(zoneId) 288 | if dnsRecords and len(dnsRecords) > 0: 289 | # 查找当前域名是否开启dns 290 | for record in dnsRecords: 291 | recordName = record['name'] 292 | domain = recordName 293 | if recordName in domain_ports: 294 | updateDomains[recordName] = domain_ports[recordName] 295 | if record['proxied']: 296 | self.logger.info(domain + "::已经开启dns代理") 297 | isNormal = 1 298 | 299 | else: 300 | isNormal = 1 301 | self.logger.info(domain + "::未开启dns代理,请务必先配置") 302 | break 303 | if isNormal: 304 | 305 | break 306 | #else: 307 | #self.logger.info(recordName + "::未配置域名dns记录,请务必先配置") 308 | des = domain.split(".")[0] 309 | 310 | if isNormal: 311 | if updateDomains and len(updateDomains)>0: 312 | rules = self.listOriginRules(normalZoneId) 313 | newPorts = [] 314 | oldPorts = [] 315 | for domain in updateDomains: 316 | p = updateDomains[domain] 317 | key = domain+"_"+str(p) 318 | newPorts.append(key) 319 | if rules and len(rules) > 0: 320 | 321 | for rule in rules: 322 | # 检查端口是否配置 323 | expression = rule['expression'] 324 | rid = rule['id'] 325 | # 该域名已经配置规则,更新 326 | if expression and domain in expression: 327 | port = rule['action_parameters']['origin']['port'] 328 | oldKey = domain+"_"+str(port) 329 | oldPorts.append(oldKey) 330 | pp = set(newPorts)-set(oldPorts) 331 | if not pp: 332 | self.logger.info(f"{domain}:提供的新端口与旧端口一致,不操作") 333 | else: 334 | # 创建规则 335 | res = self.updateRule(normalZoneId, updateDomains) 336 | self.logger.info(f"{domain}:创建规则结果为:{res}") 337 | @staticmethod 338 | def run(ownDomain,servDomain,ports,username,token): 339 | cf = CFServer(username, token) 340 | cf.runMain(ownDomain,servDomain,ports) 341 | #cf.runNewMain(ownDomain,ports) 342 | 343 | 344 | if __name__ == '__main__': 345 | print("=============") 346 | 347 | -------------------------------------------------------------------------------- /conf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjxde/serv00-autodeploy/37df353b35c976d9c35d0b9caf1f869b339d2b31/conf/__init__.py -------------------------------------------------------------------------------- /dates.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Author: jxing666 3 | from datetime import datetime, timedelta 4 | class DateUtils(object): 5 | 6 | @staticmethod 7 | def dateOperations(date=None, timedelta_kwargs=None): 8 | """ 9 | 日期操作工具 10 | :param date: datetime or date str 11 | :param timedelta_kwargs: date operations kwargs 12 | """ 13 | if timedelta_kwargs and not isinstance(timedelta_kwargs, dict): 14 | raise ValueError("参数错误") 15 | if date: 16 | if isinstance(date, str): 17 | # 将时间字符串解析为日期对象 18 | date = datetime.strptime(date, "%Y-%m-%d") 19 | elif isinstance(date, datetime): 20 | pass 21 | else: 22 | raise TypeError("日期类型错误") 23 | else: 24 | date = datetime.now() 25 | new_date_after_addition = date + timedelta(**timedelta_kwargs) 26 | return str(new_date_after_addition)[:19] 27 | 28 | 29 | if __name__ == '__main__': 30 | print(f"当前时间: {str(datetime.now())[:10]} +3天 = :", DateUtils.dateOperations(timedelta_kwargs={"days": 3})) 31 | print(f"当前时间: {str(datetime.now())[:10]} -3天 = :", DateUtils.dateOperations(timedelta_kwargs={"days": -3})) 32 | 33 | 34 | # 指定日期字符串 35 | print(f"时间: 2023-11-01 +3天 = :", DateUtils.dateOperations("2023-11-01", timedelta_kwargs={"days": 3})) 36 | print(f"时间: 2023-11-01 +3天 = :", DateUtils.dateOperations("2023-11-01", timedelta_kwargs={"days": -3})) 37 | print(f"时间: 2023-11-01 +3天 = :", DateUtils.dateOperations("2023-11-01", timedelta_kwargs={"days": -3})) -------------------------------------------------------------------------------- /default_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "env_config": { 3 | "reset": 0, 4 | "outo_npm_install": 1, 5 | "code_source_url": "git clone http://github.com/zjxde/serv00-ws", 6 | "kill_pid_path": "serv00", 7 | "nodejs_name": "index" 8 | } 9 | } -------------------------------------------------------------------------------- /github_opt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjxde/serv00-autodeploy/37df353b35c976d9c35d0b9caf1f869b339d2b31/github_opt.png -------------------------------------------------------------------------------- /logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | from logging.handlers import RotatingFileHandler 4 | class Mylogger(object): 5 | def __init__(self): 6 | logging.info("mylogger init") 7 | 8 | 9 | # 创建一个logger对象 10 | @classmethod 11 | def getLogger(self,logName,logFileName,logLevel,maxSize,backupCount,console): 12 | 13 | logger = logging.getLogger(logName) 14 | if console==1 : 15 | logger.setLevel(logging.INFO) # 设置日志级别 16 | 17 | # 创建一个handler,用于将日志打印到控制台 18 | console_handler = logging.StreamHandler() 19 | console_handler.setLevel(logging.INFO) 20 | 21 | # 创建一个formatter,用于控制日志的输出格式 22 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 23 | 24 | # 将formatter添加到handler 25 | console_handler.setFormatter(formatter) 26 | 27 | # 将handler添加到logger 28 | logger.addHandler(console_handler) 29 | else: 30 | 31 | # 创建一个RotatingFileHandler,设置文件的最大字节和文件的最大数量 32 | rotating_handler = RotatingFileHandler(logFileName, maxBytes=maxSize, backupCount=backupCount) 33 | rotating_handler.setLevel(logLevel) 34 | 35 | # 创建一个Formatter对象 36 | formatter = logging.Formatter('%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s') 37 | rotating_handler.setFormatter(formatter) 38 | logger.setLevel(logLevel) 39 | logger.addHandler(rotating_handler) 40 | return logger 41 | @classmethod 42 | def getCommonLogger(self,logFileName,logLevel,console): 43 | return self.getLogger(Mylogger.__name__,logFileName,logLevel,100*1024*1024,2,console) 44 | 45 | if __name__ == "__main__": 46 | log = Mylogger.getCommonLogger("app2.log",logging.INFO,1) 47 | log.info("=-------------------------") 48 | 49 | -------------------------------------------------------------------------------- /login_auto_deploy.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import asyncio 3 | import concurrent 4 | import random 5 | import threading 6 | import uuid 7 | from concurrent.futures import ThreadPoolExecutor 8 | 9 | import paramiko 10 | import os 11 | import logging 12 | import json 13 | import time 14 | import sys 15 | 16 | import requests 17 | from paramiko import SSHClient 18 | 19 | from cloudf import CFServer 20 | from dates import DateUtils 21 | from serv import Serv00 22 | from logger import Mylogger 23 | from apscheduler.schedulers.blocking import BlockingScheduler 24 | from sshs import SSHClientManagement 25 | 26 | """ 27 | 重启,保活,端口被禁,可自动申请端口,自动部署环境 28 | """ 29 | class AutoServ(object): 30 | 31 | sched = BlockingScheduler() 32 | def __init__(self, defaultConfig,account,tgConfig): 33 | self.currentTime = DateUtils.dateOperations(timedelta_kwargs={"hours": 8}) 34 | self.logger = Mylogger.getCommonLogger("app.log",logging.INFO,1) 35 | self.showNodeInfo = 0 36 | if 'uuid_ports' in account and account['uuid_ports']: 37 | defaultConfig['uuid_ports'] = account['uuid_ports'] 38 | 39 | if 'env_config' in account and account['env_config']: 40 | defaultConfig['env_config'] = account['env_config'] 41 | # 从环境变量中获取通道数 用户名 密码 42 | # 域名 app.js部署的根路径 如:/home/XXX[用户名]/domains/XXX[域名]/app/serv00-ws/ 43 | #服务器编号 如 https://panel6.serv00.com/ 中的6 44 | self.PANNELNUM = account["pannelnum"] 45 | # server_type:1 serv00 2 ct8 46 | self.SERVER_TYPE = 1 47 | if 'server_type' in account and account['server_type'] == 2: 48 | self.SERVER_TYPE = 2 49 | self.HOSTNAME = 'panel.ct8.pl' 50 | else: 51 | self.HOSTNAME = 'panel' + str(self.PANNELNUM) + '.serv00.com' 52 | #如果ip配置存在 优先显示 53 | #域名 54 | self.DOMAIN = account["domain"] 55 | self.nodeHost = self.DOMAIN 56 | if 'ip' in account: 57 | self.nodeHost = account['ip'] 58 | 59 | self.USERNAME = account["username"] 60 | #self.logger.info(self.DOMAIN +"::"+self.USERNAME +" run.................") 61 | #密码 62 | self.PASSWORD = account["password"] 63 | # 根路径 默认以app命名 64 | 65 | #删除pm2 66 | self.delePm2 = 1 67 | 68 | envConfig = defaultConfig["env_config"] 69 | #是否重置运行环境 70 | self.RESET = envConfig['reset'] 71 | #self.USE_PM2 = envConfig['usepm2'] 72 | #是否执行npm install命令 比较耗时建议不开启 手动执行 73 | self.OUTO_NPM_INSTALL = envConfig['outo_npm_install'] 74 | 75 | # 程序简单路径 默认从app文件后的路径 如'/serv00-vless/app' 76 | #self.APP_PATH = os.getenv('app_path') 77 | # 源代码路径 'git clone http://github.com/zjxde/serv00-vless' 78 | self.CODE_SOURCE_URL = envConfig['code_source_url'] 79 | self.KILL_PID_PATH = envConfig['kill_pid_path'] 80 | #tgConfig = userInfo['tg_config'] 81 | self.NODEJS_NAME = envConfig['nodejs_name'] 82 | self.DEL_SSL = 0 83 | if tgConfig: 84 | 85 | self.SEND_TG = tgConfig['send_tg'] 86 | self.USE_PM2 = tgConfig['usepm2'] 87 | self.NODE_NUM = tgConfig['node_num'] 88 | if self.SEND_TG: 89 | self.TG_BOT_TOKEN = tgConfig['tg_bot_token'] 90 | self.TG_CHAT_ID = tgConfig['tg_chat_id'] 91 | if 'timeout' in tgConfig: 92 | self.TIMEOUT = tgConfig['timeout'] 93 | else: 94 | self.TIMEOUT = 100 95 | if 'tryTimes' in tgConfig: 96 | self.tryTimes = tgConfig['tryTimes'] 97 | else: 98 | self.tryTimes = 3 99 | #兼容旧版本 user_info.json 配置优先级最高 100 | if 'reset' in tgConfig: 101 | self.RESET = tgConfig['reset'] 102 | if 'outo_npm_install' in tgConfig: 103 | self.OUTO_NPM_INSTALL = tgConfig['outo_npm_install'] 104 | if 'code_source_url' in tgConfig: 105 | self.CODE_SOURCE_URL = tgConfig['code_source_url'] 106 | if 'kill_pid_path' in tgConfig: 107 | self.KILL_PID_PATH = tgConfig['kill_pid_path'] 108 | if 'nodejs_name' in tgConfig: 109 | self.NODEJS_NAME = tgConfig['nodejs_name'] 110 | if 'del_ssl' in tgConfig: 111 | self.DEL_SSL = tgConfig['del_ssl'] 112 | if 'show_node_info' in tgConfig: 113 | self.showNodeInfo = tgConfig['show_node_info'] 114 | 115 | 116 | 117 | 118 | 119 | 120 | self.proxy = '' 121 | """ 122 | proxies = envConfig['proxies'] 123 | if proxies: 124 | self.PROXIES = proxies[random.randint(0,len(proxies)-1)] 125 | """ 126 | #self.configInfo = defaultConfig['uuid_ports'] 127 | 128 | #self.PANNELNUM = 6 129 | 130 | if 'basepath' in account: 131 | self.BASEPATH = account["basepath"] 132 | else: 133 | self.BASEPATH = "/home/"+self.USERNAME+"/domains/"+self.DOMAIN+"/app2" 134 | 135 | self.PIDPATH=self.CODE_SOURCE_URL.split('/')[-1] 136 | self.APP_PATH = '/'+self.PIDPATH+'/'+self.NODEJS_NAME 137 | self.FULLPATH = self.BASEPATH+self.APP_PATH 138 | #self.SEND_TG = 0 139 | #self.loop = asyncio.get_event_loop() 140 | 141 | #self.KILL_PID_PATH = envConfig['kill_pid_path'] 142 | self.pm2path = '/home/'+self.USERNAME+'/.npm-global/bin/pm2' 143 | 144 | if self.PANNELNUM is None: 145 | self.logger.error('please set the pannelnum') 146 | raise Exception('please set the pannelnum') 147 | 148 | 149 | 150 | 151 | self.logininfo = {} 152 | self.logininfo['username'] = self.USERNAME 153 | self.logininfo['password'] = self.PASSWORD 154 | #初始化默认端口 155 | if 'uuid_ports' in defaultConfig: 156 | self.portUidInfos = defaultConfig['uuid_ports'] 157 | else: 158 | defaultPortUid={} 159 | defaultPortUid['uuid']="" 160 | defaultPortUid['port']=0 161 | self.portUidInfos=[] 162 | for i in range(3): 163 | self.portUidInfos.append(defaultPortUid) 164 | 165 | 166 | #self.serv = Serv00(self.PANNELNUM, self.logininfo,self.HOSTNAME) 167 | self.hostfullName = self.USERNAME+"::server"+str(self.SERVER_TYPE)+"::" 168 | self.USE_CF = 0 169 | self.CF_IP = None 170 | if 'use_cf' in account and account['use_cf'] ==1: 171 | self.USE_CF = 1 172 | if 'cf_token' in tgConfig: 173 | self.CF_TOKEN = tgConfig['cf_token'] 174 | if 'cf_username' in tgConfig: 175 | self.CF_USERNAME = tgConfig['cf_username'] 176 | if 'cf_ip' in tgConfig: 177 | self.CF_IP = tgConfig['cf_ip'] 178 | if self.CF_TOKEN: 179 | self.logger.info(self.hostfullName+"set cf_token success") 180 | 181 | if 'reset' in account and account['reset'] == 1: 182 | self.RESET = 1 183 | self.OUTO_NPM_INSTALL = 1 184 | 185 | if 'del_ssl' in account and account['del_ssl'] == 1: 186 | self.DEL_SSL = 1 187 | #是否第一次布署 188 | self.IS_FIRST = 0 189 | if 'is_first' in account and account['is_first'] == 1: 190 | self.IS_FIRST = 1 191 | self.uuidPorts = {} 192 | self.alive = 0 193 | self.serv = None 194 | self.runningPorts = [] 195 | self.logger.info(self.hostfullName +" server init finish.................") 196 | self.initRes = 0 197 | self.cfserver = None 198 | self.CF_UPDATE = 0 199 | self.CF_UPDATE_PORTS=[] 200 | self.SSL_DOMAINS=[self.nodeHost] 201 | if 'ssl_domains' in account: 202 | self.SSL_DOMAINS = [] 203 | if "," not in account["ssl_domains"]: 204 | for i in range(min(self.NODE_NUM,3)): 205 | self.SSL_DOMAINS.append(account["ssl_domains"]) 206 | else: 207 | self.SSL_DOMAINS = account["ssl_domains"].split(",") 208 | else: 209 | self.SSL_DOMAINS = [] 210 | for i in range(min(self.NODE_NUM,3)): 211 | self.SSL_DOMAINS.append(self.nodeHost) 212 | if self.USERNAME is None: 213 | self.logger.error('please set the username') 214 | raise Exception('please set the username') 215 | 216 | 217 | if self.PASSWORD is None: 218 | self.logger.error('please set the password') 219 | raise Exception('please set the password') 220 | 221 | if self.BASEPATH is None: 222 | self.logger.error('please set the app dir basepath') 223 | raise Exception('please set the basepath') 224 | 225 | 226 | self.logger.info("PANNELNUM is "+str(self.PANNELNUM)) 227 | self.logger.info("USERNAME is "+self.USERNAME) 228 | self.logger.info(self.hostfullName+"BASEPATH is "+self.BASEPATH) 229 | 230 | try: 231 | self.ssh = self.getSshClient() 232 | self.serv = Serv00(self.PANNELNUM, self.logininfo,self.HOSTNAME) 233 | except Exception as e: 234 | self.logger.error(self.hostfullName+"ssh or serv timeout init error") 235 | 236 | #ftp = None 237 | # 获取远程ssh客户端链接 238 | def setSSHClient(self): 239 | self.ssh = self.getSshClient() 240 | self.serv = Serv00(self.PANNELNUM, self.logininfo,self.HOSTNAME) 241 | 242 | def getSshClient(self): 243 | # SSHclient 实例化 244 | ssh: SSHClient = paramiko.SSHClient() 245 | # 保存服务器密钥 246 | ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 247 | if self.RESET: 248 | self.TIMEOUT = 600 249 | # 输入服务器地址,账户名,密码 250 | ssh.connect( 251 | hostname=self.HOSTNAME, 252 | port=22, 253 | username=self.USERNAME, 254 | password=self.PASSWORD, 255 | timeout=self.TIMEOUT 256 | 257 | ) 258 | 259 | return ssh 260 | def getCurrentTime(self): 261 | return DateUtils.dateOperations(timedelta_kwargs={"hours": 8}) 262 | # 自动执行 启动节点 263 | def execute(self, ssh, sftp_client, myuuid, port,index): 264 | global ftp, data, file,ouuid 265 | if port is not None and port == 0: 266 | self.logger.info('not set port ,will genate random') 267 | #port = random.randint(1024, 65535) 268 | 269 | 270 | self.logger.info('auto get port from server') 271 | if not myuuid: 272 | logging.info("not set uuid ,will genate random") 273 | ouuid = str(uuid.uuid1()) 274 | else: 275 | ouuid = myuuid 276 | 277 | #logging.info("uuid:: " + ouuid+",port::"+str(port)) 278 | uuidinfo = ouuid.replace("-", "") 279 | #sftp_client = ssh.open_sftp() 280 | # 打开远程文件 281 | ftp = sftp_client.open(self.FULLPATH +'-template.js') 282 | # 读取文件内容 283 | data = ftp.read() 284 | #newcontent = str(data, 'UTF-8').replace('$$UUID$$', '\'' + uuidinfo + '\'').replace("$$PORT$$", str(port)) 285 | newcontent = str(data, 'UTF-8').replace('process.env.UUID', '\'' + uuidinfo + '\'').replace("process.env.PORT", str(port)) 286 | templateName = self.FULLPATH+"_"+ouuid+"_"+str(port)+".js" 287 | try: 288 | sftp_client.remove(templateName) 289 | self.logger.info(self.FULLPATH +"_"+str(port)+".js"+ ":::file exist") 290 | except Exception: 291 | self.logger.info(templateName + ":::file not exist") 292 | self.logger.info("The file remove finish.") 293 | file = sftp_client.file(templateName, "a", -1) 294 | file.write(newcontent) 295 | file.flush() 296 | 297 | msg = "vless://"+ouuid+"@"+self.nodeHost+":"+str(port)+"?encryption=none&security=none&type=ws&host="+self.nodeHost+"&path=%2F#"+self.USERNAME+"_"+str(port) 298 | #self.nodeHost = self.SSL_DOMAINS[index] 299 | #ssh.exec_command('~/.npm-global/bin/pm2 start ' + templateName + ' --name vless') 300 | self.startCmd(templateName,port,ssh) 301 | if self.USE_CF: 302 | self.CF_UPDATE = 1 303 | self.CF_UPDATE_PORTS.append(int(port)) 304 | #nodeHost = 305 | domain = self.SSL_DOMAINS[index] 306 | if self.CF_IP: 307 | domain = self.CF_IP 308 | msg = msg +"\n"+ "vless://"+ouuid+"@"+domain+":"+str(443)+"?encryption=none&security=tls&sni="+domain+"&type=ws&host="+domain+"&path=%2F#"+self.USERNAME+"_"+str(port)+"_443" 309 | #msg = "vless://"+ouuid+"@"+domain+":"+str(443)+"?encryption=none&security=none&type=ws&host="+self.nodeHost+"&path=%2F#"+self.USERNAME+"_"+str(port)+"_80" 310 | #异步发送节点链接到tg 311 | if self.showNodeInfo: 312 | self.logger.info("url is::"+msg) 313 | else: 314 | self.logger.info(self.hostfullName+str(port)) 315 | if self.SEND_TG: 316 | self.sendTgMsgSync(msg) 317 | #tasklist = [self.sendTelegramMessage(msg),self.sendTgMsgLog()] 318 | 319 | def sendTgMsgSync(self,msg): 320 | if self.showNodeInfo: 321 | self.logger.info(self.hostfullName+msg) 322 | else: 323 | self.logger.info(self.hostfullName+"send tg msg start...") 324 | with ThreadPoolExecutor(max_workers=5) as executor: 325 | # 使用executor提交任务 326 | executor.submit(self.sendTelegramMessage,msg) 327 | pass 328 | def startCmd(self,templateName,port,ssh): 329 | if self.USE_PM2: 330 | ssh.exec_command('/home/'+self.USERNAME+'/.npm-global/bin/pm2 start '+templateName+' --name vless') 331 | ssh.exec_command('/home/'+self.USERNAME+'/.npm-global/bin/pm2 monitor') 332 | else: 333 | ssh.exec_command('nohup node '+templateName+' > '+self.FULLPATH+'_'+str(port)+'.log 2>&1 &') 334 | def main(self): 335 | try: 336 | try: 337 | ssh = self.getSshClient() 338 | ftp = ssh.open_sftp() 339 | serv = self.serv 340 | self.killPid(ssh) 341 | self.delNodejsFile(ssh) 342 | if self.USE_CF: 343 | self.CF_UPDATE = 1 344 | except Exception as e: 345 | self.logger.error("connect is timeout or error") 346 | 347 | if self.RESET: 348 | self.resetEnv(ssh,self.OUTO_NPM_INSTALL) 349 | # 从serv00服务器删除原有端口,自动获取端口 350 | 351 | ports = serv.getloginPorts() 352 | #杀掉现有节点pid,重新申请端口,自动部署 353 | uuidPorts = self.uuidPorts 354 | if ports and len(ports) > 0: 355 | for port in ports: 356 | #cmd = 'kill -9 '+str(port) 357 | #logging.info("kill 端口::"+str(port)) 358 | #ssh.exec_command(cmd) 359 | serv.delport(port) 360 | if len(uuidPorts) > 0 : 361 | if port in uuidPorts: 362 | uuidPorts.pop(port) 363 | for i in range(0,min(self.NODE_NUM,3)): 364 | serv.addport(None) 365 | 366 | ports = serv.getports(); 367 | #首次部署帮开通权限,申请端口 368 | if self.IS_FIRST: 369 | serv.runMain([self.DOMAIN],ports[:min(self.NODE_NUM, 2)],self.DEL_SSL,1) 370 | 371 | i = 0 372 | for index,data in enumerate(self.portUidInfos): 373 | UUID = data['uuid'] 374 | port = data['port'] 375 | if(port == 0): 376 | port = ports[i] 377 | 378 | self.execute(ssh, ftp, UUID, port,index) 379 | if len(uuidPorts) > 0: 380 | if port not in uuidPorts: 381 | uuidPorts[port] = UUID 382 | i +=1 383 | self.logger.info("deploy node_"+str(i)+"_"+str(port)+" finish") 384 | if i>=self.NODE_NUM: 385 | break 386 | 387 | except Exception as e: 388 | self.logger.error(e) 389 | #raise Exception("main方法异常,请检查变量是否配置准确") 390 | finally: 391 | #if not self.alive: 392 | if ftp: 393 | ftp.close() 394 | del ftp 395 | if ssh: 396 | ssh.close() 397 | #自动安装部署node等运行环境 398 | def resetEnv(self,ssh,outoNpmInstall): 399 | pm2path = '/home/'+self.USERNAME+'/.npm-global/bin/pm2' 400 | try: 401 | pidfullpath = self.BASEPATH+"/"+self.PIDPATH 402 | #此方法耗时较长 取决你的网络环境,若等待时间超时,请手动r执行npm install 403 | if outoNpmInstall: 404 | cdcmd = "cd "+self.BASEPATH +"&& " 405 | rmcmd = " rm -rf "+self.BASEPATH 406 | self.executeCmd(ssh, rmcmd,5) 407 | self.logger.info(rmcmd + "::: finish") 408 | createcmd = " mkdir -p "+self.BASEPATH 409 | self.executeCmd(ssh, createcmd,5) 410 | self.logger.info(createcmd + "::: create finish") 411 | wget = self.CODE_SOURCE_URL +" "+pidfullpath 412 | timeout = 20 413 | lscmd = "ls "+pidfullpath+" |grep "+self.NODEJS_NAME 414 | #self.executeCmd(ssh, lscmd,5) 415 | stdin, stdout, stderr = self.ssh.exec_command(lscmd,get_pty=True) 416 | res = stdout.read().decode() 417 | files = res.split('\r\n') 418 | delayTime = 5 419 | if self.USE_PM2: 420 | lsPm2 = pm2path+' list' 421 | files = self.executeNewCmd(ssh, lsPm2,120)[0] 422 | self.logger.info(files) 423 | if files and 'No such' in files[0]: 424 | pm2="npm install -g pm2" 425 | files = self.executeNewCmd(ssh, pm2,120)[0] 426 | self.logger.info(files) 427 | 428 | while (files and 'No such' in files[0]) or timeout >=0: 429 | 430 | timeout = timeout-delayTime 431 | stdin, stdout, stderr = self.ssh.exec_command(lscmd,get_pty=True) 432 | res = stdout.read().decode() 433 | files = res.split('\r\n') 434 | if files and self.NODEJS_NAME in files[0]: 435 | self.logger.info(self.hostfullName+"main js ok break") 436 | break 437 | files = self.executeNewCmd(ssh, wget,120)[0] 438 | self.logger.info(files) 439 | self.logger.info(self.hostfullName+"main js ::"+files[0]) 440 | self.logger.info(wget+"::: try timeout::"+str(timeout)) 441 | time.sleep(delayTime) 442 | 443 | #npmInstall = "cd "+BASEPATH+PIDPATH+" && npm install" 444 | npmInstall = "unzip "+ pidfullpath+"/node_modules.zip -d "+pidfullpath 445 | self.executeCmd(ssh,npmInstall,60) 446 | self.logger.info(npmInstall + "::: finish") 447 | 448 | cpcmd = 'cp '+self.FULLPATH+'.js '+self.FULLPATH+'-template.js' 449 | self.executeCmd(ssh, cpcmd,5) 450 | self.logger.info(cpcmd + ":::finish") 451 | self.logger.info(self.hostfullName+"init env is ok....") 452 | self.setSSHClient() 453 | 454 | except Exception as e: 455 | self.logger.error(e) 456 | self.logger.error(self.hostfullName+":::env init error") 457 | #远程执行相关命令 458 | def executeCmd(self,ssh, cmd,waitTime): 459 | stdin, stdout, stderr = ssh.exec_command(cmd,timeout=waitTime,get_pty=True) 460 | try: 461 | self.logger.info(cmd +" execute start.....") 462 | while not stdout.channel.exit_status_ready(): 463 | result = stdout.readlines() 464 | self.logger.info(result) 465 | if stdout.channel.exit_status_ready(): 466 | res = stdout.readlines; 467 | self.logger.info(res) 468 | break 469 | self.logger.info(cmd +" execute finish.....") 470 | except Exception as e: 471 | self.logger.error(self.hostfullName+"execute cmd error::"+cmd) 472 | #执行命令返回结果 473 | def executeNewCmd(self,ssh,cmd,waitTime): 474 | stdin, stdout, stderr = ssh.exec_command(cmd,timeout=waitTime,get_pty=True) 475 | res = stdout.read().decode() 476 | files = res.split('\r\n') 477 | return files,stdin,stdout,stderr 478 | #杀死现有节点进程 479 | def killPid(self,ssh): 480 | #ssh = getSshClient() 481 | 482 | if self.USE_PM2: 483 | pidcmd = self.pm2path + ' delete all' 484 | self.executeNewCmd(ssh,pidcmd,5) 485 | self.logger.info(self.hostfullName+"pm2 kill all pid") 486 | else: 487 | try: 488 | if self.delePm2: 489 | pidcmd = self.pm2path + ' delete all' 490 | self.executeNewCmd(ssh,pidcmd,5) 491 | self.logger.info(self.hostfullName+"pm2 kill all pid") 492 | except Exception as e: 493 | self.logger.info(self.hostfullName+"pm2 env not found or time out") 494 | self.delePm2 = 0 495 | cmd = 'pgrep -f '+self.KILL_PID_PATH 496 | pids,stdin,stdout, stderr = self.executeNewCmd(ssh,cmd,5) 497 | #res = stdout.read().decode() 498 | #pids = res.split('\r\n') 499 | if pids and len(pids) > 0: 500 | for pid in pids: 501 | if pid : 502 | pidcmd = 'kill -9 '+pid 503 | self.executeNewCmd(ssh,pidcmd,5) 504 | self.logger.info(self.hostfullName+"kill pid::"+pid) 505 | # 发送节点到tg 506 | def sendTelegramMessage(self,message): 507 | url = f"https://api.telegram.org/bot{self.TG_BOT_TOKEN}/sendMessage" 508 | payload = { 509 | 'chat_id': self.TG_CHAT_ID, 510 | 'text': message, 511 | 'reply_markup': { 512 | 'inline_keyboard': [ 513 | [ 514 | { 515 | 'text': '问题反馈❓', 516 | 'url': 'https://t.me/yxjsjl' 517 | } 518 | ] 519 | ] 520 | } 521 | } 522 | headers = { 523 | 'Content-Type': 'application/json' 524 | } 525 | try: 526 | response = requests.session().post(url, json=payload, headers=headers) 527 | if response.status_code != 200: 528 | self.logger.info(f"发送消息到Telegram失败: {response.text}") 529 | except Exception as e: 530 | print(f"发送消息到Telegram时出错: {e}") 531 | 532 | # 只重启 不重新部署环境 533 | def restart(self): 534 | try: 535 | self.killPid(self.ssh) 536 | ports = self.serv.getloginPorts(); 537 | self.getNodejsFile(self.ssh) 538 | if ports and len(ports) > 0: 539 | for index,port in enumerate(ports): 540 | if port not in self.uuidPorts: 541 | self.logger.info(self.hostfullName+str(port)+" is not auto create ,continue") 542 | self.forceConfig() 543 | break 544 | ouuid = self.uuidPorts[str(port)] 545 | msg = "vless://"+ouuid+"@"+self.nodeHost+":"+str(port)+"?encryption=none&security=none&type=ws&host="+self.nodeHost+"&path=%2F#"+self.USERNAME+"_"+str(port) 546 | 547 | #ouuid = self.portUidInfos[index]['uuid'] 548 | 549 | if self.USE_CF: 550 | self.CF_UPDATE = 1 551 | self.CF_UPDATE_PORTS.append(int(port)) 552 | #nodeHost = 553 | domain = self.SSL_DOMAINS[index] 554 | if self.CF_IP: 555 | domain = self.CF_IP 556 | msg = msg +"\n"+ "vless://"+ouuid+"@"+domain+":"+str(443)+"?encryption=none&security=tls&sni="+domain+"&type=ws&host="+domain+"&path=%2F#"+self.USERNAME+"_"+str(port)+"_443" 557 | 558 | 559 | if self.showNodeInfo: 560 | self.logger.info("url is::"+msg) 561 | else: 562 | self.logger.info(self.hostfullName+str(port)) 563 | templateName = self.FULLPATH+"_"+ouuid+"_"+str(port)+".js" 564 | #ssh.exec_command('~/.npm-global/bin/pm2 start ' + templateName + ' --name vless') 565 | self.startCmd(templateName,port,self.ssh) 566 | if self.SEND_TG: 567 | #tasklist = [self.sendTelegramMessage(msg),self.sendTgMsgLog()] 568 | #self.loop.run_until_complete(asyncio.wait(tasklist)) 569 | self.sendTgMsgSync(msg) 570 | except Exception as e: 571 | self.logger.error(e) 572 | self.logger.error(self.hostfullName+" restart error") 573 | finally: 574 | #if not self.alive: 575 | if self.ssh is not None: 576 | self.ssh.close() 577 | #获取nodejs文件名字 578 | def getNodejsFile(self,ssh): 579 | pidfullpath = self.BASEPATH+"/"+self.PIDPATH 580 | cmd = "ls "+pidfullpath +" | grep 'index_.*.js'" 581 | filenames,stdin, stdout, stderr = self.executeNewCmd(ssh,cmd,10) 582 | #res = stdout.read().decode() 583 | #filenames = files.split('\r\n') 584 | if filenames and len(filenames) > 0: 585 | for filename in filenames: 586 | if filename: 587 | uuid = filename.split('_')[1] 588 | port = filename.split('_')[2].replace(".js","") 589 | self.uuidPorts[port] = uuid 590 | else: 591 | msg = self.hostfullName+"not find nodejs file,regenerate " 592 | self.logger.info(msg) 593 | if self.SEND_TG: 594 | self.sendTgMsgSync(msg) 595 | 596 | #self.forceConfig() 597 | if not self.USE_CF: 598 | self.IS_FIRST = 0 599 | self.RESET = 1 600 | self.OUTO_NPM_INSTALL = 1 601 | self.main() 602 | 603 | def delNodejsFile(self,ssh): 604 | pidfullpath = self.BASEPATH+"/"+self.PIDPATH 605 | cmd = "ls "+pidfullpath +" | grep 'index_.*.js'" 606 | filenames,stdin, stdout, stderr = self.executeNewCmd(ssh,cmd,10) 607 | #res = stdout.read().decode() 608 | #filenames = files.split('\r\n') 609 | if filenames and len(filenames) > 0: 610 | for filename in filenames: 611 | if filename: 612 | delcmd = "rm -rf "+pidfullpath+"/"+filename 613 | self.executeNewCmd(ssh,delcmd,10) 614 | #保活 615 | def keepAlive(self): 616 | #self.ssh = self.getSshClient() 617 | 618 | try: 619 | self.setSSHClient() 620 | self.getNodejsFile(self.ssh) 621 | ports = None 622 | if self.runningPorts: 623 | ports = self.runningPorts 624 | else: 625 | ports = self.serv.getloginPorts() 626 | ssh = self.ssh 627 | if ports and len(ports) > 0: 628 | if len(ports) >=min(len(ports),3): 629 | self.runningPorts = ports 630 | for index,port in enumerate(ports): 631 | if port not in self.uuidPorts: 632 | self.logger.info(str(port)+" is not auto create ,continue") 633 | self.forceConfig() 634 | if self.SEND_TG: 635 | msg = self.hostfullName+"重新创建部署节点ok,请重新到TG复制最新的节点信息" 636 | self.sendTgMsgSync(msg) 637 | break 638 | ouuid = self.uuidPorts[str(port)] 639 | cmd = "sockstat -l|grep ':"+str(port)+"'|awk '{print$3}'" 640 | stdin, stdout, stderr = ssh.exec_command(cmd,get_pty=True) 641 | res = stdout.read().decode() 642 | pids = res.split('\r\n') 643 | 644 | if pids and len(pids) > 0 and pids[0]: 645 | self.logger.info(self.getCurrentTime()+":"+self.hostfullName+str(port)+"::"+str(pids[0]) +" is running") 646 | continue 647 | #双重判断 通过进程判断 防止重复启动 648 | cmd = 'pgrep -f '+self.KILL_PID_PATH 649 | pids,stdin,stdout, stderr = self.executeNewCmd(ssh,cmd,5) 650 | if pids and len(pids)>0: 651 | pidlist = list(filter(bool, pids)) 652 | if len(pidlist) == len(self.uuidPorts): 653 | self.logger.info(f"{self.hostfullName} {pidlist} is running") 654 | continue 655 | self.logger.info(self.hostfullName+" nodes keepalive restart.....") 656 | msg = "vless://"+ouuid+"@"+self.nodeHost+":"+str(port)+"?encryption=none&security=none&type=ws&host="+self.nodeHost+"&path=%2F#"+self.USERNAME+"_"+str(port) 657 | self.nodeHost = self.SSL_DOMAINS[index] 658 | templateName = self.FULLPATH+"_"+ouuid+"_"+str(port)+".js" 659 | #ouuid = outoServ02.portUidInfos[index]['uuid'] 660 | ouuid = self.uuidPorts[port] 661 | if self.USE_CF: 662 | self.CF_UPDATE = 1 663 | self.CF_UPDATE_PORTS.append(int(port)) 664 | domain = self.SSL_DOMAINS[index] 665 | if self.CF_IP: 666 | domain = self.CF_IP 667 | msg = msg +"\n"+ "vless://"+ouuid+"@"+domain+":"+str(443)+"?encryption=none&security=tls&sni="+domain+"&type=ws&host="+domain+"&path=%2F#"+self.USERNAME+"_"+str(port)+"_443" 668 | if self.showNodeInfo: 669 | self.logger.info("url is::"+msg) 670 | else: 671 | self.logger.info(self.hostfullName+str(port)) 672 | self.startCmd(templateName,port,ssh) 673 | 674 | #time.sleep(waitTime) 675 | self.logger.info(self.hostfullName+" nodes keepalive") 676 | except Exception as e: 677 | 678 | self.logger.error(f"keepAlive error: {e}") 679 | self.logger.error(self.hostfullName+" keepAlive error") 680 | finally: 681 | if self.ssh is not None: 682 | self.ssh.close() 683 | 684 | #重启,保活,非首次部署不进行重置,申请证书操作 685 | def forceConfig(self): 686 | #使用cf 需要重新申请证书,重置部署 687 | if self.USE_CF: 688 | self.IS_FIRST = 0 689 | self.RESET = 1 690 | self.OUTO_NPM_INSTALL = 1 691 | else: 692 | self.RESET = 0 693 | self.IS_FIRST = 0 694 | self.main() 695 | 696 | @staticmethod 697 | def runAcount(defaultConfig,tgConfig,account,cmd): 698 | ssh = None 699 | outoServ = AutoServ(defaultConfig,account,tgConfig) 700 | try: 701 | #outoServ.setSSHClient() 702 | ssh = outoServ.ssh 703 | outoServ.logger.info(f"os cmd::{cmd}") 704 | if not cmd:# 如果github工作流使用命令 优先级最高 705 | cmd = account['cmd'] 706 | args = cmd.split() 707 | #不需要保活 708 | if '0' in args: 709 | args=[x for x in args if x!= '0'] 710 | logger = outoServ.logger; 711 | logger.info("cmd::"+cmd) 712 | if args and len(args) ==2: 713 | cmd = args[1] 714 | logger.info(cmd) 715 | if cmd == 'reset': 716 | outoServ.main() 717 | outoServ.initRes = 1 718 | elif cmd == 'restart': 719 | outoServ.restart() 720 | outoServ.initRes = 1 721 | elif cmd == 'keepalive': 722 | outoServ.alive = 1 723 | AutoServ.sched.add_job(outoServ.keepAlive,'interval', minutes=10) 724 | #AutoServ.sched.start() 725 | else: 726 | logger.error("请输入如下命令:reset、restart、keepalive") 727 | ssh.close() 728 | 729 | elif args and len(args) ==3: 730 | cmd2 = args[2] 731 | cmd1 = args[1] 732 | logger.info("cmd::"+cmd) 733 | try: 734 | waitTime = int(cmd2) 735 | #手动启动不活 736 | if cmd1 == 'reset': 737 | outoServ.alive = 1 738 | outoServ.main() 739 | #outoServ.keepAlive() 740 | elif cmd1=='restart': 741 | outoServ.alive = 1 742 | outoServ.restart() 743 | #AutoServ.sched.add_job(outoServ.keepAlive,'interval', minutes=waitTime) 744 | #AutoServ.sched.start() 745 | #outoServ.keepAlive(waitTime) 746 | elif cmd1=='keepalive': 747 | outoServ.alive = 1 748 | logger.info("输入命令为:keepalive") 749 | else: 750 | logger.error("请输入如下命令:reset、restart、keepalive") 751 | ssh.close() 752 | if outoServ.alive: 753 | logger.info(outoServ.hostfullName+" keepalive interval for::"+str(waitTime)) 754 | AutoServ.sched.add_job(outoServ.keepAlive,'interval', minutes=waitTime) 755 | outoServ.initRes = 1 756 | outoServ.logger.info(f"AutoServ.sched.state:{AutoServ.sched.state}") 757 | 758 | except Exception as e: 759 | print(e) 760 | logger.error(e) 761 | ssh.close() 762 | ssh=None 763 | return outoServ 764 | except Exception as e: 765 | outoServ.logger.error(e) 766 | return outoServ 767 | finally: 768 | if ssh: 769 | ssh.close() 770 | return outoServ 771 | #list去重有序 772 | def get_cf_ports(self): 773 | lst = self.CF_UPDATE_PORTS 774 | return [x for i, x in enumerate(lst) if x not in lst[:i]] 775 | def get_thread_id(self): 776 | return threading.current_thread().ident 777 | 778 | if __name__ == "__main__": 779 | cmd = os.getenv("ENV_CMD") 780 | manual = os.getenv("USER_INFO_MANUAL") 781 | with open('default_config.json', 'r') as f: 782 | defaultConfig = json.load(f) 783 | with open('user_info.json', 'r') as f: 784 | accounts = json.load(f) 785 | try: 786 | with open('env_config.json', 'r') as f: 787 | envConfig = json.load(f) 788 | defaultConfig = envConfig 789 | except Exception as e: 790 | print("使用默认环境配置") 791 | #手动启动 792 | accounts_manual = None 793 | try: 794 | with open('user_info_manual.json', 'r') as f: 795 | accounts_manual = json.load(f) 796 | except Exception as e: 797 | print("使用自动配置") 798 | 799 | 800 | #args = ['python','keepalive',60] 801 | # 如果命令 802 | args = sys.argv 803 | myAccounts = accounts['accounts'] 804 | if manual: 805 | myAccounts = accounts_manual['accounts'] 806 | tgConfig = accounts['tg_config'] 807 | #runservloop = asyncio.get_event_loop() 808 | #asyncio.run(runMain()) 809 | with ThreadPoolExecutor(max_workers=5) as executor: 810 | # 使用executor提交任务 811 | if myAccounts and len(myAccounts) > 0: 812 | future_results = [] 813 | for account in myAccounts: 814 | res = executor.submit(AutoServ.runAcount,defaultConfig,tgConfig,account,cmd) 815 | future_results.append(res) 816 | #break 817 | pass 818 | results=[] 819 | needSchedules = [] 820 | for future in concurrent.futures.as_completed(future_results): 821 | autoServ = future.result() 822 | autoServ: AutoServ = future.result() 823 | uid = autoServ.hostfullName+str(autoServ.initRes) 824 | autoServ.logger.info(f"Task result: {uid}") 825 | results.append(uid) 826 | needSchedules.append(autoServ.alive) 827 | #更新cf Origin Rules 828 | with ThreadPoolExecutor(max_workers=5) as cfExecutor: 829 | update_list = autoServ.get_cf_ports() 830 | autoServ.logger.info(f"{autoServ.hostfullName}::{update_list}") 831 | if autoServ.USE_CF and autoServ.CF_TOKEN and len(autoServ.CF_UPDATE_PORTS)>0: 832 | cfExecutor.submit(CFServer.run,autoServ.SSL_DOMAINS,autoServ.DOMAIN,update_list,autoServ.CF_USERNAME,autoServ.CF_TOKEN) 833 | #更新cf Origin Rules 834 | print(f"sched::{AutoServ.sched.state}") 835 | if 1 in needSchedules: 836 | AutoServ.sched.start() 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | 847 | 848 | 849 | 850 | 851 | 852 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.9.5 2 | APScheduler==3.10.4 3 | jsonpath==0.82.2 4 | lxml==5.2.2 5 | paramiko==3.4.0 6 | Requests==2.32.3 7 | -------------------------------------------------------------------------------- /serv.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import json 3 | import logging 4 | import re 5 | 6 | import requests 7 | from lxml import etree 8 | 9 | from logger import Mylogger 10 | import asyncio 11 | 12 | class Serv00(object): 13 | 14 | def __init__(self,pannelnum,logininfo,hostname): 15 | self.logger = Mylogger.getCommonLogger("Serv00.log",logging.INFO,1) 16 | #basepath = 'https://panel'+str(pannelnum)+'.serv00.com/' 17 | basepath = 'https://'+hostname+'/' 18 | self.loginReferer = basepath +'login/' 19 | self.portlistReferer = basepath 20 | self.delPortReferer = basepath+'port/' 21 | self.addPortReferer = basepath+'port/add' 22 | self.addwebsiteReferer = basepath+'www/add' 23 | self.headers = { 24 | 'Accept': '*/*', 25 | 'Accept-Language': 'en-US,en;q=0.8', 26 | 'Cache-Control': 'max-age=0', 27 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36', 28 | 'Connection': 'keep-alive', 29 | 'Referer': self.loginReferer 30 | } 31 | 32 | self.portToken = None 33 | self.websiteToken = None 34 | 35 | self.session = requests.session() 36 | self.url = self.loginReferer 37 | self.portUrl = self.delPortReferer 38 | self.addPortUrl = self.addPortReferer 39 | self.addWebsiteUrl = self.addwebsiteReferer 40 | self.getWebsitesUrl = basepath+"www/" 41 | self.getSSLWebsitesUrl = basepath+"ssl/www" 42 | self.addSertificateUrl = basepath+"ssl/www/sni/add/" 43 | self.runAppPermissionUrl = basepath+'permissions/binexec' 44 | proxy = '127.0.0.1:10809' 45 | self.proxies = { 46 | 'http': 'http://' + proxy, 47 | 'https': 'https://' + proxy, 48 | } 49 | self.logininfo = logininfo 50 | self.pannelnum = pannelnum 51 | self.islogin = self.login() 52 | 53 | def login(self): 54 | rep = self.session.get(self.loginReferer) 55 | html = etree.HTML(rep.text) 56 | token = html.xpath('//*[@id="centerlogin"]/div[1]/div[1]/form/input/@value')[0] 57 | self.logininfo['csrfmiddlewaretoken'] = token 58 | response = self.session.post(self.url,data=self.logininfo,headers = self.headers) 59 | print(response.status_code) 60 | return response.status_code == 200 61 | def getports(self): 62 | headers = self.headers 63 | headers['Referer'] = self.portlistReferer 64 | portrsp = self.session.get(self.portUrl,headers = headers) 65 | porthtml = portrsp.text 66 | html = etree.HTML(porthtml) 67 | portlist = html.xpath('//*[@id="port_list"]/tbody//td[2]/@data-order') 68 | 69 | return portlist 70 | #获取port相关token 71 | def getPortToken(self): 72 | token = self.portToken 73 | if token is None: 74 | rep = self.session.get(self.addPortUrl) 75 | html = etree.HTML(rep.text) 76 | token = html.xpath('//*[@id="content-wrapper"]/form/input/@value')[0] 77 | self.portToken = token 78 | return token 79 | #请求添加端口 80 | def addport(self,port): 81 | headers = self.headers 82 | 83 | headers['Referer'] = self.addPortReferer 84 | data = {} 85 | if port is not None: 86 | port = port 87 | data['id_port-placeholder-1'] = port 88 | else: 89 | port = 'random' 90 | data['csrfmiddlewaretoken'] = self.getPortToken() 91 | data['port'] = port 92 | data['port_type'] = 'tcp' 93 | data['description'] = '' 94 | resp = self.session.post(self.addPortUrl,data=data,headers = headers) 95 | #print(resp.text) 96 | if resp and resp.status_code == 200: 97 | return port 98 | else: 99 | self.logger.info(f"{port} add port erro :{resp.status_code}") 100 | return 0 101 | #请求删除端口 102 | def delport(self,port): 103 | headers = self.headers 104 | headers['Referer'] = self.delPortReferer 105 | data = {} 106 | data['csrfmiddlewaretoken'] = self.getPortToken() 107 | data['del_port'] = port 108 | data['del_port_type'] = 'tcp' 109 | resp = self.session.post(self.portUrl,data=data,headers = headers) 110 | return resp and resp.status_code == 200 111 | 112 | def getloginPorts(self): 113 | islogin = self.islogin 114 | if islogin: 115 | ports = self.getports() 116 | print(ports) 117 | return ports 118 | def getWebsites(self): 119 | resp = self.session.get(self.getWebsitesUrl) 120 | reshtml = resp.text 121 | html = etree.HTML(reshtml) 122 | 123 | webSites = html.xpath('//*[@id="www_domain_list"]//tr/td[2]/text()') 124 | res=[] 125 | for chu in webSites: 126 | ele = re.sub('\s', '', ''.join(chu)) 127 | if ele: 128 | res.append(ele) 129 | return res 130 | #添加网址 131 | def addWebsite(self,domain,port): 132 | headers = self.headers 133 | headers['Referer'] = self.addwebsiteReferer 134 | data = {} 135 | data['csrfmiddlewaretoken'] = self.getWebsiteToken() 136 | #//*[@id="id_domain"] 137 | data['domain'] = domain 138 | data['type'] = 'proxy' 139 | data['proxy_target'] = 'localhost' 140 | data['pointer_target'] = 'webmail' 141 | data['proxy_port'] = port 142 | data['environment'] = 'production' 143 | 144 | resp = self.session.post(self.addWebsiteUrl,data=data,headers = headers) 145 | #print(resp.text) 146 | if resp and resp.status_code == 200: 147 | self.logger.info(f"{domain} addWebsite success {resp.status_code}") 148 | return domain 149 | else: 150 | self.logger.info(f"{domain} addWebsite erro {resp.status_code}") 151 | return 0 152 | 153 | #获取port相关token 154 | def getWebsiteToken(self): 155 | token = self.websiteToken 156 | if token is None: 157 | rep = self.session.get(self.addWebsiteUrl) 158 | html = etree.HTML(rep.text) 159 | #//*[@id="www_add_form"]/input 160 | token = html.xpath('//*[@id="www_add_form"]/input/@value') 161 | self.websiteToken = token 162 | return token 163 | #获取ip列表 164 | def getSSLWebsites(self): 165 | headers = self.headers 166 | url = self.getSSLWebsitesUrl 167 | headers['Referer'] = self.getReferer(url) 168 | portrsp = self.session.get(url,headers = headers) 169 | porthtml = portrsp.text 170 | html = etree.HTML(porthtml) 171 | #//*[@id="DataTables_Table_0"]/tbody/tr[1]/td[2]/text() 172 | ips = html.xpath('//td[2]/text()') 173 | #//*[@id="DataTables_Table_0"]/tbody/tr[1]/td[3]/span 174 | nums = html.xpath('//td[3]/text()') 175 | return dict(zip(ips, nums)),ips,nums 176 | 177 | def getReferer(self, url): 178 | if url: 179 | paths = url.split("/") 180 | newurl = "/".join(paths[:-1])+"/" 181 | return newurl 182 | def getSertificateToken(self,ip): 183 | rep = self.session.get(self.addSertificateUrl+ip) 184 | html = etree.HTML(rep.text) 185 | token = html.xpath('//form/input/@value')[1] 186 | return token 187 | #自动添加证书 188 | def addSertificate(self,ip,domain): 189 | url = self.addSertificateUrl+ip 190 | headers = self.headers 191 | headers['Referer'] = self.getReferer(self.addSertificateUrl) 192 | data = {} 193 | data['csrfmiddlewaretoken'] = self.getSertificateToken(ip) 194 | #//*[@id="id_domain"] 195 | data['cert_type'] = 'le' 196 | data['ip'] = ip 197 | data['domain'] = domain 198 | data['cert_cert'] = '(二进制)' 199 | data['cert_key'] = '(二进制)' 200 | resp = self.session.post(url, data=data, headers=headers) 201 | #print(resp.text) 202 | if resp and resp.status_code == 200: 203 | self.logger.info(f"addSertificate success {resp.status_code}") 204 | return domain 205 | else: 206 | self.logger.info(f"{domain} addSertificate erro {resp.status_code}") 207 | return 0 208 | 209 | #自动添加证书 210 | def delSertificate(self,domain): 211 | url = self.getWebsitesUrl 212 | headers = self.headers 213 | headers['Referer'] = self.getReferer(self.getWebsitesUrl) 214 | data = {} 215 | data['csrfmiddlewaretoken'] = self.getWebsiteToken() 216 | #//*[@id="id_domain"] 217 | data['del_domain'] = domain 218 | data['del_files'] = '' 219 | data['del_dns'] = 'on' 220 | resp = self.session.post(url, data=data, headers=headers) 221 | #print(resp.text) 222 | if resp and resp.status_code == 200: 223 | self.logger.info(f"{domain} delSertificate success") 224 | return domain 225 | else: 226 | self.logger.info(f"{domain} delSertificate erro {resp.status_code}") 227 | return 0 228 | #查询是否开启app权限 229 | def runAppPermission(self): 230 | headers = self.headers 231 | url = self.runAppPermissionUrl 232 | headers['Referer'] = self.getReferer(url) 233 | portrsp = self.session.get(url,headers = headers) 234 | porthtml = portrsp.text 235 | html = etree.HTML(porthtml) 236 | #//*[@id="DataTables_Table_0"]/tbody/tr[1]/td[2]/text() 237 | res = html.xpath('//input/@placeholder')[0] 238 | return res 239 | def getToken(self,url): 240 | rep = self.session.get(url) 241 | html = etree.HTML(rep.text) 242 | token = html.xpath('//form/input/@value')[1] 243 | return token 244 | #开启运行app权限 245 | def enableAppPermission(self): 246 | enableRes = self.runAppPermission() 247 | if enableRes == 'Enabled': 248 | return 1 249 | else: 250 | headers = self.headers 251 | url = self.runAppPermissionUrl 252 | headers['Referer'] = url 253 | data = {} 254 | data['csrfmiddlewaretoken'] = self.getToken(url) 255 | data['action'] = 'on' 256 | resp = self.session.post(url, data=data, headers=headers) 257 | if resp and resp.status_code == 200: 258 | self.logger.info(f"enableAppPermission success {resp.status_code}") 259 | return 1 260 | else: 261 | self.logger.info(f"enableAppPermission erro {resp.status_code}") 262 | return 0 263 | """ 264 | 处理主方法 265 | domains:域名集合 266 | ports:端口 267 | isForce:是否强行删除 268 | cnameType:是否是cf cname配置方式 269 | """ 270 | def runMain(self,domains,ports,isForce,cnameType): 271 | #serv = Serv00(pannelnum,logininfo,hostname) 272 | serv = self 273 | enableRes = serv.enableAppPermission() 274 | serv.logger.info(f"{domains} enableAppPermission state::{enableRes}") 275 | length = len(domains) 276 | #服务器只提供2个id,只帮申请2个端口 277 | if ports and len(ports)>0: 278 | results = [] 279 | ip_nums,ips,nums = serv.getSSLWebsites() 280 | serv.logger.info(f"get ips : {ips} nums:{nums}") 281 | if ips and len(ips)>0: 282 | for index, ip in enumerate(ips): 283 | servDomain = domains[index] 284 | if ip_nums[ip] == '1': 285 | if not cnameType or isForce: 286 | serv.delSertificate(servDomain) 287 | else: 288 | serv.logger.info(f"{servDomain} ssl certificate exisit...") 289 | break 290 | else: 291 | if not cnameType: 292 | #绑定proxy端口 293 | #websites = serv.getWebsites() 294 | res = serv.addWebsite(servDomain, ports[index]) 295 | if res and res == servDomain: 296 | serv.logger.info(f"{servDomain} add ssl certificate start,please waiting...") 297 | #申请证书 298 | res = serv.addSertificate(ips[index], servDomain) 299 | if res and res == servDomain: 300 | results.append(res) 301 | serv.logger.info(f"{servDomain} add ssl certificate success") 302 | else: 303 | serv.logger.info(f"{servDomain} add ssl certificate start,please waiting...") 304 | res = serv.addSertificate(ips[index], servDomain) 305 | if res and res == servDomain: 306 | results.append(res) 307 | serv.logger.info(f"{servDomain} add ssl certificate success") 308 | if length<=1: 309 | break 310 | 311 | return results 312 | 313 | if __name__ == '__main__': 314 | with open('user_info3.json', 'r') as f: 315 | userInfo = json.load(f) 316 | accounts = userInfo['accounts'] 317 | if accounts and len(accounts) > 0: 318 | sSHClients = [] 319 | for account in accounts: 320 | pannelnum = account['pannelnum'] 321 | username = account['username'] 322 | password = account['password'] 323 | logininfo = { 324 | "username":username, 325 | "password":password 326 | } 327 | SERVER_TYPE = 1 328 | if 'server_type' in account and account['server_type'] == 2: 329 | SERVER_TYPE = 2 330 | HOSTNAME = 'panel.ct8.pl' 331 | else: 332 | HOSTNAME = 'panel' + str(pannelnum) + '.serv00.com' 333 | serv00 = Serv00(pannelnum,logininfo,HOSTNAME) 334 | serv00.delSertificate("junjie2.junx888.us.kg") 335 | #serv00.runMain(["junx123.cloudns.ch","vl.junx888.us.kg"],[8006,8007]) 336 | #serv00.runAppPermission() 337 | #serv00.enableAppPermission() 338 | #serv00.runMain(domain) 339 | #asyncio.run(Serv00.runMain(domain)) 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | -------------------------------------------------------------------------------- /serv00-autodeploy.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ssh.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import paramiko 3 | 4 | class SSHClient(object): 5 | 6 | err = "argument passwd or rsafile can not be None" 7 | 8 | def __init__(self, host, port, user, passwd=None, rsafile=None): 9 | self.h = host 10 | self.p = port 11 | self.u = user 12 | self.w = passwd 13 | self.rsa = rsafile 14 | 15 | def _connect(self): 16 | if self.w: 17 | return self.pwd_connect() 18 | elif self.rsa: 19 | return self.rsa_connect() 20 | else: 21 | raise ConnectionError(self.err) 22 | 23 | def pwd_connect(self): 24 | conn = paramiko.SSHClient() 25 | conn.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 26 | conn.connect(self.h, self.p, self.u, self.w) 27 | return conn 28 | 29 | def rsa_connect(self): 30 | pkey = paramiko.RSAKey.from_private_key_file(self.rsa) 31 | conn = paramiko.SSHClient() 32 | conn.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 33 | conn.connect(hostname=self.h, port=self.p, username=self.u, pkey=pkey) 34 | return conn 35 | 36 | def run_cmd(self, cmd,close=0): 37 | conn = self._connect() 38 | stdin, stdout, stderr = conn.exec_command(cmd) 39 | code = stdout.channel.recv_exit_status() 40 | stdout, stderr = stdout.read(), stderr.read() 41 | if close: 42 | conn.close() 43 | if not stderr: 44 | return code, stdout.decode() 45 | else: 46 | return code, stderr.decode() 47 | 48 | def close(self): 49 | conn = self._connect() 50 | if conn: 51 | conn.close() 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /sshs.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import json 3 | from concurrent.futures import ThreadPoolExecutor 4 | 5 | from ssh import SSHClient 6 | 7 | 8 | class SSHClientManagement(object): 9 | def __init__(self, ssh_objs, max_worker=50): 10 | self.objs = [o for o in ssh_objs] 11 | self.sshs = {} 12 | for sc in self.objs: 13 | username = sc.u 14 | self.sshs[username] = sc 15 | self.cmds = None 16 | self.max_worker = max_worker # 最大并发线程数 17 | 18 | self.success_hosts = [] # 存放成功机器数目 19 | self.failed_hosts = [] # 存放失败的机器IP 20 | self.mode = None 21 | self.func = None 22 | 23 | def setCmds(self,cmds): 24 | self.cmds = [c for c in cmds] 25 | def serial_exec(self, obj): 26 | """单台机器上串行执行命令,并返回结果至字典""" 27 | result = list() 28 | for c in self.cmds: 29 | r = obj.run_cmd(c) 30 | result.append([c, r]) 31 | return obj, result 32 | 33 | def concurrent_run(self): 34 | """并发执行""" 35 | future = ThreadPoolExecutor(self.max_worker) 36 | for obj in self.objs: 37 | try: 38 | future.submit(self.serial_exec, obj).add_done_callback(self.callback) 39 | except Exception as err: 40 | err = self.color_str(err, "red") 41 | print(err) 42 | future.shutdown(wait=True) 43 | 44 | def callback(self, future_obj): 45 | """回调函数,处理返回结果""" 46 | ssh_obj, rlist = future_obj.result() 47 | print(self.color_str("{} execute detail:".format(ssh_obj.h), "yellow")) 48 | is_success = True 49 | for item in rlist: 50 | cmd, [code, res] = item 51 | info = f"{cmd} | code => {code}\nResult:\n{res}" 52 | if code != 0: 53 | info = self.color_str(info, "red") 54 | is_success = False 55 | if ssh_obj.h not in self.failed_hosts: 56 | self.failed_hosts.append(ssh_obj.h) 57 | else: 58 | info = self.color_str(info, "green") 59 | print(info) 60 | if is_success: 61 | self.success_hosts.append(ssh_obj.h) 62 | if ssh_obj.h in self.failed_hosts: 63 | self.failed_hosts.remove(ssh_obj.h) 64 | 65 | def overview(self): 66 | """展示总的执行结果""" 67 | for i in self.success_hosts: 68 | print(self.color_str(i, "green")) 69 | print("-" * 30) 70 | for j in self.failed_hosts: 71 | print(self.color_str(j, "red")) 72 | info = "Success hosts {}; Failed hosts {}." 73 | s, f = len(self.success_hosts), len(self.failed_hosts) 74 | info = self.color_str(info.format(s, f), "yellow") 75 | print(info) 76 | 77 | @staticmethod 78 | def color_str(old, color=None): 79 | """给字符串添加颜色""" 80 | if color == "red": 81 | new = "\033[31;1m{}\033[0m".format(old) 82 | elif color == "yellow": 83 | new = "\033[33;1m{}\033[0m".format(old) 84 | elif color == "blue": 85 | new = "\033[34;1m{}\033[0m".format(old) 86 | elif color == "green": 87 | new = "\033[36;1m{}\033[0m".format(old) 88 | else: 89 | new = old 90 | return new 91 | def close(self): 92 | sshs = self.objs; 93 | if sshs and len(sshs)>0: 94 | for sshc in sshs: 95 | sshc.close() 96 | 97 | 98 | if __name__ == "__main__": 99 | 100 | with open('user_info.json', 'r') as f: 101 | userInfo = json.load(f) 102 | 103 | accounts = userInfo['account'] 104 | if accounts and len(accounts) > 0: 105 | cmds = ["df -h", "ls"] 106 | sSHClients = [] 107 | for account in accounts: 108 | hostname = account['domain'] 109 | port = 22 110 | username = account['username'] 111 | passwrod = account['password'] 112 | 113 | sshClient = SSHClient(hostname, port, username, passwrod) 114 | sSHClients.append(sshClient) 115 | sshs = SSHClientManagement(sSHClients) 116 | sshs.setCmds(cmds) 117 | sshs.concurrent_run() 118 | sshs.overview() 119 | sshs.close() -------------------------------------------------------------------------------- /user_info_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "tg_config": { 3 | "tg_bot_token": "【tg token】", 4 | "tg_chat_id": "【tg chat id】", 5 | "send_tg": 1, 6 | "node_num": 2, 7 | "usepm2": 0, 8 | "cf_token": "xxx", 9 | "cf_username": "xxx" 10 | }, 11 | "accounts": [ 12 | { 13 | "username": "【用户名】", 14 | "password": "【密码】", 15 | "domain": "【域名】", 16 | "pannelnum": 6, 17 | "cmd":"python reset 20", 18 | "server_type": 1, 19 | "ip": "xxxxx" 20 | }, 21 | { 22 | "username": "【用户名】", 23 | "password": "【密码】", 24 | "domain": "【域名】", 25 | "pannelnum": 6, 26 | "cmd":"python reset 20", 27 | "server_type": 1 28 | } 29 | ] 30 | } --------------------------------------------------------------------------------