xflow.framework 0.1.0__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.
- xflow_framework-0.1.0/LICENSE +24 -0
- xflow_framework-0.1.0/MANIFEST.in +5 -0
- xflow_framework-0.1.0/PKG-INFO +43 -0
- xflow_framework-0.1.0/README.md +3 -0
- xflow_framework-0.1.0/pyproject.toml +3 -0
- xflow_framework-0.1.0/requirements.txt +6 -0
- xflow_framework-0.1.0/setup.cfg +4 -0
- xflow_framework-0.1.0/setup.py +67 -0
- xflow_framework-0.1.0/xflow/framework/__init__.py +0 -0
- xflow_framework-0.1.0/xflow/framework/common.py +14 -0
- xflow_framework-0.1.0/xflow/framework/container.py +292 -0
- xflow_framework-0.1.0/xflow/framework/env.py +72 -0
- xflow_framework-0.1.0/xflow/framework/errors.py +33 -0
- xflow_framework-0.1.0/xflow/framework/main.py +112 -0
- xflow_framework-0.1.0/xflow/framework/node.py +392 -0
- xflow_framework-0.1.0/xflow/framework/pipeline.py +189 -0
- xflow_framework-0.1.0/xflow/framework/ssh.py +372 -0
- xflow_framework-0.1.0/xflow/framework/statics/initdir/.gitignore +215 -0
- xflow_framework-0.1.0/xflow/framework/statics/initdir/README.md +0 -0
- xflow_framework-0.1.0/xflow/framework/statics/initdir/env.yml +41 -0
- xflow_framework-0.1.0/xflow/framework/statics/initdir/pipelines/__init__.py +0 -0
- xflow_framework-0.1.0/xflow/framework/statics/initdir/pipelines/example.py +60 -0
- xflow_framework-0.1.0/xflow/framework/statics/initdir/pipelines/template.py +32 -0
- xflow_framework-0.1.0/xflow/framework/statics/initdir/requirements.txt +1 -0
- xflow_framework-0.1.0/xflow/framework/statics/initdir/scripts/.gitkeep +0 -0
- xflow_framework-0.1.0/xflow/framework/statics/initdir/workdir/.gitkeep +0 -0
- xflow_framework-0.1.0/xflow/framework/utils.py +59 -0
- xflow_framework-0.1.0/xflow/framework/version.py +4 -0
- xflow_framework-0.1.0/xflow.framework.egg-info/PKG-INFO +43 -0
- xflow_framework-0.1.0/xflow.framework.egg-info/SOURCES.txt +32 -0
- xflow_framework-0.1.0/xflow.framework.egg-info/dependency_links.txt +1 -0
- xflow_framework-0.1.0/xflow.framework.egg-info/entry_points.txt +2 -0
- xflow_framework-0.1.0/xflow.framework.egg-info/requires.txt +8 -0
- xflow_framework-0.1.0/xflow.framework.egg-info/top_level.txt +3 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, zhaowcheng
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
16
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
17
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
18
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
19
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
20
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
21
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
23
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
24
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xflow.framework
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 可以用 Python 写 Pipeline 的 CICD 框架。
|
|
5
|
+
Home-page: https://github.com/zhaowcheng/xflow.framework
|
|
6
|
+
Author: zhaowcheng
|
|
7
|
+
Author-email: zhaowcheng@163.com
|
|
8
|
+
Project-URL: Homepage, https://github.com/zhaowcheng/xflow.framework
|
|
9
|
+
Project-URL: Issues, https://github.com/zhaowcheng/xflow.framework/issues
|
|
10
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Requires-Python: >=3.6
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: ruamel.yaml; python_version >= "3.10"
|
|
24
|
+
Requires-Dist: paramiko; python_version >= "3.10"
|
|
25
|
+
Requires-Dist: decorator; python_version >= "3.10"
|
|
26
|
+
Requires-Dist: filelock; python_version >= "3.10"
|
|
27
|
+
Requires-Dist: docker; python_version >= "3.10"
|
|
28
|
+
Requires-Dist: typed-settings[click,option-groups,pydantic]; python_version >= "3.10"
|
|
29
|
+
Dynamic: author
|
|
30
|
+
Dynamic: author-email
|
|
31
|
+
Dynamic: classifier
|
|
32
|
+
Dynamic: description
|
|
33
|
+
Dynamic: description-content-type
|
|
34
|
+
Dynamic: home-page
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
Dynamic: project-url
|
|
37
|
+
Dynamic: requires-dist
|
|
38
|
+
Dynamic: requires-python
|
|
39
|
+
Dynamic: summary
|
|
40
|
+
|
|
41
|
+
# xflow.framework
|
|
42
|
+
|
|
43
|
+
可以用 Python 写 Pipeline 的 CICD 框架。
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026, zhaowcheng <zhaowcheng@163.com>
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from setuptools import setup, find_namespace_packages
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
BASEDIR = os.path.abspath(os.path.dirname(__file__))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def find_version():
|
|
12
|
+
verfile = os.path.join(BASEDIR, 'xflow', 'framework', 'version.py')
|
|
13
|
+
with open(verfile, 'r', encoding='utf8') as f:
|
|
14
|
+
for line in f.readlines():
|
|
15
|
+
if line.startswith('__version__'):
|
|
16
|
+
return line.split('=')[1].strip().strip("'").strip('"')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def find_requires():
|
|
20
|
+
reqfile = os.path.join(BASEDIR, 'requirements.txt')
|
|
21
|
+
with open(reqfile, 'r', encoding='utf8') as f:
|
|
22
|
+
return [r.strip() for r in f.readlines()]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def find_long_description():
|
|
26
|
+
with open('README.md', encoding='utf8') as f:
|
|
27
|
+
desc = f.read()
|
|
28
|
+
desc = desc.replace('/blob/master/',
|
|
29
|
+
f'/blob/v{find_version()}/')
|
|
30
|
+
return desc
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
setup(
|
|
34
|
+
name='xflow.framework',
|
|
35
|
+
version=find_version(),
|
|
36
|
+
description='可以用 Python 写 Pipeline 的 CICD 框架。',
|
|
37
|
+
long_description=find_long_description(),
|
|
38
|
+
long_description_content_type='text/markdown',
|
|
39
|
+
author='zhaowcheng',
|
|
40
|
+
author_email='zhaowcheng@163.com',
|
|
41
|
+
install_requires=find_requires(),
|
|
42
|
+
packages=find_namespace_packages(exclude=['tests', 'tests.*']),
|
|
43
|
+
entry_points={
|
|
44
|
+
'console_scripts': [
|
|
45
|
+
'xflow = xflow.framework.main:main'
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
include_package_data=True,
|
|
49
|
+
classifiers=[
|
|
50
|
+
"License :: OSI Approved :: BSD License",
|
|
51
|
+
"Operating System :: OS Independent",
|
|
52
|
+
"Programming Language :: Python :: 3",
|
|
53
|
+
"Programming Language :: Python :: 3.6",
|
|
54
|
+
"Programming Language :: Python :: 3.7",
|
|
55
|
+
"Programming Language :: Python :: 3.8",
|
|
56
|
+
"Programming Language :: Python :: 3.9",
|
|
57
|
+
"Programming Language :: Python :: 3.10",
|
|
58
|
+
"Programming Language :: Python :: 3.11",
|
|
59
|
+
"Programming Language :: Python :: 3.12"
|
|
60
|
+
],
|
|
61
|
+
url='https://github.com/zhaowcheng/xflow.framework',
|
|
62
|
+
python_requires='>=3.6',
|
|
63
|
+
project_urls={
|
|
64
|
+
'Homepage': 'https://github.com/zhaowcheng/xflow.framework',
|
|
65
|
+
'Issues': 'https://github.com/zhaowcheng/xflow.framework/issues'
|
|
66
|
+
}
|
|
67
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026, zhaowcheng <zhaowcheng@163.com>
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
公共变量/常量。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
XFLOW_DIR: Path = Path(__file__).parent
|
|
11
|
+
|
|
12
|
+
STATICS_DIR = XFLOW_DIR.joinpath('statics')
|
|
13
|
+
|
|
14
|
+
INIT_DIR = STATICS_DIR.joinpath('initdir')
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026, zhaowcheng <zhaowcheng@163.com>
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
容器模块。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import tarfile
|
|
8
|
+
import io
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
from typing import Generator, Optional, Union, Dict
|
|
12
|
+
from select import select
|
|
13
|
+
from socket import SocketIO
|
|
14
|
+
from contextlib import contextmanager
|
|
15
|
+
from pathlib import Path, PurePosixPath
|
|
16
|
+
from functools import cached_property
|
|
17
|
+
|
|
18
|
+
from decorator import decorator
|
|
19
|
+
from docker.client import DockerClient
|
|
20
|
+
from docker.models.containers import Container
|
|
21
|
+
from docker.tls import TLSConfig
|
|
22
|
+
|
|
23
|
+
from xflow.framework.errors import CommandError
|
|
24
|
+
from xflow.framework.ssh import CommandResult
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@decorator
|
|
28
|
+
def autopen(func, *args, **kwargs):
|
|
29
|
+
"""
|
|
30
|
+
自动连接器。
|
|
31
|
+
"""
|
|
32
|
+
conn: ContainerConnection = args[0]
|
|
33
|
+
conn.open()
|
|
34
|
+
return func(*args, **kwargs)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ContainerConnection(object):
|
|
38
|
+
"""
|
|
39
|
+
容器连接。
|
|
40
|
+
"""
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
ip: str,
|
|
44
|
+
port: int = 2375,
|
|
45
|
+
user: Optional[str] = None,
|
|
46
|
+
name: Optional[str] = None,
|
|
47
|
+
image: Optional[str] = None,
|
|
48
|
+
runargs: Optional[Dict] = None,
|
|
49
|
+
cacert: Optional[str] = None,
|
|
50
|
+
clientcert: Optional[str] = None,
|
|
51
|
+
clientkey: Optional[str] = None,
|
|
52
|
+
envs: Optional[Dict[str, str]] = None
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
:param ip: IP 地址。
|
|
56
|
+
:param port: Docker 服务端口。
|
|
57
|
+
:param user: 用户名。
|
|
58
|
+
:param name: 容器名,和 `image` 为互斥参数,必须且只能指定其中一个。
|
|
59
|
+
:param image: 镜像名,和 `name` 为互斥参数,必须且只能指定其中一个,
|
|
60
|
+
如果该参数指定,则每次连接用该镜像创建一个新容器。
|
|
61
|
+
:param runargs: 使用镜像启动容器时的参数。
|
|
62
|
+
:param cacert: CA 证书。
|
|
63
|
+
:param clientcert: 客户端证书,当指定 `clientkey` 参数时必须同时指定该参数。
|
|
64
|
+
:param clientkey: 客户端私钥,当指定 `clientcert` 参数时必须同时指定该参数。
|
|
65
|
+
:param envs: 连接的默认环境变量,其中:
|
|
66
|
+
`LANG` 默认值为 `en_US.UTF-8`。
|
|
67
|
+
`LANGUAGE` 默认值为 `en_US.UTF-8`。
|
|
68
|
+
"""
|
|
69
|
+
# 必须且只能指定 `name` 和 `image` 参数中的一个。
|
|
70
|
+
if (name == None and image == None) or \
|
|
71
|
+
(name != None and image != None):
|
|
72
|
+
raise ValueError('Must specify only one of `name` and `image`.')
|
|
73
|
+
# `clientcert` 指定的同时必须指定 `clientkey`,反之亦然。
|
|
74
|
+
if (clientcert != None and clientkey == None) or \
|
|
75
|
+
(clientcert == None and clientkey != None):
|
|
76
|
+
raise ValueError('`clientcert` must be specified along with `clientkey`, and vice versa.')
|
|
77
|
+
self._ip = ip
|
|
78
|
+
self._port = port
|
|
79
|
+
self._user = user
|
|
80
|
+
self._envs = envs or {}
|
|
81
|
+
self._name = name
|
|
82
|
+
self._image = image
|
|
83
|
+
self._runargs = runargs or {}
|
|
84
|
+
self._cacert = cacert
|
|
85
|
+
self._clientcert = clientcert
|
|
86
|
+
self._clientkey = clientkey
|
|
87
|
+
self._dockerclient: DockerClient = None
|
|
88
|
+
self._container: Container = None
|
|
89
|
+
self._cwd: str = ''
|
|
90
|
+
for k, v in {'LANG': 'en_US.UTF-8',
|
|
91
|
+
'LANGUAGE': 'en_US.UTF-8'}.items():
|
|
92
|
+
self._envs.setdefault(k, v)
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def _connstr(self) -> str:
|
|
96
|
+
"""
|
|
97
|
+
连接字符串。
|
|
98
|
+
"""
|
|
99
|
+
if self._user:
|
|
100
|
+
prefix = f'docker://{self._user}@{self._ip}:{self._port}'
|
|
101
|
+
else:
|
|
102
|
+
prefix = f'docker://{self._ip}:{self._port}'
|
|
103
|
+
if self._name:
|
|
104
|
+
return f'{prefix}:{self._name}'
|
|
105
|
+
else: # 从镜像自动创建容器
|
|
106
|
+
if self._container:
|
|
107
|
+
# 容器已创建
|
|
108
|
+
return f'{prefix}:{self._image}->{self._container.name}'
|
|
109
|
+
else:
|
|
110
|
+
# 容器未创建
|
|
111
|
+
return f'{prefix}:{self._image}->...'
|
|
112
|
+
|
|
113
|
+
@cached_property
|
|
114
|
+
def _uid(self) -> int:
|
|
115
|
+
"""
|
|
116
|
+
用户 id。
|
|
117
|
+
"""
|
|
118
|
+
return int(self.exec('id -u'))
|
|
119
|
+
|
|
120
|
+
def open(self) -> None:
|
|
121
|
+
"""
|
|
122
|
+
开启连接。
|
|
123
|
+
"""
|
|
124
|
+
if self._container:
|
|
125
|
+
return
|
|
126
|
+
print(f'[{self._connstr}] Connecting...')
|
|
127
|
+
if self._cacert or self._clientcert:
|
|
128
|
+
tls = TLSConfig(
|
|
129
|
+
ca_cert=self._cacert,
|
|
130
|
+
verify=True if self._cacert else False,
|
|
131
|
+
client_cert=(self._clientcert, self._clientkey) if self._clientcert else None
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
tls = False
|
|
135
|
+
self._dockerclient = DockerClient(base_url=f'tcp://{self._ip}:{self._port}', tls=tls)
|
|
136
|
+
if self._name:
|
|
137
|
+
self._container = self._dockerclient.containers.get(self._name)
|
|
138
|
+
else:
|
|
139
|
+
self._container = self._dockerclient.containers.run(self._image, detach=True, **self._runargs)
|
|
140
|
+
print(f'[{self._connstr}] Connected')
|
|
141
|
+
|
|
142
|
+
def close(self) -> None:
|
|
143
|
+
"""
|
|
144
|
+
关闭连接。
|
|
145
|
+
"""
|
|
146
|
+
self._dockerclient.close()
|
|
147
|
+
|
|
148
|
+
def remove(self, force: bool = False) -> None:
|
|
149
|
+
"""
|
|
150
|
+
删除本容器。
|
|
151
|
+
|
|
152
|
+
:param force: 是否强制删除正在运行的容器。
|
|
153
|
+
"""
|
|
154
|
+
self._container.remove(force=force)
|
|
155
|
+
print(f'[{self._connstr}] Removed container {self._container.name}')
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def existed(self) -> bool:
|
|
159
|
+
"""
|
|
160
|
+
是否一个已存在的容器(即不是从镜像临时创建的)。
|
|
161
|
+
"""
|
|
162
|
+
return not self._image
|
|
163
|
+
|
|
164
|
+
@autopen()
|
|
165
|
+
def exec(
|
|
166
|
+
self,
|
|
167
|
+
cmd: str,
|
|
168
|
+
envs: Optional[Dict[str, str]] = None
|
|
169
|
+
) -> CommandResult:
|
|
170
|
+
"""
|
|
171
|
+
执行命令。
|
|
172
|
+
|
|
173
|
+
:param cmd: 被执行的命令。
|
|
174
|
+
:param envs: 环境变量。
|
|
175
|
+
:return: 命令输出。
|
|
176
|
+
|
|
177
|
+
:raises:
|
|
178
|
+
`CommandError` -- 命令返回码不为 0。
|
|
179
|
+
|
|
180
|
+
>>> exec('ls /home') # successful # doctest: +SKIP
|
|
181
|
+
>>> exec('ls /errpath') # CommandError # doctest: +SKIP
|
|
182
|
+
"""
|
|
183
|
+
envs = envs or {}
|
|
184
|
+
environment = self._envs.copy()
|
|
185
|
+
environment.update(envs)
|
|
186
|
+
print(f'[{self._connstr}:{self._cwd or "~"}] {cmd}')
|
|
187
|
+
cmdid = self._dockerclient.api.exec_create(
|
|
188
|
+
self._container.id,
|
|
189
|
+
# 使用 bash -c 的方式是为了避免 docker exec api 报 "cd not found" 的问题
|
|
190
|
+
'bash -c "' + (f'cd {self._cwd} && {cmd}' if self._cwd else cmd) + '"',
|
|
191
|
+
stdout=True,
|
|
192
|
+
stderr=True,
|
|
193
|
+
stdin=True,
|
|
194
|
+
tty=True,
|
|
195
|
+
environment=environment,
|
|
196
|
+
user=self._user
|
|
197
|
+
)['Id']
|
|
198
|
+
sock: SocketIO = self._dockerclient.api.exec_start(cmdid, tty=True, socket=True)
|
|
199
|
+
encoding = environment['LANG'].split('.')[-1]
|
|
200
|
+
output = ''
|
|
201
|
+
while True:
|
|
202
|
+
rlist, _, _ = select([sock], [], [], 0.1)
|
|
203
|
+
if sock in rlist:
|
|
204
|
+
data = sock.read(1024).decode(encoding=encoding, errors='ignore')
|
|
205
|
+
if data == '':
|
|
206
|
+
break
|
|
207
|
+
output += data
|
|
208
|
+
print(data, end='')
|
|
209
|
+
rc = self._dockerclient.api.exec_inspect(cmdid)['ExitCode']
|
|
210
|
+
if rc != 0:
|
|
211
|
+
raise CommandError(f'ExitCode {rc}: `{cmd}`')
|
|
212
|
+
return CommandResult(output, rc=rc, cmd=cmd)
|
|
213
|
+
|
|
214
|
+
@contextmanager
|
|
215
|
+
def dir(self, path: str | PurePosixPath) -> Generator[None, None, None]:
|
|
216
|
+
"""
|
|
217
|
+
切换工作目录。
|
|
218
|
+
|
|
219
|
+
>>> with dir('/my/workdir'): # doctest: +SKIP
|
|
220
|
+
... d = exec('pwd') # doctest: +SKIP
|
|
221
|
+
... # doctest: +SKIP
|
|
222
|
+
>>> d # doctest: +SKIP
|
|
223
|
+
'/my/workdir' # doctest: +SKIP
|
|
224
|
+
"""
|
|
225
|
+
try:
|
|
226
|
+
self._cwd = str(path)
|
|
227
|
+
yield
|
|
228
|
+
finally:
|
|
229
|
+
self._cwd = ''
|
|
230
|
+
|
|
231
|
+
@autopen()
|
|
232
|
+
def getfile(
|
|
233
|
+
self,
|
|
234
|
+
rfile: Union[str, PurePosixPath],
|
|
235
|
+
ldir: Union[str, Path]
|
|
236
|
+
) -> None:
|
|
237
|
+
"""
|
|
238
|
+
从容器下载文件 `rfile` 到本地目录 `ldir`。
|
|
239
|
+
|
|
240
|
+
:param rfile: 容器内文件。
|
|
241
|
+
:param ldir: 本地目录。
|
|
242
|
+
|
|
243
|
+
>>> getfile('/tmp/myfile', '/home') # /home/myfile
|
|
244
|
+
>>> getfile('/tmp/myfile', 'D:\\') # D:\\myfile
|
|
245
|
+
"""
|
|
246
|
+
rfile = PurePosixPath(rfile)
|
|
247
|
+
ldir = Path(ldir)
|
|
248
|
+
lfile = ldir.joinpath(rfile.name)
|
|
249
|
+
ltarfile = ldir.joinpath(f'{rfile.name}.tar')
|
|
250
|
+
print(f'[{self._connstr}] Get {lfile} <= {rfile}')
|
|
251
|
+
stream, _ = self._container.get_archive(str(rfile))
|
|
252
|
+
with open(ltarfile, 'wb') as f:
|
|
253
|
+
for chunk in stream:
|
|
254
|
+
f.write(chunk)
|
|
255
|
+
with tarfile.open(ltarfile) as tar:
|
|
256
|
+
tar.extractall(ldir)
|
|
257
|
+
os.remove(str(ltarfile))
|
|
258
|
+
|
|
259
|
+
@autopen()
|
|
260
|
+
def putfile(
|
|
261
|
+
self,
|
|
262
|
+
lfile: Union[str, Path],
|
|
263
|
+
rdir: Union[str, PurePosixPath]
|
|
264
|
+
) -> None:
|
|
265
|
+
"""
|
|
266
|
+
上传本地文件 `lfile` 到容器目录 `rdir`。
|
|
267
|
+
|
|
268
|
+
:param lfile: 本地文件。
|
|
269
|
+
:param rdir: 容器内目录。
|
|
270
|
+
|
|
271
|
+
>>> putfile('/home/myfile', '/tmp') # /tmp/myfile
|
|
272
|
+
>>> putfile('D:\\myfile', '/tmp') # /tmp/myfile
|
|
273
|
+
"""
|
|
274
|
+
lfile = Path(lfile)
|
|
275
|
+
rdir = PurePosixPath(rdir)
|
|
276
|
+
rfile = rdir.joinpath(lfile.name)
|
|
277
|
+
print(f'[{self._connstr}] Put {lfile} => {rfile}')
|
|
278
|
+
stream = io.BytesIO()
|
|
279
|
+
with tarfile.open(fileobj=stream, mode='w') as tar:
|
|
280
|
+
tarinfo = tar.gettarinfo(str(lfile), arcname=lfile.name)
|
|
281
|
+
tarinfo.uid = self._uid
|
|
282
|
+
with open(lfile, 'rb') as f:
|
|
283
|
+
tar.addfile(tarinfo, f)
|
|
284
|
+
stream.seek(0)
|
|
285
|
+
self._container.put_archive(str(rdir), stream)
|
|
286
|
+
|
|
287
|
+
@autopen()
|
|
288
|
+
def exists(self, path: Union[str, PurePosixPath]) -> bool:
|
|
289
|
+
"""
|
|
290
|
+
检查远端路径是否存在。
|
|
291
|
+
"""
|
|
292
|
+
return self.exec(f'test -e {path} && echo true || echo flase') == 'true'
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026, zhaowcheng <zhaowcheng@163.com>
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
环境信息管理。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ruamel import yaml
|
|
11
|
+
|
|
12
|
+
from xflow.framework.node import Node, NativeNode, ContainerNode
|
|
13
|
+
from xflow.framework.errors import NoSuchNodeError, NoSuchDockerError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Env(object):
|
|
17
|
+
"""
|
|
18
|
+
环境信息管理类。
|
|
19
|
+
"""
|
|
20
|
+
def __init__(self, envfile: str | Path):
|
|
21
|
+
"""
|
|
22
|
+
:param envfile: 环境信息文件。
|
|
23
|
+
"""
|
|
24
|
+
with open(envfile, encoding='utf8') as f:
|
|
25
|
+
self.__data = yaml.YAML(typ='safe').load(f)
|
|
26
|
+
self.__nodes: List[Node] = []
|
|
27
|
+
for nodeinfo in self.__data['nodes']:
|
|
28
|
+
if nodeinfo['docker']:
|
|
29
|
+
for dockerinfo in self.__data['dockers']:
|
|
30
|
+
if dockerinfo['name'] == nodeinfo['docker']:
|
|
31
|
+
self.__nodes.append(
|
|
32
|
+
ContainerNode(
|
|
33
|
+
name=nodeinfo['name'],
|
|
34
|
+
ip=dockerinfo['ip'],
|
|
35
|
+
port=dockerinfo['port'],
|
|
36
|
+
bwd=nodeinfo['workdir'],
|
|
37
|
+
user=nodeinfo['user'],
|
|
38
|
+
container=nodeinfo['container'],
|
|
39
|
+
image=nodeinfo['image'],
|
|
40
|
+
runargs=nodeinfo['runargs'],
|
|
41
|
+
cacert=dockerinfo['tls']['cacert'],
|
|
42
|
+
clientcert=dockerinfo['tls']['clientcert'],
|
|
43
|
+
clientkey=dockerinfo['tls']['clientkey'],
|
|
44
|
+
envs=nodeinfo['envs']
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
break
|
|
48
|
+
else:
|
|
49
|
+
raise NoSuchDockerError(f'name: {nodeinfo["docker"]}')
|
|
50
|
+
else:
|
|
51
|
+
self.__nodes.append(
|
|
52
|
+
NativeNode(
|
|
53
|
+
name=nodeinfo['name'],
|
|
54
|
+
ip=nodeinfo['ip'],
|
|
55
|
+
sshport=nodeinfo['sshport'],
|
|
56
|
+
user=nodeinfo['user'],
|
|
57
|
+
password=nodeinfo['password'],
|
|
58
|
+
bwd=nodeinfo['workdir'],
|
|
59
|
+
envs=nodeinfo['envs']
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def get_node(self, name: str) -> Node:
|
|
64
|
+
"""
|
|
65
|
+
获取节点。
|
|
66
|
+
|
|
67
|
+
:param name: 节点名。
|
|
68
|
+
"""
|
|
69
|
+
for node in self.__nodes:
|
|
70
|
+
if node.name == name:
|
|
71
|
+
return node
|
|
72
|
+
raise NoSuchNodeError(f'name: {name}')
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026, zhaowcheng <zhaowcheng@163.com>
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
异常类。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SSHConnectError(Exception):
|
|
9
|
+
"""
|
|
10
|
+
SSH 连接错误。
|
|
11
|
+
"""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CommandError(Exception):
|
|
16
|
+
"""
|
|
17
|
+
命令执行错误。
|
|
18
|
+
"""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NoSuchNodeError(Exception):
|
|
23
|
+
"""
|
|
24
|
+
无此节点。
|
|
25
|
+
"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class NoSuchDockerError(Exception):
|
|
30
|
+
"""
|
|
31
|
+
无此 Docker。
|
|
32
|
+
"""
|
|
33
|
+
pass
|