From da19b8c0c431cd66385ac8067243ae7d337011ba Mon Sep 17 00:00:00 2001 From: New Future Date: Mon, 2 Jun 2025 16:16:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=85=8D=E7=BD=AE=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=A8=A1=E5=BC=8F=E8=87=B3v4.0=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E9=85=8D=E7=BD=AE,(#465)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Break changes * 移除调试模式, * 添加日志级别和输出文件 --- .gitignore | 1 - .vscode/extensions.json | 8 +++ README.md | 28 +++++++--- run.py | 30 +++++------ schema/v2.8.json | 7 +-- schema.json => schema/v4.0.json | 95 ++++++++++++++++++++++++++------- util/config.py | 48 ++++++++++++----- 7 files changed, 158 insertions(+), 59 deletions(-) create mode 100644 .vscode/extensions.json rename schema.json => schema/v4.0.json (58%) diff --git a/.gitignore b/.gitignore index 396abe99a..d04382847 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ config.*.json -.vscode *.temp # Byte-compiled / optimized / DLL files diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..cc7c43b54 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "ms-python.flake8", + "ms-python.autopep8", + "ms-python.python", + "github.vscode-github-actions", + ] +} \ No newline at end of file diff --git a/README.md b/README.md index bc9c4ddad..35a3dbf92 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ # [DDNS](https://github.com/NewFuture/DDNS) [](https://ddns.newfuture.cc) - > 自动更新 DNS 解析 到本机 IP 地址,支持 ipv4 和 ipv6 以 本地(内网)IP 和 公网 IP。 > 代理模式,支持自动创建域名记录。 - [![PyPI](https://img.shields.io/pypi/v/ddns.svg?label=DDNS&style=social)](https://pypi.org/project/ddns/) [![Build Status](https://github.com/NewFuture/DDNS/actions/workflows/build.yml/badge.svg?event=push)](https://github.com/NewFuture/DDNS/actions/workflows/build.yml) [![Publish Status](https://github.com/NewFuture/DDNS/actions/workflows/publish.yml/badge.svg)](https://github.com/NewFuture/DDNS/releases/latest) @@ -18,7 +16,7 @@ - [x] 多系统兼容 ![cross platform](https://img.shields.io/badge/platform-windows_%7C%20linux_%7C%20osx-success.svg?style=social) - [x] python3 支持 ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/ddns.svg?style=social)(2.x支持python2和python3) - [x] PIP 安装 ![PyPI - Wheel](https://img.shields.io/pypi/wheel/ddns.svg?style=social) - - [x] Docker 支持(@NN708) + - [x] Docker 支持(@NN708) - 域名支持: - [x] 多个域名支持 - [x] 多级域名解析 @@ -52,17 +50,25 @@ 根据需要选择一种方式: `二进制`版,`pip`版,`源码`运行,或者`Docker` - #### pip 安装(需要 pip 或 easy_install) + 1. 安装 ddns: `pip install ddns` 或 `easy_install ddns` 2. 运行: `ddns` + - #### 二进制版(单文件,无需 python) + - Windows [ddns.exe](https://github.com/NewFuture/DDNS/releases/latest) - Linux (仅 Ubuntu 测试) [ddns](https://github.com/NewFuture/DDNS/releases/latest) - Mac OSX [ddns-osx](https://github.com/NewFuture/DDNS/releases/latest) + - #### 源码运行(无任何依赖, 需 python 环境) + 1. clone 或者[下载此仓库](https://github.com/NewFuture/DDNS/archive/master.zip)并解压 2. 运行./run.py (widnows 双击`run.bat`或者运行`python run.py`) + - #### Docker(需要安装 Docker) + - 使用环境变量: + ``` docker run -d \ -e DDNS_DNS=dnspod \ @@ -73,7 +79,9 @@ --network host \ newfuture/ddns ``` + - 使用配置文件(docker 工作目录`/ddds/`,默认配置位置`/ddns/config.json`): + ``` docker run -d \ -v /local/config/path/:/ddns/ \ @@ -102,7 +110,7 @@ 1. 命令行参数 `ddns --key=value` (`ddns -h` 查看详情),优先级最高 2. JSON配置文件(值为null认为是有效值,会覆盖环境变量的设置,如果没有对应的key则会尝试试用环境变量) -3. 环境变量DDNS_前缀加上key 全大写或者全小写 (`${ddns_key}` 或 `${DDNS_KEY}`) +3. 环境变量DDNS_前缀加上key 全大写或者全小写,点转下划线 (`${ddns_id}` 或 `${DDNS_ID}`,`${DDNS_LOG_LEVEL}`)
@@ -132,8 +140,9 @@ python run.py -c /path/to/config.json | index6 | string\|int\|array | No | `"default"` | ipv6 获取方式 | 可设置`网卡`,`内网`,`公网`,`正则`等方式 | | ttl | number | No | `null` | DNS 解析 TTL 时间 | 不设置采用 DNS 默认策略 | | proxy | string | No | 无 | http 代理`;`分割 | 多代理逐个尝试直到成功,`DIRECT`为直连 | -| debug | bool | No | `false` | 是否开启调试 | 运行异常时,打开调试输出,方便诊断错误 | +| ~~debug~~ | bool | No | `false` | 是否开启调试 | v4 中弃用,请改用log.level=DEBUG | | cache | string\|bool | No | `true` | 是否缓存记录 | 正常情况打开避免频繁更新,默认位置为临时目录下`ddns.cache`,
也可以指定一个具体文件实现自定义文件缓存位置 | +| log | {"level":string,"file":string} | No | `null` | 日志配置(可选) | 日志配置,日志级别和路径(默认命令行),
例如: `{ "level": "DEBUG", "file": "/path/to/logfile.log" }` | #### index4 和 index6 参数说明 @@ -167,7 +176,7 @@ python run.py -c /path/to/config.json ```json { - "$schema": "https://ddns.newfuture.cc/schema/v2.8.json", + "$schema": "https://ddns.newfuture.cc/schema/v4.0.json", "id": "12345", "token": "mytokenkey", "dns": "dnspod 或 dnspod_com 或 alidns 或 dnscom 或 cloudflare 或 he 或 huaweidns 或 callback", @@ -177,7 +186,10 @@ python run.py -c /path/to/config.json "index6": "public", "ttl": 600, "proxy": "127.0.0.1:1080;DIRECT", - "debug": false + "log": { + "level": "DEBUG", + "file": "dns.log" + }, } ``` @@ -200,12 +212,14 @@ python run.py -c /path/to/config.json - 使用init.d和crontab: `sudo ./task.sh` - 使用systemd: + ```bash 安装: sudo ./systemd.sh install 卸载: sudo ./systemd.sh uninstall ``` + 该脚本安装的文件符合 [Filesystem Hierarchy Standard (FHS)](https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard): 可执行文件所在目录为 `/usr/share/DDNS` 配置文件所在目录为 `/etc/DDNS` diff --git a/run.py b/run.py index 768175cd7..94bda2c40 100755 --- a/run.py +++ b/run.py @@ -11,11 +11,9 @@ # nuitka-project-else: # nuitka-project: --product-version=0.0.0 -from __future__ import print_function -from time import ctime, asctime from os import path, environ, name as os_name from tempfile import gettempdir -from logging import DEBUG, basicConfig, info, warning, error, debug +from logging import basicConfig, info, warning, error, debug from subprocess import check_output import sys @@ -82,8 +80,7 @@ def change_dns_record(dns, proxy_list, **kw): else: dns.Config.PROXY = proxy record_type, domain = kw['record_type'], kw['domain'] - print('\n%s %s(%s) ==> %s [via %s]' % - (asctime(), domain, record_type, kw['ip'], proxy)) + info("%s(%s) ==> %s [via %s]", domain, record_type, kw['ip'], proxy) try: return dns.update_record(domain, kw['ip'], record_type=record_type) except Exception as e: @@ -108,7 +105,7 @@ def update_ip(ip_type, cache, dns, proxy_list): error('Fail to get %s address!', ipname) return False elif cache and (address == cache[ipname]): - print('.', end=" ") # 缓存命中 + info('%s address not changed, using cache.', ipname) return True record_type = (ip_type == '4') and 'A' or 'AAAA' update_fail = False # https://github.com/NewFuture/DDNS/issues/16 @@ -132,16 +129,17 @@ def main(): dns.Config.ID = get_config('id') dns.Config.TOKEN = get_config('token') dns.Config.TTL = get_config('ttl') - if get_config('debug'): - ip.DEBUG = get_config('debug') - basicConfig( - level=DEBUG, - format='%(asctime)s <%(module)s.%(funcName)s> %(lineno)d@%(pathname)s \n[%(levelname)s] %(message)s') - print("DDNS[", __version__, "] run:", os_name, sys.platform) - if get_config("config"): - print("Configuration was loaded from <==", - path.abspath(get_config("config"))) - print("=" * 25, ctime(), "=" * 25, sep=' ') + + basicConfig( + level=get_config('log.level'), + format='%(asctime)s [%(levelname)s] %(message)s', + datefmt='%m-%d %H:%M:%S', + filename=get_config('log.file'), + ) + + info("DDNS[ %s ] run: %s %s", __version__, os_name, sys.platform) + if get_config("config"): + info("loaded Config from: %s", path.abspath(get_config('config'))) proxy = get_config('proxy') or 'DIRECT' proxy_list = proxy if isinstance( diff --git a/schema/v2.8.json b/schema/v2.8.json index 9830cc365..11b6db977 100644 --- a/schema/v2.8.json +++ b/schema/v2.8.json @@ -176,13 +176,14 @@ "debug": { "$id": "/properties/debug", "type": "boolean", - "title": "Enable Debug Mode", - "description": "是否启用调试模式显示更多信息", + "title": "Enable Debug Mode (deprecated, use logger instead)", + "description": "是否启用调试模式显示更多信息(已废弃,请使用 logger 字段)", "default": false, "examples": [ false, true - ] + ], + "deprecated": true }, "cache": { "$id": "/properties/cache", diff --git a/schema.json b/schema/v4.0.json similarity index 58% rename from schema.json rename to schema/v4.0.json index 9fb37fa81..808e4e74c 100644 --- a/schema.json +++ b/schema/v4.0.json @@ -1,21 +1,26 @@ { "$schema": "http://json-schema.org/draft-07/schema", - "$id": "https://ddns.newfuture.cc/schema.json", - "description": "[Deprecated] 已弃用,请使用 https://ddns.newfuture.cc/schema/v2.json", + "$id": "https://ddns.newfuture.cc/schema/v4.0.json", + "description": "DNS 配置文件 https://github.com/NewFuture/DDNS", "type": "object", "properties": { "$schema": { "type": "string", - "title": "please use https://ddns.newfuture.cc/schema/v2.json", - "description": "请更换为 https://ddns.newfuture.cc/schema/v2.json", + "title": "please use https://ddns.newfuture.cc/schema/v2.8.json", + "description": "请更换为 https://ddns.newfuture.cc/schema/v2.8.json", + "default": "https://ddns.newfuture.cc/schema/v2.8.json", "enum": [ - "https://ddns.newfuture.cc/schema/v2.json", - "http://ddns.newfuture.cc/schema/v2.json" + "https://ddns.newfuture.cc/schema/v2.8.json", + "http://ddns.newfuture.cc/schema/v2.8.json", + "./schema/v2.8.json" ] }, "id": { "$id": "/properties/id", - "type": "string", + "type": [ + "string", + "null" + ], "title": "ID or Email", "description": "DNS服务API认证的ID或者邮箱" }, @@ -29,7 +34,7 @@ "$id": "/properties/dns", "type": "string", "title": "DNS Provider", - "description": "dns服务商:阿里为alidns,DNS.COM为dnscom,DNSPOD国际版为(dnspod_com),cloudflare,HE.net为he", + "description": "dns服务商:阿里为alidns,DNS.COM为dnscom,DNSPOD国际版为(dnspod_com),cloudflare,HE.net为he,华为DNS为huaweidns,自定义回调为callback", "default": "dnspod", "examples": [ "dnspod", @@ -42,7 +47,9 @@ "cloudflare", "dnspod_com", "dnscom", - "he" + "he", + "huaweidns", + "callback" ] }, "ipv4": { @@ -55,7 +62,7 @@ "$id": "/properties/ipv4/items", "title": "ipv4 domain for DDNS", "type": "string", - "pattern": "^([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,18}$", + "pattern": "^(?:\\*\\.)?(?:[a-zA-Z0-9](?:[a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,18}$", "examples": [ "newfuture.cc", "ipv4.example.newfuture.cc" @@ -72,7 +79,7 @@ "$id": "/properties/ipv6/items", "title": "The ipv6 domain for DDNS", "type": "string", - "pattern": "^([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,6}$", + "pattern": "^(?:\\*\\.)?(?:[a-zA-Z0-9](?:[a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,18}$", "examples": [ "newfuture.cc", "ipv6.example.newfuture.cc" @@ -84,8 +91,19 @@ "type": [ "string", "integer", - "boolean" + "boolean", + "array" ], + "items": { + "type": [ + "string", + "integer" + ], + "minimum": 0 + }, + "uniqueItems": true, + "minItems": 1, + "minimum": 0, "title": "IPv4 address Setting", "description": "本机 IPv4 获取方式设置", "default": "default", @@ -103,8 +121,19 @@ "type": [ "string", "integer", - "boolean" + "boolean", + "array" ], + "items": { + "type": [ + "string", + "integer" + ], + "minimum": 0 + }, + "uniqueItems": true, + "minItems": 1, + "minimum": 0, "title": "IPv6 address Setting", "description": "本机 IPv6 获取方式设置", "default": "default", @@ -144,16 +173,42 @@ "127.0.0.1:1080;DIRECT" ] }, - "debug": { - "$id": "/properties/debug", - "type": "boolean", - "title": "Enable Debug Mode", - "description": "是否启用调试模式显示更多信息", - "default": false, + "cache": { + "$id": "/properties/cache", + "type": [ + "string", + "boolean" + ], + "title": "Enable Cache", + "description": "是否启用缓存记录以避免频繁更新", + "default": true, "examples": [ + true, false, - true + "/path/to/cache/ddns.cache" ] + }, + "log": { + "$id": "/properties/log", + "type": "object", + "title": "Log Config", + "description": "日志配置,支持自定义日志级别和输出位置。可通过命令行 --log.level, --log.file 或环境变量 DDNS_LOG_LEVEL, DDNS_LOG_FILE 设置。", + "properties": { + "level": { + "type": "string", + "title": "Log Level", + "description": "日志级别,如 DEBUG、INFO、WARNING、ERROR、CRITICAL", + "default": "INFO", + "enum": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + }, + "file": { + "type": ["string", "null"], + "title": "Log Output File", + "description": "日志输出文件路径,留空或为null时输出到控制台" + } + }, + "required": [], + "additionalProperties": false } }, "required": [ diff --git a/util/config.py b/util/config.py index 7f7fe9680..2647cf70d 100644 --- a/util/config.py +++ b/util/config.py @@ -2,8 +2,9 @@ # -*- coding:utf-8 -*- from argparse import ArgumentParser, ArgumentTypeError, Namespace, RawTextHelpFormatter # noqa: F401 from json import load as loadjson, dump as dumpjson -from logging import error from os import stat, environ +from logging import error, getLevelName + from time import time import sys @@ -11,6 +12,8 @@ __cli_args = {} # type: Namespace __config = {} # type: dict +log_levels = ['CRITICAL', 'FATAL', 'ERROR', + 'WARN', 'WARNING', 'INFO', 'DEBUG', 'NOTSET'] def str2bool(v): @@ -24,7 +27,14 @@ def str2bool(v): elif v.lower() in ('no', 'false', 'f', 'n', '0'): return False else: - raise ArgumentTypeError('Boolean value expected.') + return v + + +def log_level(value): + """ + parse string to log level + """ + return getLevelName(value.upper()) def init_config(description, doc, version): @@ -52,10 +62,12 @@ def init_config(description, doc, version): parser.add_argument('--ttl', type=int, help="ttl for DNS [DNS 解析 TTL 时间]") parser.add_argument('--proxy', nargs="*", help="https proxy [设置http 代理,多代理逐个尝试直到成功]") - parser.add_argument('--debug', type=str2bool, nargs='?', - const=True, help="debug mode [是否开启调试,默认否]", ) parser.add_argument('--cache', type=str2bool, nargs='?', - const=True, help="enable cache [是否缓存记录,默认是]") + const=True, help="cache flag [启用缓存,可配配置路径或开关]") + parser.add_argument('--log.file', metavar="LOG_FILE", + help="log file [日志文件,默认标准输出]") + parser.add_argument('--log.level', type=log_level, + metavar="|".join(log_levels)) __cli_args = parser.parse_args() is_configfile_optional = get_config("token") or get_config("id") @@ -74,6 +86,13 @@ def __load_config(path="config.json", skip_auto_generation=False): with open(path) as configfile: __config = loadjson(configfile) __config["config_modified_time"] = stat(path).st_mtime + if 'log' in __config: + if 'level' in __config['log'] and __config['log']['level'] is not None: + __config['log.level'] = log_level(__config['log']['level']) + if 'file' in __config['log']: + __config['log.file'] = __config['log']['file'] + elif 'log.level' in __config: + __config['log.level'] = log_level(__config['log.level']) except IOError: if skip_auto_generation: __config["config_modified_time"] = time() @@ -81,7 +100,7 @@ def __load_config(path="config.json", skip_auto_generation=False): error(' Config file `%s` does not exist!' % path) with open(path, 'w') as configfile: configure = { - "$schema": "https://ddns.newfuture.cc/schema/v2.8.json", + "$schema": "https://ddns.newfuture.cc/schema/v4.0.json", "id": "YOUR ID or EMAIL for DNS Provider", "token": "YOUR TOKEN or KEY for DNS Provider", "dns": "dnspod", @@ -97,14 +116,17 @@ def __load_config(path="config.json", skip_auto_generation=False): "index6": "default", "ttl": None, "proxy": None, - "debug": False, + "log": { + "level": "INFO", + "file": None + } } dumpjson(configure, configfile, indent=2, sort_keys=True) sys.stdout.write( "New template configure file `%s` is generated.\n" % path) sys.exit(1) - except Exception: - sys.exit('fail to load config from file: %s' % path) + except Exception as e: + sys.exit('fail to load config from file: %s\n%s' % (path, e)) def get_config(key, default=None): @@ -118,9 +140,11 @@ def get_config(key, default=None): return getattr(__cli_args, key) if key in __config: return __config.get(key) - env_name = 'DDNS_'+key.upper() # type:str - if env_name in environ: # 大写环境变量 + env_name = 'DDNS_' + key.replace('.', '_') # type:str + if env_name in environ: # 环境变量 return environ.get(env_name) - if env_name.lower() in environ: # 小写环境变量 + elif env_name.upper() in environ: # 大写环境变量 + return environ.get(env_name.upper()) + elif env_name.lower() in environ: # 小写环境变量 return environ.get(env_name.lower()) return default