seto 3.0.3__tar.gz → 3.2.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.
- {seto-3.0.3 → seto-3.2.0}/PKG-INFO +4 -4
- {seto-3.0.3 → seto-3.2.0}/README.md +3 -3
- {seto-3.0.3 → seto-3.2.0}/pyproject.toml +1 -1
- {seto-3.0.3 → seto-3.2.0}/seto/__main__.py +34 -20
- {seto-3.0.3 → seto-3.2.0}/seto/commands/config.py +2 -2
- {seto-3.0.3 → seto-3.2.0}/seto/commands/down.py +3 -3
- {seto-3.0.3 → seto-3.2.0}/seto/core/docker.py +8 -7
- {seto-3.0.3 → seto-3.2.0}/seto/core/driver.py +11 -8
- {seto-3.0.3 → seto-3.2.0}/seto/core/network.py +15 -14
- {seto-3.0.3 → seto-3.2.0}/seto/core/parser.py +25 -15
- {seto-3.0.3 → seto-3.2.0}/seto/core/shell.py +20 -10
- {seto-3.0.3 → seto-3.2.0}/seto/core/traefik.py +3 -3
- {seto-3.0.3 → seto-3.2.0}/seto/shells/remote.py +5 -2
- seto-3.0.3/seto/drivers/gluster.py +0 -146
- {seto-3.0.3 → seto-3.2.0}/LICENSE +0 -0
- {seto-3.0.3 → seto-3.2.0}/LICENSE_HEADER.txt +0 -0
- {seto-3.0.3 → seto-3.2.0}/seto/__init__.py +0 -0
- {seto-3.0.3 → seto-3.2.0}/seto/commands/deploy.py +0 -0
- {seto-3.0.3 → seto-3.2.0}/seto/commands/mount.py +0 -0
- {seto-3.0.3 → seto-3.2.0}/seto/commands/setup.py +0 -0
- {seto-3.0.3 → seto-3.2.0}/seto/commands/umount.py +0 -0
- {seto-3.0.3 → seto-3.2.0}/seto/commands/volumes.py +0 -0
- {seto-3.0.3 → seto-3.2.0}/seto/core/command.py +0 -0
- {seto-3.0.3 → seto-3.2.0}/seto/core/dns.py +0 -0
- {seto-3.0.3 → seto-3.2.0}/seto/core/permissions.py +0 -0
- {seto-3.0.3 → seto-3.2.0}/seto/core/swarm.py +0 -0
- {seto-3.0.3 → seto-3.2.0}/seto/core/volume.py +0 -0
- {seto-3.0.3 → seto-3.2.0}/seto/drivers/nfs.py +0 -0
- {seto-3.0.3 → seto-3.2.0}/seto/shells/local.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: seto
|
|
3
|
-
Version: 3.0
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: A Docker Swarm Deployment Manager
|
|
5
5
|
License: Apache 2.0
|
|
6
6
|
Keywords: docker,swarm,manager
|
|
@@ -21,8 +21,8 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
# Ṣeto
|
|
22
22
|
|
|
23
23
|
Ṣeto is a command-line tool designed to assist with setting up and managing
|
|
24
|
-
shared storage volumes using NFS
|
|
25
|
-
|
|
24
|
+
shared storage volumes using NFS driver. It simplifies the process of
|
|
25
|
+
configuring stack-based deployments, setting up manager and replica nodes,
|
|
26
26
|
creating and syncing shared volumes, and mounting and unmounting these volumes.
|
|
27
27
|
|
|
28
28
|
### Features
|
|
@@ -43,7 +43,7 @@ description of each subcommand and its options.
|
|
|
43
43
|
These options are applicable to all subcommands:
|
|
44
44
|
|
|
45
45
|
- `--stack`: Required. Specifies the stack name.
|
|
46
|
-
- `--driver`: Required. Specifies the driver URI to use.
|
|
46
|
+
- `--driver`: Required. Specifies the driver URI to use. Example: `nfs://username:password@hostname`
|
|
47
47
|
|
|
48
48
|
#### Subcommands
|
|
49
49
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Ṣeto
|
|
2
2
|
|
|
3
3
|
Ṣeto is a command-line tool designed to assist with setting up and managing
|
|
4
|
-
shared storage volumes using NFS
|
|
5
|
-
|
|
4
|
+
shared storage volumes using NFS driver. It simplifies the process of
|
|
5
|
+
configuring stack-based deployments, setting up manager and replica nodes,
|
|
6
6
|
creating and syncing shared volumes, and mounting and unmounting these volumes.
|
|
7
7
|
|
|
8
8
|
### Features
|
|
@@ -23,7 +23,7 @@ description of each subcommand and its options.
|
|
|
23
23
|
These options are applicable to all subcommands:
|
|
24
24
|
|
|
25
25
|
- `--stack`: Required. Specifies the stack name.
|
|
26
|
-
- `--driver`: Required. Specifies the driver URI to use.
|
|
26
|
+
- `--driver`: Required. Specifies the driver URI to use. Example: `nfs://username:password@hostname`
|
|
27
27
|
|
|
28
28
|
#### Subcommands
|
|
29
29
|
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "seto"
|
|
7
|
-
version = "3.0
|
|
7
|
+
version = "3.2.0"
|
|
8
8
|
description = "A Docker Swarm Deployment Manager"
|
|
9
9
|
keywords = ["docker", "swarm", "manager"]
|
|
10
10
|
authors = ["Sébastien Demanou <demsking@gmail.com>"]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2024 Sébastien Demanou. All Rights Reserved.
|
|
1
|
+
# Copyright 2024-2025 Sébastien Demanou. All Rights Reserved.
|
|
2
2
|
#
|
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
# you may not use this file except in compliance with the License.
|
|
@@ -27,47 +27,53 @@ from .commands.setup import print_ssh_copy_id_commands
|
|
|
27
27
|
from .commands.umount import execute_umount_volumes_command
|
|
28
28
|
from .commands.volumes import execute_create_volumes_command
|
|
29
29
|
from .core.shell import Setting
|
|
30
|
-
from .drivers.gluster import GlusterDriver
|
|
31
30
|
from .drivers.nfs import NFSDriver
|
|
32
31
|
from .shells.local import LocalShell
|
|
33
32
|
from .shells.remote import RemoteShell
|
|
34
33
|
|
|
35
|
-
DriverType =
|
|
36
|
-
ShellConnectionString =
|
|
34
|
+
DriverType = str
|
|
35
|
+
ShellConnectionString = str
|
|
37
36
|
|
|
38
37
|
|
|
39
|
-
driver_shemes = ['nfs://'
|
|
38
|
+
driver_shemes = ['nfs://']
|
|
40
39
|
driver_examples = [f'{sheme}username:password@hostname' for sheme in driver_shemes]
|
|
41
40
|
DRIVER_EXAMPLES_STR = ' or '.join(driver_examples)
|
|
42
41
|
|
|
43
42
|
|
|
44
43
|
def manager_type(value: str) -> tuple[DriverType, ShellConnectionString]:
|
|
45
44
|
if not any(value.startswith(scheme) for scheme in driver_shemes):
|
|
46
|
-
raise argparse.ArgumentError(
|
|
45
|
+
raise argparse.ArgumentError(
|
|
46
|
+
'manager', # type: ignore
|
|
47
|
+
f'{value} is not a valid manager ({DRIVER_EXAMPLES_STR})',
|
|
48
|
+
)
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
driver_type, connection_string = value.split('://', 1)
|
|
51
|
+
|
|
52
|
+
return driver_type, connection_string
|
|
49
53
|
|
|
50
54
|
|
|
51
55
|
def replica_type(value: str) -> list[Setting]:
|
|
52
56
|
connection_strings = value.split(' ')
|
|
53
57
|
|
|
54
58
|
try:
|
|
55
|
-
return [
|
|
56
|
-
Setting.from_connection_string(item.strip()) for item in connection_strings
|
|
57
|
-
]
|
|
59
|
+
return [Setting.from_connection_string(item.strip()) for item in connection_strings]
|
|
58
60
|
except Exception as exception:
|
|
59
|
-
raise argparse.ArgumentError(
|
|
61
|
+
raise argparse.ArgumentError(
|
|
62
|
+
'replica', # type: ignore
|
|
63
|
+
f'{value} is not a replica value (username:password@hostname)',
|
|
64
|
+
) from exception
|
|
60
65
|
|
|
61
66
|
|
|
62
67
|
def nodes_type(value: str) -> list[Setting]:
|
|
63
68
|
connection_strings = value.split(' ')
|
|
64
69
|
|
|
65
70
|
try:
|
|
66
|
-
return [
|
|
67
|
-
Setting.from_connection_string(item.strip()) for item in connection_strings
|
|
68
|
-
]
|
|
71
|
+
return [Setting.from_connection_string(item.strip()) for item in connection_strings]
|
|
69
72
|
except Exception as exception:
|
|
70
|
-
raise argparse.ArgumentError(
|
|
73
|
+
raise argparse.ArgumentError(
|
|
74
|
+
'nodes', # type: ignore
|
|
75
|
+
f'{value} is not a nodes value (username:password@hostname)',
|
|
76
|
+
) from exception
|
|
71
77
|
|
|
72
78
|
|
|
73
79
|
def main() -> None:
|
|
@@ -125,7 +131,9 @@ def main() -> None:
|
|
|
125
131
|
|
|
126
132
|
#
|
|
127
133
|
# Create Volumes command
|
|
128
|
-
create_volumes_parser = subparsers.add_parser(
|
|
134
|
+
create_volumes_parser = subparsers.add_parser(
|
|
135
|
+
'create-volumes', description='Create and sync shared volumes'
|
|
136
|
+
)
|
|
129
137
|
create_volumes_parser.set_defaults(func=execute_create_volumes_command)
|
|
130
138
|
create_volumes_parser.add_argument(
|
|
131
139
|
'--replica',
|
|
@@ -155,7 +163,9 @@ def main() -> None:
|
|
|
155
163
|
|
|
156
164
|
#
|
|
157
165
|
# Unmount Volumes command
|
|
158
|
-
umount_volumes_parser = subparsers.add_parser(
|
|
166
|
+
umount_volumes_parser = subparsers.add_parser(
|
|
167
|
+
'unmount-volumes', description='Unmount shared volumes'
|
|
168
|
+
)
|
|
159
169
|
umount_volumes_parser.set_defaults(func=execute_umount_volumes_command)
|
|
160
170
|
umount_volumes_parser.add_argument(
|
|
161
171
|
'--clients',
|
|
@@ -167,7 +177,9 @@ def main() -> None:
|
|
|
167
177
|
|
|
168
178
|
#
|
|
169
179
|
# Config command
|
|
170
|
-
config_parser = subparsers.add_parser(
|
|
180
|
+
config_parser = subparsers.add_parser(
|
|
181
|
+
'config', description='Parse, resolve and render compose file in canonical format'
|
|
182
|
+
)
|
|
171
183
|
config_parser.set_defaults(func=execute_config_command)
|
|
172
184
|
|
|
173
185
|
config_parser.add_argument(
|
|
@@ -178,7 +190,9 @@ def main() -> None:
|
|
|
178
190
|
|
|
179
191
|
#
|
|
180
192
|
# Deploy command
|
|
181
|
-
deploy_parser = subparsers.add_parser(
|
|
193
|
+
deploy_parser = subparsers.add_parser(
|
|
194
|
+
'deploy', description='Deploy a new stack or update an existing stack'
|
|
195
|
+
)
|
|
182
196
|
deploy_parser.set_defaults(func=execute_deploy_command)
|
|
183
197
|
|
|
184
198
|
#
|
|
@@ -197,7 +211,7 @@ def main() -> None:
|
|
|
197
211
|
driver_name, connection_string = args.manager
|
|
198
212
|
setting = Setting.from_connection_string(connection_string)
|
|
199
213
|
create_shell = LocalShell if setting.local else RemoteShell
|
|
200
|
-
create_driver =
|
|
214
|
+
create_driver = NFSDriver
|
|
201
215
|
shell = create_shell(setting, args.key)
|
|
202
216
|
driver = create_driver(args.stack, project=args.project, shell=shell)
|
|
203
217
|
|
|
@@ -87,11 +87,11 @@ def resolve(
|
|
|
87
87
|
|
|
88
88
|
def execute_config_command(args, driver: Driver) -> None:
|
|
89
89
|
compose = resolve(args, driver)
|
|
90
|
-
compose_output = yaml.dump(compose)
|
|
90
|
+
compose_output = yaml.dump(compose) or '{}'
|
|
91
91
|
|
|
92
92
|
if args.compose:
|
|
93
93
|
command = f'docker compose -p {driver.stack_id} -f - config'
|
|
94
94
|
else:
|
|
95
95
|
command = 'docker stack config -c -'
|
|
96
96
|
|
|
97
|
-
Shell.pipe_exec(command, pipe_input=compose_output)
|
|
97
|
+
Shell.pipe_exec(command, pipe_input=str(compose_output))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2024 Sébastien Demanou. All Rights Reserved.
|
|
1
|
+
# Copyright 2024-2025 Sébastien Demanou. All Rights Reserved.
|
|
2
2
|
#
|
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
# you may not use this file except in compliance with the License.
|
|
@@ -28,7 +28,7 @@ def execute_down_command(args, driver: Driver) -> None:
|
|
|
28
28
|
setattr(args, 'compose', False)
|
|
29
29
|
|
|
30
30
|
swarm = DockerSwarm(
|
|
31
|
-
stack=driver.stack_id,
|
|
31
|
+
# stack=driver.stack_id,
|
|
32
32
|
client=client,
|
|
33
33
|
driver=driver,
|
|
34
34
|
config=resolve(args, driver),
|
|
@@ -42,7 +42,7 @@ def execute_down_command(args, driver: Driver) -> None:
|
|
|
42
42
|
|
|
43
43
|
if config['services']:
|
|
44
44
|
compose = DockerCompose(
|
|
45
|
-
stack=driver.stack_id,
|
|
45
|
+
# stack=driver.stack_id,
|
|
46
46
|
client=client,
|
|
47
47
|
driver=driver,
|
|
48
48
|
config=config,
|
|
@@ -45,8 +45,9 @@ class Docker:
|
|
|
45
45
|
@property
|
|
46
46
|
def external_networks(self) -> list[str]:
|
|
47
47
|
return [
|
|
48
|
-
item.attrs['Name']
|
|
49
|
-
|
|
48
|
+
item.attrs['Name']
|
|
49
|
+
for item in self.client.networks.list()
|
|
50
|
+
if item.attrs and item.attrs['Name'].startswith(self.driver.stack_id)
|
|
50
51
|
]
|
|
51
52
|
|
|
52
53
|
@staticmethod
|
|
@@ -57,7 +58,7 @@ class Docker:
|
|
|
57
58
|
pipe_input: str,
|
|
58
59
|
) -> None:
|
|
59
60
|
ssh_command = f"ssh {Driver.setouser}@{hostname} '{command}'"
|
|
60
|
-
return Shell.pipe_exec(ssh_command, pipe_input=pipe_input)
|
|
61
|
+
return Shell.pipe_exec(ssh_command, pipe_input=pipe_input) # type: ignore
|
|
61
62
|
|
|
62
63
|
def build(self) -> None:
|
|
63
64
|
Shell.pipe_exec(
|
|
@@ -99,8 +100,8 @@ class DockerCompose(Docker):
|
|
|
99
100
|
return self.config.get('x-placement-hostname', None)
|
|
100
101
|
|
|
101
102
|
@property
|
|
102
|
-
def placement(self) -> str
|
|
103
|
-
return self.config.get('x-placement',
|
|
103
|
+
def placement(self) -> str:
|
|
104
|
+
return self.config.get('x-placement', '')
|
|
104
105
|
|
|
105
106
|
@property
|
|
106
107
|
def current_hostname(self) -> str:
|
|
@@ -108,7 +109,7 @@ class DockerCompose(Docker):
|
|
|
108
109
|
|
|
109
110
|
@property
|
|
110
111
|
@functools.lru_cache(maxsize=128)
|
|
111
|
-
def node_hostname(self) -> str
|
|
112
|
+
def node_hostname(self) -> str:
|
|
112
113
|
if self.placement_hostname:
|
|
113
114
|
return self.placement_hostname
|
|
114
115
|
|
|
@@ -119,7 +120,7 @@ class DockerCompose(Docker):
|
|
|
119
120
|
value = f'{label_value}'.strip()
|
|
120
121
|
|
|
121
122
|
for node in nodes:
|
|
122
|
-
if node.attrs['Spec']['Labels'].get(label_key) == value:
|
|
123
|
+
if node.attrs and node.attrs['Spec']['Labels'].get(label_key) == value:
|
|
123
124
|
return node.attrs['Description']['Hostname']
|
|
124
125
|
|
|
125
126
|
raise ValueError(f'Unable to found node for placement "{self.placement}"')
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2024 Sébastien Demanou. All Rights Reserved.
|
|
1
|
+
# Copyright 2024-2025 Sébastien Demanou. All Rights Reserved.
|
|
2
2
|
#
|
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
# you may not use this file except in compliance with the License.
|
|
@@ -30,7 +30,7 @@ class Driver:
|
|
|
30
30
|
project: str,
|
|
31
31
|
shell: Shell,
|
|
32
32
|
) -> None:
|
|
33
|
-
self.stack = stack
|
|
33
|
+
self.stack: str | None = stack
|
|
34
34
|
self.project = project
|
|
35
35
|
self.shell = shell
|
|
36
36
|
|
|
@@ -48,7 +48,7 @@ class Driver:
|
|
|
48
48
|
|
|
49
49
|
@property
|
|
50
50
|
def sheme(self) -> str:
|
|
51
|
-
|
|
51
|
+
return f'{self.slug}://'
|
|
52
52
|
|
|
53
53
|
@property
|
|
54
54
|
def driver_name(self) -> str:
|
|
@@ -92,7 +92,10 @@ class Driver:
|
|
|
92
92
|
node.run(f'touch {authorized_keys_file}', quiet=True)
|
|
93
93
|
authorized_keys = node.file(authorized_keys_file)
|
|
94
94
|
|
|
95
|
-
authorized_keys
|
|
95
|
+
# Append the SSH public key to authorized_keys file
|
|
96
|
+
if node.ssh_pub_key:
|
|
97
|
+
authorized_keys.append(node.ssh_pub_key)
|
|
98
|
+
|
|
96
99
|
authorized_keys.chown(self.setouser, self.setouser)
|
|
97
100
|
authorized_keys.chmod('600')
|
|
98
101
|
|
|
@@ -132,7 +135,7 @@ class Driver:
|
|
|
132
135
|
) -> None:
|
|
133
136
|
raise NotImplementedError()
|
|
134
137
|
|
|
135
|
-
def mount_volumes(self, *, clients: list[
|
|
138
|
+
def mount_volumes(self, *, clients: list[Setting], volumes: list[Volume]) -> None:
|
|
136
139
|
if len(volumes) > 0:
|
|
137
140
|
for node_setting in clients:
|
|
138
141
|
node = Driver.get_shell(node_setting, self.shell.key_file_path)
|
|
@@ -162,7 +165,7 @@ class Driver:
|
|
|
162
165
|
def mount(self, node: Shell, storage: str, *, device: str, fstab: File) -> None:
|
|
163
166
|
raise NotImplementedError()
|
|
164
167
|
|
|
165
|
-
def umount_volumes(self, *, clients: list[
|
|
168
|
+
def umount_volumes(self, *, clients: list[Setting], volumes: list[Volume]) -> None:
|
|
166
169
|
if len(volumes) > 0:
|
|
167
170
|
for node_setting in clients:
|
|
168
171
|
node = Driver.get_shell(node_setting, self.shell.key_file_path)
|
|
@@ -173,7 +176,7 @@ class Driver:
|
|
|
173
176
|
def umount_node_volumes(self, node: Shell, volumes: list[Volume]) -> None:
|
|
174
177
|
for volume in volumes:
|
|
175
178
|
print(f'\nUnmounting volume {node.hostname}:{self.mount_point(volume.mount_folder)}')
|
|
176
|
-
self.umount(node, volume)
|
|
179
|
+
self.umount(node, volume.mount_folder)
|
|
177
180
|
|
|
178
181
|
def umount(self, node: Shell, device: str) -> None:
|
|
179
182
|
node.run(f'umount {device}')
|
|
@@ -181,6 +184,6 @@ class Driver:
|
|
|
181
184
|
def resolve_compose_volume(self, volume: Volume) -> dict:
|
|
182
185
|
raise NotImplementedError()
|
|
183
186
|
|
|
184
|
-
def terminate(self):
|
|
187
|
+
def terminate(self) -> None:
|
|
185
188
|
# Close the SSH connection
|
|
186
189
|
self.shell.close()
|
|
@@ -17,33 +17,31 @@ import os
|
|
|
17
17
|
|
|
18
18
|
import yaml
|
|
19
19
|
|
|
20
|
+
|
|
21
|
+
NETWORK_CONFIG = {
|
|
22
|
+
'ipam': {
|
|
23
|
+
'driver': 'default',
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
GLOBAL_NETWORKS = {
|
|
21
28
|
'seto-cloud-public': {
|
|
22
29
|
'name': 'seto-cloud-public',
|
|
23
30
|
'driver': 'overlay',
|
|
24
31
|
'attachable': True,
|
|
32
|
+
**NETWORK_CONFIG,
|
|
25
33
|
},
|
|
26
34
|
'seto-cloud-edge': {
|
|
27
35
|
'name': 'seto-cloud-edge',
|
|
28
36
|
'driver': 'overlay',
|
|
29
37
|
'attachable': True,
|
|
30
|
-
|
|
31
|
-
'config': [
|
|
32
|
-
{'subnet': '172.20.0.0/20'},
|
|
33
|
-
{'subnet': 'fd00:3984:3989::/64'},
|
|
34
|
-
],
|
|
35
|
-
},
|
|
38
|
+
**NETWORK_CONFIG,
|
|
36
39
|
},
|
|
37
40
|
'seto-http-provider': {
|
|
38
41
|
'name': 'seto-http-provider',
|
|
39
42
|
'driver': 'overlay',
|
|
40
43
|
'attachable': True,
|
|
41
|
-
|
|
42
|
-
'config': [
|
|
43
|
-
{'subnet': '172.20.0.0/20'},
|
|
44
|
-
{'subnet': 'fd00:3984:3989::/64'},
|
|
45
|
-
],
|
|
46
|
-
},
|
|
44
|
+
**NETWORK_CONFIG,
|
|
47
45
|
},
|
|
48
46
|
}
|
|
49
47
|
|
|
@@ -53,7 +51,8 @@ def get_global_external_networks() -> dict:
|
|
|
53
51
|
shortname: {
|
|
54
52
|
'name': network['name'],
|
|
55
53
|
'external': True,
|
|
56
|
-
}
|
|
54
|
+
}
|
|
55
|
+
for shortname, network in GLOBAL_NETWORKS.items()
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
|
|
@@ -67,7 +66,7 @@ def resolve_networks(
|
|
|
67
66
|
for network_file in networks_files:
|
|
68
67
|
with open(network_file, encoding='utf-8') as file:
|
|
69
68
|
network_key = os.path.splitext(os.path.basename(network_file))[0]
|
|
70
|
-
network_data = yaml.safe_load(file)
|
|
69
|
+
network_data: dict = yaml.safe_load(file) or {} # type: ignore
|
|
71
70
|
network_name = network_data.get('name', f'{project}_{network_key}')
|
|
72
71
|
merged_data[network_key] = network_data
|
|
73
72
|
|
|
@@ -75,6 +74,8 @@ def resolve_networks(
|
|
|
75
74
|
network_data.clear()
|
|
76
75
|
|
|
77
76
|
network_data['external'] = True
|
|
77
|
+
else:
|
|
78
|
+
network_data.update(NETWORK_CONFIG)
|
|
78
79
|
|
|
79
80
|
network_data['name'] = network_name
|
|
80
81
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2024 Sébastien Demanou. All Rights Reserved.
|
|
1
|
+
# Copyright 2024-2025 Sébastien Demanou. All Rights Reserved.
|
|
2
2
|
#
|
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
# you may not use this file except in compliance with the License.
|
|
@@ -79,9 +79,13 @@ def parse_local_volumes(
|
|
|
79
79
|
image_name = f'{stack}-{service_name}'.replace('_', '-')
|
|
80
80
|
service_dockerfile_name = f'{image_name}.dockerfile'
|
|
81
81
|
service_dockerfile_file = os.path.join('images', service_dockerfile_name)
|
|
82
|
-
service_dockerfile_definition =
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
service_dockerfile_definition = (
|
|
83
|
+
[
|
|
84
|
+
f'FROM {service["image"]}',
|
|
85
|
+
]
|
|
86
|
+
+ [f'COPY {source} {target}' for source, target in local_volumes]
|
|
87
|
+
+ ['']
|
|
88
|
+
)
|
|
85
89
|
service_dockerfile = '\n'.join(service_dockerfile_definition)
|
|
86
90
|
service_dockerfile = resolve_env_vars(service_dockerfile)
|
|
87
91
|
|
|
@@ -235,11 +239,15 @@ def parse_service_configs(
|
|
|
235
239
|
|
|
236
240
|
if source.startswith('./') or source.startswith('../'):
|
|
237
241
|
if os.path.isfile(source):
|
|
238
|
-
config_name = re.sub(
|
|
242
|
+
config_name = re.sub(
|
|
243
|
+
r'_{2,}',
|
|
244
|
+
'_',
|
|
245
|
+
f"{service_name}_{source.replace('/', '_').replace('.', '_')}".replace('-', '_'),
|
|
246
|
+
)
|
|
239
247
|
|
|
240
248
|
if inject:
|
|
241
249
|
with open(source, encoding='utf-8') as config:
|
|
242
|
-
configs[config_name] = {
|
|
250
|
+
configs[config_name] = { # type: ignore
|
|
243
251
|
'content': config.read(),
|
|
244
252
|
}
|
|
245
253
|
else:
|
|
@@ -247,11 +255,13 @@ def parse_service_configs(
|
|
|
247
255
|
'file': source,
|
|
248
256
|
}
|
|
249
257
|
|
|
250
|
-
service_configs.append(
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
258
|
+
service_configs.append(
|
|
259
|
+
{
|
|
260
|
+
'source': config_name,
|
|
261
|
+
'target': target,
|
|
262
|
+
'mode': parse_permission_mode(mode),
|
|
263
|
+
}
|
|
264
|
+
)
|
|
255
265
|
|
|
256
266
|
continue
|
|
257
267
|
|
|
@@ -274,7 +284,7 @@ def resolve_env_vars(content: str) -> str:
|
|
|
274
284
|
return output.stdout
|
|
275
285
|
|
|
276
286
|
|
|
277
|
-
def parse_compose_file(compose_file: str, resolve_vars=False) -> tuple[dict, str
|
|
287
|
+
def parse_compose_file(compose_file: str, resolve_vars=False) -> tuple[dict, str]:
|
|
278
288
|
compose_file = os.path.realpath(compose_file)
|
|
279
289
|
|
|
280
290
|
with open(compose_file, encoding='utf-8') as file:
|
|
@@ -285,7 +295,7 @@ def parse_compose_file(compose_file: str, resolve_vars=False) -> tuple[dict, str
|
|
|
285
295
|
|
|
286
296
|
compose_data = yaml.safe_load(compose_content)
|
|
287
297
|
|
|
288
|
-
return compose_data, compose_file
|
|
298
|
+
return compose_data, compose_file # type: ignore
|
|
289
299
|
|
|
290
300
|
|
|
291
301
|
def parse_services(
|
|
@@ -295,13 +305,13 @@ def parse_services(
|
|
|
295
305
|
execute: Callable[[dict, list], None] | None = None,
|
|
296
306
|
mode: list[ResolveMode] | None = None,
|
|
297
307
|
inject: bool = False,
|
|
298
|
-
) -> tuple[
|
|
308
|
+
) -> tuple[list, list]:
|
|
299
309
|
services_files = glob.glob(os.path.join(stack, '*.yaml'))
|
|
300
310
|
output_resolved_compose_data = []
|
|
301
311
|
output_volumes = []
|
|
302
312
|
|
|
303
313
|
for service_file in services_files:
|
|
304
|
-
resolve_vars = mode and 'compose' in mode
|
|
314
|
+
resolve_vars = isinstance(mode, list) and 'compose' in mode
|
|
305
315
|
compose_data, _ = parse_compose_file(service_file, resolve_vars)
|
|
306
316
|
x_mode = compose_data.get('x-mode', 'swarm')
|
|
307
317
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2024 Sébastien Demanou. All Rights Reserved.
|
|
1
|
+
# Copyright 2024-2025 Sébastien Demanou. All Rights Reserved.
|
|
2
2
|
#
|
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
# you may not use this file except in compliance with the License.
|
|
@@ -49,6 +49,12 @@ class Setting:
|
|
|
49
49
|
node_host = client_user
|
|
50
50
|
client_user = 'root'
|
|
51
51
|
|
|
52
|
+
if not node_host:
|
|
53
|
+
raise ValueError('Unable to determine hostname from connection string')
|
|
54
|
+
|
|
55
|
+
if not client_user:
|
|
56
|
+
raise ValueError('Unable to determine username from connection string')
|
|
57
|
+
|
|
52
58
|
username, password = tuple((client_user.split(':') + [None, None])[0:2])
|
|
53
59
|
node_ip = resolve_hostname(node_host)
|
|
54
60
|
|
|
@@ -86,7 +92,7 @@ class File:
|
|
|
86
92
|
else:
|
|
87
93
|
self.shell.run(f'chown {owner} {self.filename}', quiet=True)
|
|
88
94
|
|
|
89
|
-
def chmod(self, mode: str):
|
|
95
|
+
def chmod(self, mode: str | int):
|
|
90
96
|
if 'letsencrypt' in self.filename:
|
|
91
97
|
mode = 600
|
|
92
98
|
|
|
@@ -104,7 +110,7 @@ class Shell:
|
|
|
104
110
|
return self.setting.hostname
|
|
105
111
|
|
|
106
112
|
@property
|
|
107
|
-
def username(self) -> str:
|
|
113
|
+
def username(self) -> str | None:
|
|
108
114
|
return self.setting.username
|
|
109
115
|
|
|
110
116
|
@property
|
|
@@ -113,9 +119,11 @@ class Shell:
|
|
|
113
119
|
|
|
114
120
|
@property
|
|
115
121
|
@lru_cache
|
|
116
|
-
def ssh_pub_key(self) -> str:
|
|
117
|
-
|
|
118
|
-
|
|
122
|
+
def ssh_pub_key(self) -> str | None:
|
|
123
|
+
if self.pub_key_file_path:
|
|
124
|
+
with open(self.pub_key_file_path, encoding='utf-8') as key_file:
|
|
125
|
+
return key_file.read()
|
|
126
|
+
return None
|
|
119
127
|
|
|
120
128
|
def connect(self):
|
|
121
129
|
raise NotImplementedError()
|
|
@@ -135,7 +143,7 @@ class Shell:
|
|
|
135
143
|
stderr=subprocess.PIPE,
|
|
136
144
|
text=True,
|
|
137
145
|
shell=True,
|
|
138
|
-
env={**(env_vars or {}), **dict(subprocess.os.environ)},
|
|
146
|
+
env={**(env_vars or {}), **dict(subprocess.os.environ)}, # type: ignore
|
|
139
147
|
)
|
|
140
148
|
|
|
141
149
|
standard_output, standard_error = process.communicate(**kwargs)
|
|
@@ -155,16 +163,18 @@ class Shell:
|
|
|
155
163
|
with StringIO(pipe_input) as pipe:
|
|
156
164
|
return Shell.exec(command, stdout=stdout, env_vars=env_vars, input=pipe.read())
|
|
157
165
|
|
|
158
|
-
def install(self, package_name: str, *, user='nobody', group='nogroup') ->
|
|
166
|
+
def install(self, package_name: str, *, user='nobody', group='nogroup') -> bool:
|
|
159
167
|
result = self.run(f'dpkg -l | grep {package_name}', quiet=True, stdout=False)
|
|
160
168
|
|
|
161
169
|
if package_name not in result:
|
|
162
|
-
self.run(
|
|
170
|
+
self.run(
|
|
171
|
+
f'DEBIAN_FRONTEND=noninteractive apt-get install -y --quiet {package_name}', quiet=True
|
|
172
|
+
)
|
|
163
173
|
return True
|
|
164
174
|
|
|
165
175
|
return False
|
|
166
176
|
|
|
167
|
-
def mkdir(self, path: str, *, user='nobody', group='nogroup', mode: str = 'g+w') -> None:
|
|
177
|
+
def mkdir(self, path: str, *, user='nobody', group='nogroup', mode: str | int = 'g+w') -> None:
|
|
168
178
|
try:
|
|
169
179
|
self.run(f'ls {path}', quiet=True, stderr=True)
|
|
170
180
|
except Exception:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2024 Sébastien Demanou. All Rights Reserved.
|
|
1
|
+
# Copyright 2024-2025 Sébastien Demanou. All Rights Reserved.
|
|
2
2
|
#
|
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
# you may not use this file except in compliance with the License.
|
|
@@ -37,10 +37,10 @@ def deep_set(dct: dict[str, Any], keys: str, value: Any):
|
|
|
37
37
|
# Extend the list if the index doesn't exist yet
|
|
38
38
|
while len(current) <= key:
|
|
39
39
|
current.append({})
|
|
40
|
-
current = current[key]
|
|
40
|
+
current = current[key] # type: ignore
|
|
41
41
|
else:
|
|
42
42
|
if isinstance(current, list): # Handle case where current is mistakenly a list
|
|
43
|
-
current = current[-1]
|
|
43
|
+
current = current[-1] # type: ignore
|
|
44
44
|
current = current.setdefault(key, {})
|
|
45
45
|
|
|
46
46
|
# Handle the last key separately
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2024 Sébastien Demanou. All Rights Reserved.
|
|
1
|
+
# Copyright 2024-2025 Sébastien Demanou. All Rights Reserved.
|
|
2
2
|
#
|
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
# you may not use this file except in compliance with the License.
|
|
@@ -45,7 +45,10 @@ class RemoteShell(Shell):
|
|
|
45
45
|
pkey=private_key,
|
|
46
46
|
)
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
fd = self.ssh.open_sftp()
|
|
49
|
+
|
|
50
|
+
if fd:
|
|
51
|
+
self._fs = fd
|
|
49
52
|
|
|
50
53
|
def run(
|
|
51
54
|
self,
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
# Copyright 2024 Sébastien Demanou. All Rights Reserved.
|
|
2
|
-
#
|
|
3
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
# you may not use this file except in compliance with the License.
|
|
5
|
-
# You may obtain a copy of the License at
|
|
6
|
-
#
|
|
7
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
#
|
|
9
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
# See the License for the specific language governing permissions and
|
|
13
|
-
# limitations under the License.
|
|
14
|
-
# ==============================================================================
|
|
15
|
-
from ..core.driver import Driver
|
|
16
|
-
from ..core.shell import File
|
|
17
|
-
from ..core.shell import Setting
|
|
18
|
-
from ..core.shell import Shell
|
|
19
|
-
from ..core.volume import Volume
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class GlusterDriver(Driver):
|
|
23
|
-
@property
|
|
24
|
-
def slug(self) -> str:
|
|
25
|
-
return 'glusterfs'
|
|
26
|
-
|
|
27
|
-
@property
|
|
28
|
-
def driver_name(self) -> str:
|
|
29
|
-
return 'GlusterFS'
|
|
30
|
-
|
|
31
|
-
@staticmethod
|
|
32
|
-
def install_plugin(shell: Shell, force: bool = False) -> None:
|
|
33
|
-
"""
|
|
34
|
-
Install GlusterFS Volume Plugin.
|
|
35
|
-
|
|
36
|
-
Images:
|
|
37
|
-
- https://registry.hub.docker.com/r/mochoa/glusterfs-volume-plugin
|
|
38
|
-
- https://registry.hub.docker.com/r/mochoa/glusterfs-volume-plugin-aarch64
|
|
39
|
-
"""
|
|
40
|
-
uname = shell.run('uname -a', stdout=False)
|
|
41
|
-
plugins_list = shell.run(
|
|
42
|
-
cmd='docker plugin ls | grep glusterfs',
|
|
43
|
-
stdout=False,
|
|
44
|
-
quiet=True,
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
if 'glusterfs' in plugins_list:
|
|
48
|
-
if 'true' in plugins_list and not force:
|
|
49
|
-
# Skip install if already installed
|
|
50
|
-
return
|
|
51
|
-
|
|
52
|
-
shell.run('docker plugin disable glusterfs')
|
|
53
|
-
shell.run('docker plugin rm glusterfs')
|
|
54
|
-
|
|
55
|
-
if 'aarch64' in uname:
|
|
56
|
-
image = 'mochoa/glusterfs-volume-plugin-aarch64'
|
|
57
|
-
else:
|
|
58
|
-
image = 'mochoa/glusterfs-volume-plugin'
|
|
59
|
-
|
|
60
|
-
shell.run(f'docker plugin install --alias glusterfs {image} --grant-all-permissions --disable')
|
|
61
|
-
shell.run('docker plugin enable glusterfs')
|
|
62
|
-
shell.run('docker plugin ls')
|
|
63
|
-
|
|
64
|
-
@staticmethod
|
|
65
|
-
def install_server(shell: Shell) -> None:
|
|
66
|
-
# Add the user to the nogroup group
|
|
67
|
-
shell.run(f'usermod -aG nogroup {shell.username}')
|
|
68
|
-
|
|
69
|
-
fresh_installed = shell.install('glusterfs-server')
|
|
70
|
-
|
|
71
|
-
if fresh_installed:
|
|
72
|
-
shell.run('systemctl start glusterd')
|
|
73
|
-
shell.run('systemctl enable glusterd')
|
|
74
|
-
|
|
75
|
-
def setup_manager(self, replica: list[Setting], force: bool = False) -> None:
|
|
76
|
-
super().setup_manager(replica, force)
|
|
77
|
-
|
|
78
|
-
# Add the user to the nogroup group
|
|
79
|
-
GlusterDriver.install_server(self.shell)
|
|
80
|
-
GlusterDriver.install_plugin(self.shell, force)
|
|
81
|
-
|
|
82
|
-
for node_setting in replica:
|
|
83
|
-
shell = Driver.get_shell(node_setting, self.shell.key_file_path)
|
|
84
|
-
|
|
85
|
-
shell.connect()
|
|
86
|
-
GlusterDriver.install_server(shell)
|
|
87
|
-
GlusterDriver.install_plugin(shell, force)
|
|
88
|
-
shell.close()
|
|
89
|
-
|
|
90
|
-
for node_setting in replica:
|
|
91
|
-
self.shell.run(f'gluster peer probe {node_setting.hostname}')
|
|
92
|
-
|
|
93
|
-
self.shell.run('gluster peer status')
|
|
94
|
-
self.shell.run('gluster pool list')
|
|
95
|
-
|
|
96
|
-
def apply_manager_changes(self) -> None:
|
|
97
|
-
pass
|
|
98
|
-
|
|
99
|
-
def setup_node(self, shell: Shell, force: bool = False) -> None:
|
|
100
|
-
shell.install('glusterfs-client')
|
|
101
|
-
GlusterDriver.install_plugin(shell, force)
|
|
102
|
-
|
|
103
|
-
def create_volumes(
|
|
104
|
-
self,
|
|
105
|
-
*,
|
|
106
|
-
replica: list[Setting],
|
|
107
|
-
volumes: list[Volume],
|
|
108
|
-
force: bool = False,
|
|
109
|
-
) -> None:
|
|
110
|
-
self.shell.mkdir(self.brick)
|
|
111
|
-
|
|
112
|
-
replica_count = len(replica)
|
|
113
|
-
replica_items = [f'{item.hostname}:{self.brick}' for item in replica]
|
|
114
|
-
replica_items_str = ' '.join(replica_items)
|
|
115
|
-
|
|
116
|
-
for volume in volumes:
|
|
117
|
-
self.shell.run(f'gluster volume create {volume.name} replica {replica_count} transport tcp {replica_items_str} force')
|
|
118
|
-
self.shell.run(f'gluster volume start {volume.name}')
|
|
119
|
-
|
|
120
|
-
self.shell.run('gluster volume info')
|
|
121
|
-
|
|
122
|
-
for volume in volumes:
|
|
123
|
-
print(f'Initializing volume {volume.name}...')
|
|
124
|
-
self.server.copy_volume(volume, self.brick)
|
|
125
|
-
|
|
126
|
-
def mount_volume(self, node: Shell, volume: Volume, fstab: File) -> None:
|
|
127
|
-
"""
|
|
128
|
-
Mount the given volume on the given node.
|
|
129
|
-
|
|
130
|
-
See https://docs.gluster.org/en/latest/Administrator-Guide/Setting-Up-Clients/#mounting-volumes
|
|
131
|
-
"""
|
|
132
|
-
volume_id = f'{self.shell.setting.ip}:/{volume.name}'
|
|
133
|
-
|
|
134
|
-
node.mkdir(volume.device)
|
|
135
|
-
node.run(f'mount -t glusterfs {volume_id} {volume.device}')
|
|
136
|
-
node.run(f'df -h {volume.device}')
|
|
137
|
-
fstab.append(f'{volume_id}\t{volume.device}\tglusterfs\tdefaults,_netdev 0 0')
|
|
138
|
-
|
|
139
|
-
def resolve_compose_volume(self, volume: Volume) -> dict:
|
|
140
|
-
return {
|
|
141
|
-
'name': volume.name + volume.mount_point,
|
|
142
|
-
'driver': 'glusterfs',
|
|
143
|
-
'driver_opts': {
|
|
144
|
-
'servers': self.shell.hostname,
|
|
145
|
-
},
|
|
146
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|