ddns 4.1.0b4__tar.gz → 4.1.1b2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ddns might be problematic. Click here for more details.
- {ddns-4.1.0b4 → ddns-4.1.1b2}/PKG-INFO +1 -1
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/__init__.py +2 -2
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/__main__.py +7 -2
- ddns-4.1.1b2/ddns/config/__init__.py +171 -0
- ddns-4.1.1b2/ddns/config/cli.py +366 -0
- ddns-4.1.1b2/ddns/config/config.py +214 -0
- ddns-4.1.1b2/ddns/config/env.py +77 -0
- ddns-4.1.1b2/ddns/config/file.py +169 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/provider/cloudflare.py +25 -6
- ddns-4.1.1b2/ddns/scheduler/__init__.py +72 -0
- ddns-4.1.1b2/ddns/scheduler/_base.py +80 -0
- ddns-4.1.1b2/ddns/scheduler/cron.py +111 -0
- ddns-4.1.1b2/ddns/scheduler/launchd.py +130 -0
- ddns-4.1.1b2/ddns/scheduler/schtasks.py +120 -0
- ddns-4.1.1b2/ddns/scheduler/systemd.py +139 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns.egg-info/PKG-INFO +1 -1
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns.egg-info/SOURCES.txt +14 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/pyproject.toml +1 -2
- ddns-4.1.1b2/tests/test_config_cli_extra.py +132 -0
- ddns-4.1.1b2/tests/test_config_env_extra.py +145 -0
- ddns-4.1.1b2/tests/test_config_extra.py +244 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_config_schema_v4_1.py +78 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_provider_alidns.py +26 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_provider_aliesa.py +27 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_provider_cloudflare.py +125 -10
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_provider_dnscom.py +17 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_provider_dnspod.py +18 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_provider_huaweidns.py +24 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_provider_namesilo.py +23 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_provider_tencentcloud.py +21 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/LICENSE +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/README.md +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/__builtins__.pyi +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/cache.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/ip.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/provider/__init__.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/provider/_base.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/provider/_signature.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/provider/alidns.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/provider/aliesa.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/provider/callback.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/provider/debug.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/provider/dnscom.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/provider/dnspod.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/provider/dnspod_com.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/provider/edgeone.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/provider/he.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/provider/huaweidns.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/provider/namesilo.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/provider/noip.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/provider/tencentcloud.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/util/__init__.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/util/comment.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/util/fileio.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/util/http.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns/util/try_run.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns.egg-info/dependency_links.txt +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns.egg-info/entry_points.txt +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns.egg-info/requires.txt +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/ddns.egg-info/top_level.txt +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/setup.cfg +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_cache.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_config_cli.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_config_cli_task.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_config_config.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_config_env.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_config_file.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_config_file_remote.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_config_init.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_config_init_multi.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_ip.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_provider__signature.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_provider_base.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_provider_base_simple.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_provider_callback.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_provider_debug.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_provider_dnspod_com.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_provider_edgeone.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_provider_he.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_provider_noip.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_provider_proxy_list.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_scheduler_base.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_scheduler_cron.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_scheduler_init.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_scheduler_launchd.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_scheduler_schtasks.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_scheduler_systemd.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_util_comment.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_util_fileio.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_util_http.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_util_http_proxy_list.py +0 -0
- {ddns-4.1.0b4 → ddns-4.1.1b2}/tests/test_util_http_retry.py +0 -0
|
@@ -68,7 +68,7 @@ def update_ip(dns, cache, index_rule, domains, record_type, config):
|
|
|
68
68
|
update_success = True
|
|
69
69
|
else:
|
|
70
70
|
try:
|
|
71
|
-
result = dns.set_record(domain, address, record_type=record_type, ttl=config.ttl, line=config.line)
|
|
71
|
+
result = dns.set_record(domain, address, record_type=record_type, ttl=config.ttl, line=config.line, **config.extra)
|
|
72
72
|
if result:
|
|
73
73
|
logger.warning("set %s[IPv%s]: %s successfully.", domain, ip_type, address)
|
|
74
74
|
update_success = True
|
|
@@ -92,7 +92,12 @@ def run(config):
|
|
|
92
92
|
# dns provider class
|
|
93
93
|
provider_class = get_provider_class(config.dns)
|
|
94
94
|
dns = provider_class(
|
|
95
|
-
config.id,
|
|
95
|
+
config.id,
|
|
96
|
+
config.token,
|
|
97
|
+
endpoint=config.endpoint,
|
|
98
|
+
logger=logger,
|
|
99
|
+
proxy=config.proxy,
|
|
100
|
+
ssl=config.ssl,
|
|
96
101
|
)
|
|
97
102
|
cache = Cache.new(config.cache, config.md5(), logger)
|
|
98
103
|
return (
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# -*- coding:utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Configuration loader for DDNS.
|
|
4
|
+
|
|
5
|
+
This module handles loading configuration from command-line arguments,
|
|
6
|
+
JSON configuration files, and environment variables.
|
|
7
|
+
|
|
8
|
+
@author: NewFuture
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import logging
|
|
14
|
+
from .cli import load_config as load_cli_config
|
|
15
|
+
from .file import load_config as load_file_config, save_config
|
|
16
|
+
from .env import load_config as load_env_config
|
|
17
|
+
from .config import Config, split_array_string
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_config_paths(config_paths):
|
|
21
|
+
# type: (list[str] | None) -> list[str]
|
|
22
|
+
"""
|
|
23
|
+
获取配置文件路径列表,支持多个配置文件
|
|
24
|
+
"""
|
|
25
|
+
if not config_paths:
|
|
26
|
+
# Find config file in default locations
|
|
27
|
+
for p in [
|
|
28
|
+
"config.json",
|
|
29
|
+
os.path.expanduser("~/.ddns/config.json"),
|
|
30
|
+
os.path.expanduser("~/.ddns.json"),
|
|
31
|
+
"/etc/ddns/config.json",
|
|
32
|
+
"/etc/ddns.json",
|
|
33
|
+
]:
|
|
34
|
+
if os.path.exists(p):
|
|
35
|
+
return [p]
|
|
36
|
+
return []
|
|
37
|
+
|
|
38
|
+
# 验证所有路径都存在(跳过URL检查)
|
|
39
|
+
for config_path in config_paths:
|
|
40
|
+
# 跳过远程URL的存在性检查
|
|
41
|
+
if "://" not in config_path and not os.path.exists(config_path):
|
|
42
|
+
sys.stderr.write("Config file `%s` does not exist!\n" % config_path)
|
|
43
|
+
sys.stdout.write("Please check the path or use `--new-config` to create new one.\n")
|
|
44
|
+
sys.exit(1)
|
|
45
|
+
|
|
46
|
+
return config_paths
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _setup_logging(cli_config, env_config, all_json_configs):
|
|
50
|
+
# type: (dict, dict, list[dict]) -> logging.Logger
|
|
51
|
+
"""Setup logging configuration and return logger."""
|
|
52
|
+
# Use first config for global log settings (log config is inherited from global in v4.1 format)
|
|
53
|
+
json_config = all_json_configs[0] if all_json_configs else {}
|
|
54
|
+
global_conf = Config(cli_config=cli_config, json_config=json_config, env_config=env_config)
|
|
55
|
+
log_format = global_conf.log_format # type: str # type: ignore
|
|
56
|
+
if log_format:
|
|
57
|
+
# A custom log format is already set; no further action is required.
|
|
58
|
+
pass
|
|
59
|
+
elif global_conf.log_level < logging.INFO:
|
|
60
|
+
# Override log format in debug mode to include filename and line number for detailed debugging
|
|
61
|
+
log_format = "%(asctime)s %(levelname)s [%(name)s.%(funcName)s](%(filename)s:%(lineno)d): %(message)s"
|
|
62
|
+
elif global_conf.log_level > logging.INFO:
|
|
63
|
+
log_format = "%(asctime)s %(levelname)s: %(message)s"
|
|
64
|
+
else:
|
|
65
|
+
log_format = "%(asctime)s %(levelname)s [%(name)s]: %(message)s"
|
|
66
|
+
logging.basicConfig(
|
|
67
|
+
level=global_conf.log_level, format=log_format, datefmt=global_conf.log_datefmt, filename=global_conf.log_file
|
|
68
|
+
)
|
|
69
|
+
return logging.getLogger().getChild("config") # type: logging.Logger
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _load_json_configs(config_paths, proxy, ssl):
|
|
73
|
+
# type: (list[str], list[str], str) -> list[dict]
|
|
74
|
+
"""Load all JSON configurations from config paths."""
|
|
75
|
+
all_json_configs = []
|
|
76
|
+
for config_path in config_paths:
|
|
77
|
+
json_configs = load_file_config(config_path, proxy=proxy, ssl=ssl)
|
|
78
|
+
if isinstance(json_configs, list):
|
|
79
|
+
all_json_configs.extend(json_configs)
|
|
80
|
+
else:
|
|
81
|
+
all_json_configs.append(json_configs)
|
|
82
|
+
|
|
83
|
+
# 如果没有找到任何配置文件或JSON配置,创建一个空配置
|
|
84
|
+
return all_json_configs or [{}]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _validate_configs(configs, logger):
|
|
88
|
+
# type: (list[Config], logging.Logger) -> None
|
|
89
|
+
"""Validate that all configs have DNS providers."""
|
|
90
|
+
for i, conf in enumerate(configs):
|
|
91
|
+
if not conf.dns:
|
|
92
|
+
logger.critical(
|
|
93
|
+
"No DNS provider specified in config %d! Please set `dns` in config or use `--dns` CLI option.", i + 1
|
|
94
|
+
)
|
|
95
|
+
sys.exit(2)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def load_configs(description, version, date):
|
|
99
|
+
# type: (str, str, str) -> list[Config]
|
|
100
|
+
"""
|
|
101
|
+
Load and merge configuration from CLI, JSON, and environment variables.
|
|
102
|
+
Supports multiple config files and array config formats.
|
|
103
|
+
|
|
104
|
+
This function loads configuration from all three sources and returns a
|
|
105
|
+
list of Config objects that provides easy access to merged configuration values.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
description (str): The program description for the CLI parser.
|
|
109
|
+
version (str): The program version for the CLI parser.
|
|
110
|
+
date (str): The program release date for the CLI parser.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
list[Config]: A list of Config objects with merged configuration from all sources.
|
|
114
|
+
"""
|
|
115
|
+
doc = """
|
|
116
|
+
ddns [v{version}@{date}]
|
|
117
|
+
(i) homepage or docs [文档主页]: https://ddns.newfuture.cc/
|
|
118
|
+
(?) issues or bugs [问题和反馈]: https://github.com/NewFuture/DDNS/issues
|
|
119
|
+
Copyright (c) NewFuture (MIT License)
|
|
120
|
+
""".format(version=version, date=date)
|
|
121
|
+
# Load CLI configuration first
|
|
122
|
+
cli_config = load_cli_config(description, doc, version, date)
|
|
123
|
+
env_config = load_env_config()
|
|
124
|
+
|
|
125
|
+
# 获取配置文件路径列表
|
|
126
|
+
config_paths = split_array_string(cli_config.get("config", env_config.get("config", [])))
|
|
127
|
+
config_paths = _get_config_paths(config_paths)
|
|
128
|
+
|
|
129
|
+
# 提取代理和SSL设置用于HTTP请求
|
|
130
|
+
proxy_settings = split_array_string(cli_config.get("proxy", env_config.get("proxy", []))) # type: list[str]
|
|
131
|
+
ssl_settings = cli_config.get("ssl", env_config.get("ssl", "auto"))
|
|
132
|
+
|
|
133
|
+
# 加载所有配置文件
|
|
134
|
+
all_json_configs = _load_json_configs(config_paths, proxy_settings, ssl_settings)
|
|
135
|
+
|
|
136
|
+
# 为每个JSON配置创建Config对象
|
|
137
|
+
configs = [
|
|
138
|
+
Config(cli_config=cli_config, json_config=json_config, env_config=env_config)
|
|
139
|
+
for json_config in all_json_configs
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
# 设置日志
|
|
143
|
+
logger = _setup_logging(cli_config, env_config, all_json_configs)
|
|
144
|
+
|
|
145
|
+
# 处理无配置情况 - inline _handle_no_config logic
|
|
146
|
+
if len(cli_config) <= 1 and len(all_json_configs) == 1 and not all_json_configs[0] and not env_config:
|
|
147
|
+
# 没有配置时生成默认配置文件
|
|
148
|
+
logger.warning("[deprecated] auto gernerate config file will be deprecated in future versions.")
|
|
149
|
+
logger.warning("usage:\n `ddns --new-config` to generate a new config.\n `ddns -h` for help.")
|
|
150
|
+
default_config_path = config_paths[0] if config_paths else "config.json"
|
|
151
|
+
save_config(default_config_path, cli_config)
|
|
152
|
+
logger.info("No config file found, generated default config at `%s`.", default_config_path)
|
|
153
|
+
sys.exit(1)
|
|
154
|
+
|
|
155
|
+
# 记录配置加载情况
|
|
156
|
+
if config_paths:
|
|
157
|
+
logger.info("load config: %s", config_paths)
|
|
158
|
+
else:
|
|
159
|
+
logger.debug("No config file specified, using CLI and environment variables only.")
|
|
160
|
+
|
|
161
|
+
# 仅在没有配置文件且开启debug时自动设置debug provider
|
|
162
|
+
if not config_paths and cli_config.get("debug") and len(configs) == 1 and not configs[0].dns:
|
|
163
|
+
configs[0].dns = "debug"
|
|
164
|
+
|
|
165
|
+
# 验证每个配置都有DNS provider
|
|
166
|
+
_validate_configs(configs, logger)
|
|
167
|
+
|
|
168
|
+
return configs
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
__all__ = ["load_configs", "Config"]
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
# -*- coding:utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Configuration loader for DDNS command-line interface.
|
|
4
|
+
@author: NewFuture
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import platform
|
|
8
|
+
import sys
|
|
9
|
+
from argparse import SUPPRESS, Action, ArgumentParser, RawTextHelpFormatter
|
|
10
|
+
from logging import DEBUG, basicConfig, getLevelName
|
|
11
|
+
from os import path as os_path
|
|
12
|
+
|
|
13
|
+
from ..scheduler import get_scheduler
|
|
14
|
+
from .file import save_config
|
|
15
|
+
|
|
16
|
+
__all__ = ["load_config", "str_bool"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def str_bool(v):
|
|
20
|
+
# type: (str | bool | None | int | float | list) -> bool | str
|
|
21
|
+
"""
|
|
22
|
+
parse string to boolean
|
|
23
|
+
"""
|
|
24
|
+
if isinstance(v, bool):
|
|
25
|
+
return v
|
|
26
|
+
if v is None:
|
|
27
|
+
return False
|
|
28
|
+
if not isinstance(v, str) and not type(v).__name__ == "unicode":
|
|
29
|
+
return bool(v) # For non-string types, convert to string first
|
|
30
|
+
if v.lower() in ("yes", "true", "t", "y", "1"): # type: ignore[attribute-defined]
|
|
31
|
+
return True
|
|
32
|
+
elif v.lower() in ("no", "false", "f", "n", "0"): # type: ignore[attribute-defined]
|
|
33
|
+
return False
|
|
34
|
+
else:
|
|
35
|
+
return v # type: ignore[return-value]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def log_level(value):
|
|
39
|
+
"""
|
|
40
|
+
parse string to log level
|
|
41
|
+
or getattr(logging, value.upper())
|
|
42
|
+
"""
|
|
43
|
+
return getLevelName(value if isinstance(value, int) else value.upper())
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _get_system_info_str():
|
|
47
|
+
system = platform.system()
|
|
48
|
+
release = platform.release()
|
|
49
|
+
machine = platform.machine()
|
|
50
|
+
arch = platform.architecture()
|
|
51
|
+
return "{}-{} {} {}".format(system, release, machine, arch)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_python_info_str():
|
|
55
|
+
version = platform.python_version()
|
|
56
|
+
branch, py_build_date = platform.python_build()
|
|
57
|
+
return "Python-{} {} ({})".format(version, branch, py_build_date)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ExtendAction(Action):
|
|
61
|
+
"""兼容 Python <3.8 的 extend action"""
|
|
62
|
+
|
|
63
|
+
def __call__(self, parser, namespace, values, option_string=None):
|
|
64
|
+
items = getattr(namespace, self.dest, None)
|
|
65
|
+
if items is None:
|
|
66
|
+
items = []
|
|
67
|
+
# values 可能是单个值或列表
|
|
68
|
+
if isinstance(values, list):
|
|
69
|
+
items.extend(values)
|
|
70
|
+
else:
|
|
71
|
+
items.append(values)
|
|
72
|
+
setattr(namespace, self.dest, items)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class NewConfigAction(Action):
|
|
76
|
+
"""生成配置文件并退出程序"""
|
|
77
|
+
|
|
78
|
+
def __call__(self, parser, namespace, values, option_string=None):
|
|
79
|
+
# 获取配置文件路径
|
|
80
|
+
if values and values != "true":
|
|
81
|
+
config_path = str(values) # type: str
|
|
82
|
+
else:
|
|
83
|
+
config_path = getattr(namespace, "config", None) or "config.json" # type: str
|
|
84
|
+
config_path = config_path[0] if isinstance(config_path, list) else config_path
|
|
85
|
+
if os_path.exists(config_path):
|
|
86
|
+
sys.stderr.write("The default %s already exists!\n" % config_path)
|
|
87
|
+
sys.stdout.write("Please use `--new-config=%s` to specify a new config file.\n" % config_path)
|
|
88
|
+
sys.exit(1)
|
|
89
|
+
# 获取当前已解析的参数
|
|
90
|
+
current_config = {k: v for k, v in vars(namespace).items() if v is not None}
|
|
91
|
+
# 保存配置文件
|
|
92
|
+
save_config(config_path, current_config)
|
|
93
|
+
sys.stdout.write("%s is generated.\n" % config_path)
|
|
94
|
+
sys.exit(0)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _add_ddns_args(arg): # type: (ArgumentParser) -> None
|
|
98
|
+
"""Add common DDNS arguments to a parser"""
|
|
99
|
+
log_levels = [
|
|
100
|
+
"CRITICAL", # 50
|
|
101
|
+
"ERROR", # 40
|
|
102
|
+
"WARNING", # 30
|
|
103
|
+
"INFO", # 20
|
|
104
|
+
"DEBUG", # 10
|
|
105
|
+
"NOTSET", # 0
|
|
106
|
+
]
|
|
107
|
+
arg.add_argument(
|
|
108
|
+
"-c",
|
|
109
|
+
"--config",
|
|
110
|
+
nargs="*",
|
|
111
|
+
action=ExtendAction,
|
|
112
|
+
metavar="FILE",
|
|
113
|
+
help="load config file [配置文件路径, 可多次指定]",
|
|
114
|
+
)
|
|
115
|
+
arg.add_argument("--debug", action="store_true", help="debug mode [开启调试模式]")
|
|
116
|
+
|
|
117
|
+
# DDNS Configuration group
|
|
118
|
+
ddns = arg.add_argument_group("DDNS Configuration [DDNS配置参数]")
|
|
119
|
+
ddns.add_argument(
|
|
120
|
+
"--dns",
|
|
121
|
+
help="DNS provider [DNS服务提供商]",
|
|
122
|
+
choices=[
|
|
123
|
+
"51dns",
|
|
124
|
+
"alidns",
|
|
125
|
+
"aliesa",
|
|
126
|
+
"callback",
|
|
127
|
+
"cloudflare",
|
|
128
|
+
"debug",
|
|
129
|
+
"dnscom",
|
|
130
|
+
"dnspod_com",
|
|
131
|
+
"dnspod",
|
|
132
|
+
"edgeone",
|
|
133
|
+
"he",
|
|
134
|
+
"huaweidns",
|
|
135
|
+
"namesilo",
|
|
136
|
+
"noip",
|
|
137
|
+
"tencentcloud",
|
|
138
|
+
],
|
|
139
|
+
)
|
|
140
|
+
ddns.add_argument("--id", help="API ID or email [对应账号ID或邮箱]")
|
|
141
|
+
ddns.add_argument("--token", help="API token or key [授权凭证或密钥]")
|
|
142
|
+
ddns.add_argument("--endpoint", help="API endpoint URL [API端点URL]")
|
|
143
|
+
ddns.add_argument(
|
|
144
|
+
"--index4", nargs="*", action=ExtendAction, metavar="RULE", help="IPv4 rules [获取IPv4方式, 多次可配置多规则]"
|
|
145
|
+
)
|
|
146
|
+
ddns.add_argument(
|
|
147
|
+
"--index6", nargs="*", action=ExtendAction, metavar="RULE", help="IPv6 rules [获取IPv6方式, 多次配置多规则]"
|
|
148
|
+
)
|
|
149
|
+
ddns.add_argument(
|
|
150
|
+
"--ipv4", nargs="*", action=ExtendAction, metavar="DOMAIN", help="IPv4 domains [IPv4域名列表, 可配多个域名]"
|
|
151
|
+
)
|
|
152
|
+
ddns.add_argument(
|
|
153
|
+
"--ipv6", nargs="*", action=ExtendAction, metavar="DOMAIN", help="IPv6 domains [IPv6域名列表, 可配多个域名]"
|
|
154
|
+
)
|
|
155
|
+
ddns.add_argument("--ttl", type=int, help="DNS TTL(s) [设置域名解析过期时间]")
|
|
156
|
+
ddns.add_argument("--line", help="DNS line/route [DNS线路设置]")
|
|
157
|
+
|
|
158
|
+
# Advanced Options group
|
|
159
|
+
advanced = arg.add_argument_group("Advanced Options [高级参数]")
|
|
160
|
+
advanced.add_argument("--proxy", nargs="*", action=ExtendAction, help="HTTP proxy [设置http代理,可配多个代理连接]")
|
|
161
|
+
advanced.add_argument(
|
|
162
|
+
"--cache", type=str_bool, nargs="?", const=True, help="set cache [启用缓存开关,或传入保存路径]"
|
|
163
|
+
)
|
|
164
|
+
advanced.add_argument(
|
|
165
|
+
"--no-cache", dest="cache", action="store_const", const=False, help="disable cache [关闭缓存等效 --cache=false]"
|
|
166
|
+
)
|
|
167
|
+
advanced.add_argument(
|
|
168
|
+
"--ssl",
|
|
169
|
+
type=str_bool,
|
|
170
|
+
nargs="?",
|
|
171
|
+
const=True,
|
|
172
|
+
help="SSL certificate verification [SSL证书验证方式]: "
|
|
173
|
+
"true(强制验证), false(禁用验证), auto(自动降级), /path/to/cert.pem(自定义证书)",
|
|
174
|
+
)
|
|
175
|
+
advanced.add_argument(
|
|
176
|
+
"--no-ssl",
|
|
177
|
+
dest="ssl",
|
|
178
|
+
action="store_const",
|
|
179
|
+
const=False,
|
|
180
|
+
help="disable SSL verify [禁用验证, 等效 --ssl=false]",
|
|
181
|
+
)
|
|
182
|
+
advanced.add_argument("--log_file", metavar="FILE", help="log file [日志文件,默认标准输出]")
|
|
183
|
+
advanced.add_argument("--log.file", "--log-file", dest="log_file", help=SUPPRESS) # 隐藏参数
|
|
184
|
+
advanced.add_argument("--log_level", type=log_level, metavar="|".join(log_levels), help=None)
|
|
185
|
+
advanced.add_argument("--log.level", "--log-level", dest="log_level", type=log_level, help=SUPPRESS) # 隐藏参数
|
|
186
|
+
advanced.add_argument("--log_format", metavar="FORMAT", help="set log format [日志格式]")
|
|
187
|
+
advanced.add_argument("--log.format", "--log-format", dest="log_format", help=SUPPRESS) # 隐藏参数
|
|
188
|
+
advanced.add_argument("--log_datefmt", metavar="FORMAT", help="set log date format [日志时间格式]")
|
|
189
|
+
advanced.add_argument("--log.datefmt", "--log-datefmt", dest="log_datefmt", help=SUPPRESS) # 隐藏参数
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _add_task_subcommand_if_needed(parser): # type: (ArgumentParser) -> None
|
|
193
|
+
"""
|
|
194
|
+
Conditionally add task subcommand to avoid Python 2 'too few arguments' error.
|
|
195
|
+
|
|
196
|
+
Python 2's argparse requires subcommand when subparsers are defined, but Python 3 doesn't.
|
|
197
|
+
We only add subparsers when the first argument is likely a subcommand (doesn't start with '-').
|
|
198
|
+
"""
|
|
199
|
+
# Python2 Only add subparsers when first argument is a subcommand (not an option)
|
|
200
|
+
if len(sys.argv) <= 1 or (sys.argv[1].startswith("-") and sys.argv[1] != "--help"):
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
# Add subparsers for subcommands
|
|
204
|
+
subparsers = parser.add_subparsers(dest="command", help="subcommands [子命令]")
|
|
205
|
+
|
|
206
|
+
# Create task subcommand parser
|
|
207
|
+
task = subparsers.add_parser("task", help="Manage scheduled tasks [管理定时任务]")
|
|
208
|
+
task.set_defaults(func=_handle_task_command)
|
|
209
|
+
_add_ddns_args(task)
|
|
210
|
+
|
|
211
|
+
# Add task-specific arguments
|
|
212
|
+
task.add_argument(
|
|
213
|
+
"-i",
|
|
214
|
+
"--install",
|
|
215
|
+
nargs="?",
|
|
216
|
+
type=int,
|
|
217
|
+
const=5,
|
|
218
|
+
metavar="MINs",
|
|
219
|
+
help="Install task with <mins> [安装定时任务,默认5分钟]",
|
|
220
|
+
)
|
|
221
|
+
task.add_argument("--uninstall", action="store_true", help="Uninstall scheduled task [卸载定时任务]")
|
|
222
|
+
task.add_argument("--status", action="store_true", help="Show task status [显示定时任务状态]")
|
|
223
|
+
task.add_argument("--enable", action="store_true", help="Enable scheduled task [启用定时任务]")
|
|
224
|
+
task.add_argument("--disable", action="store_true", help="Disable scheduled task [禁用定时任务]")
|
|
225
|
+
task.add_argument(
|
|
226
|
+
"--scheduler",
|
|
227
|
+
choices=["auto", "systemd", "cron", "launchd", "schtasks"],
|
|
228
|
+
default="auto",
|
|
229
|
+
help="Specify scheduler type [指定定时任务方式]",
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def load_config(description, doc, version, date):
|
|
234
|
+
# type: (str, str, str, str) -> dict
|
|
235
|
+
"""
|
|
236
|
+
解析命令行参数并返回配置字典。
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
description (str): 程序描述
|
|
240
|
+
doc (str): 程序文档
|
|
241
|
+
version (str): 程序版本
|
|
242
|
+
date (str): 构建日期
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
dict: 配置字典
|
|
246
|
+
"""
|
|
247
|
+
parser = ArgumentParser(description=description, epilog=doc, formatter_class=RawTextHelpFormatter)
|
|
248
|
+
sysinfo = _get_system_info_str()
|
|
249
|
+
pyinfo = _get_python_info_str()
|
|
250
|
+
compiled = getattr(sys.modules["__main__"], "__compiled__", "")
|
|
251
|
+
version_str = "v{} ({})\n{}\n{}\n{}".format(version, date, pyinfo, sysinfo, compiled)
|
|
252
|
+
|
|
253
|
+
_add_ddns_args(parser) # Add common DDNS arguments to main parser
|
|
254
|
+
# Default behavior (no subcommand) - add all the regular DDNS options
|
|
255
|
+
parser.add_argument("-v", "--version", action="version", version=version_str)
|
|
256
|
+
parser.add_argument(
|
|
257
|
+
"--new-config", metavar="FILE", action=NewConfigAction, nargs="?", help="generate new config [生成配置文件]"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Python 2/3 compatibility: conditionally add subparsers to avoid 'too few arguments' error
|
|
261
|
+
# Subparsers are only needed when user provides a subcommand (non-option argument)
|
|
262
|
+
_add_task_subcommand_if_needed(parser)
|
|
263
|
+
|
|
264
|
+
args, unknown = parser.parse_known_args()
|
|
265
|
+
|
|
266
|
+
# Parse unknown arguments that follow --extra.xxx format
|
|
267
|
+
extra_args = {} # type: dict
|
|
268
|
+
i = 0
|
|
269
|
+
while i < len(unknown):
|
|
270
|
+
arg = unknown[i]
|
|
271
|
+
if arg.startswith("--extra."):
|
|
272
|
+
key = "extra_" + arg[8:] # Remove "--extra." and add "extra_" prefix
|
|
273
|
+
# Check if there's a value for this argument
|
|
274
|
+
if i + 1 < len(unknown) and not unknown[i + 1].startswith("--"):
|
|
275
|
+
extra_args[key] = unknown[i + 1]
|
|
276
|
+
i += 2
|
|
277
|
+
else:
|
|
278
|
+
# No value provided, set to True (flag)
|
|
279
|
+
extra_args[key] = True # type: ignore[assignment]
|
|
280
|
+
i += 1
|
|
281
|
+
else:
|
|
282
|
+
# Unknown argument that doesn't match our pattern
|
|
283
|
+
sys.stderr.write("Warning: Unknown argument: {}\n".format(arg))
|
|
284
|
+
i += 1
|
|
285
|
+
|
|
286
|
+
# Merge extra_args into args namespace
|
|
287
|
+
for k, v in extra_args.items():
|
|
288
|
+
setattr(args, k, v)
|
|
289
|
+
|
|
290
|
+
# Handle task subcommand and exit early if present
|
|
291
|
+
if hasattr(args, "func"):
|
|
292
|
+
args.func(vars(args))
|
|
293
|
+
sys.exit(0)
|
|
294
|
+
|
|
295
|
+
is_debug = getattr(args, "debug", False)
|
|
296
|
+
if is_debug:
|
|
297
|
+
# 如果启用调试模式,则强制设置日志级别为 DEBUG
|
|
298
|
+
args.log_level = log_level("DEBUG")
|
|
299
|
+
if args.cache is None:
|
|
300
|
+
args.cache = False # 禁用缓存
|
|
301
|
+
|
|
302
|
+
# 将 Namespace 对象转换为字典并直接返回
|
|
303
|
+
config = vars(args)
|
|
304
|
+
return {k: v for k, v in config.items() if v is not None} # 过滤掉 None 值的配置项
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _handle_task_command(args): # type: (dict) -> None
|
|
308
|
+
"""Handle task subcommand"""
|
|
309
|
+
basicConfig(level=args["debug"] and DEBUG or args.get("log_level", "INFO"))
|
|
310
|
+
|
|
311
|
+
# Use specified scheduler or auto-detect
|
|
312
|
+
scheduler_type = args.get("scheduler", "auto")
|
|
313
|
+
scheduler = get_scheduler(scheduler_type)
|
|
314
|
+
|
|
315
|
+
interval = args.get("install", 5) or 5
|
|
316
|
+
excluded_keys = ("status", "install", "uninstall", "enable", "disable", "command", "scheduler", "func")
|
|
317
|
+
ddns_args = {k: v for k, v in args.items() if k not in excluded_keys and v is not None}
|
|
318
|
+
|
|
319
|
+
# Execute operations
|
|
320
|
+
for op in ["install", "uninstall", "enable", "disable"]:
|
|
321
|
+
if not args.get(op):
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
# Check if task is installed for enable/disable
|
|
325
|
+
if op in ["enable", "disable"] and not scheduler.is_installed():
|
|
326
|
+
print("DDNS task is not installed" + (" Please install it first." if op == "enable" else "."))
|
|
327
|
+
sys.exit(1)
|
|
328
|
+
|
|
329
|
+
# Execute operation
|
|
330
|
+
print("{} DDNS scheduled task...".format(op.title()))
|
|
331
|
+
func = getattr(scheduler, op)
|
|
332
|
+
result = func(interval, ddns_args) if op == "install" else func()
|
|
333
|
+
|
|
334
|
+
if result:
|
|
335
|
+
past_tense = {
|
|
336
|
+
"install": "installed",
|
|
337
|
+
"uninstall": "uninstalled",
|
|
338
|
+
"enable": "enabled",
|
|
339
|
+
"disable": "disabled",
|
|
340
|
+
}[op]
|
|
341
|
+
suffix = " with {} minute interval".format(interval) if op == "install" else ""
|
|
342
|
+
print("DDNS task {} successfully{}".format(past_tense, suffix))
|
|
343
|
+
else:
|
|
344
|
+
print("Failed to {} DDNS task".format(op))
|
|
345
|
+
sys.exit(1)
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
# Show status or auto-install
|
|
349
|
+
status = scheduler.get_status()
|
|
350
|
+
|
|
351
|
+
if args.get("status") or status["installed"]:
|
|
352
|
+
print("DDNS Task Status:")
|
|
353
|
+
print(" Installed: {}".format("Yes" if status["installed"] else "No"))
|
|
354
|
+
print(" Scheduler: {}".format(status["scheduler"]))
|
|
355
|
+
if status["installed"]:
|
|
356
|
+
print(" Enabled: {}".format(status.get("enabled", "unknown")))
|
|
357
|
+
print(" Interval: {} minutes".format(status.get("interval", "unknown")))
|
|
358
|
+
print(" Command: {}".format(status.get("command", "unknown")))
|
|
359
|
+
print(" Description: {}".format(status.get("description", "")))
|
|
360
|
+
else:
|
|
361
|
+
print("DDNS task is not installed. Installing with default settings...")
|
|
362
|
+
if scheduler.install(interval, ddns_args):
|
|
363
|
+
print("DDNS task installed successfully with {} minute interval".format(interval))
|
|
364
|
+
else:
|
|
365
|
+
print("Failed to install DDNS task")
|
|
366
|
+
sys.exit(1)
|