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。
> 代理模式,支持自动创建域名记录。
-
[](https://pypi.org/project/ddns/)
[](https://github.com/NewFuture/DDNS/actions/workflows/build.yml)
[](https://github.com/NewFuture/DDNS/releases/latest)
@@ -18,7 +16,7 @@
- [x] 多系统兼容 
- [x] python3 支持 (2.x支持python2和python3)
- [x] PIP 安装 
- - [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