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.
Files changed (29) hide show
  1. {seto-3.0.3 → seto-3.2.0}/PKG-INFO +4 -4
  2. {seto-3.0.3 → seto-3.2.0}/README.md +3 -3
  3. {seto-3.0.3 → seto-3.2.0}/pyproject.toml +1 -1
  4. {seto-3.0.3 → seto-3.2.0}/seto/__main__.py +34 -20
  5. {seto-3.0.3 → seto-3.2.0}/seto/commands/config.py +2 -2
  6. {seto-3.0.3 → seto-3.2.0}/seto/commands/down.py +3 -3
  7. {seto-3.0.3 → seto-3.2.0}/seto/core/docker.py +8 -7
  8. {seto-3.0.3 → seto-3.2.0}/seto/core/driver.py +11 -8
  9. {seto-3.0.3 → seto-3.2.0}/seto/core/network.py +15 -14
  10. {seto-3.0.3 → seto-3.2.0}/seto/core/parser.py +25 -15
  11. {seto-3.0.3 → seto-3.2.0}/seto/core/shell.py +20 -10
  12. {seto-3.0.3 → seto-3.2.0}/seto/core/traefik.py +3 -3
  13. {seto-3.0.3 → seto-3.2.0}/seto/shells/remote.py +5 -2
  14. seto-3.0.3/seto/drivers/gluster.py +0 -146
  15. {seto-3.0.3 → seto-3.2.0}/LICENSE +0 -0
  16. {seto-3.0.3 → seto-3.2.0}/LICENSE_HEADER.txt +0 -0
  17. {seto-3.0.3 → seto-3.2.0}/seto/__init__.py +0 -0
  18. {seto-3.0.3 → seto-3.2.0}/seto/commands/deploy.py +0 -0
  19. {seto-3.0.3 → seto-3.2.0}/seto/commands/mount.py +0 -0
  20. {seto-3.0.3 → seto-3.2.0}/seto/commands/setup.py +0 -0
  21. {seto-3.0.3 → seto-3.2.0}/seto/commands/umount.py +0 -0
  22. {seto-3.0.3 → seto-3.2.0}/seto/commands/volumes.py +0 -0
  23. {seto-3.0.3 → seto-3.2.0}/seto/core/command.py +0 -0
  24. {seto-3.0.3 → seto-3.2.0}/seto/core/dns.py +0 -0
  25. {seto-3.0.3 → seto-3.2.0}/seto/core/permissions.py +0 -0
  26. {seto-3.0.3 → seto-3.2.0}/seto/core/swarm.py +0 -0
  27. {seto-3.0.3 → seto-3.2.0}/seto/core/volume.py +0 -0
  28. {seto-3.0.3 → seto-3.2.0}/seto/drivers/nfs.py +0 -0
  29. {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
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 or GlusterFS drivers. It simplifies the process
25
- of configuring stack-based deployments, setting up manager and replica nodes,
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. Can be `nfs://username:password@hostname` or `gluster://username:password@hostname`.
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 or GlusterFS drivers. It simplifies the process
5
- of configuring stack-based deployments, setting up manager and replica nodes,
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. Can be `nfs://username:password@hostname` or `gluster://username:password@hostname`.
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.3"
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 = type[str]
36
- ShellConnectionString = type[str]
34
+ DriverType = str
35
+ ShellConnectionString = str
37
36
 
38
37
 
39
- driver_shemes = ['nfs://', 'glusterfs://']
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('manager', f'{value} is not a valid manager ({DRIVER_EXAMPLES_STR})')
45
+ raise argparse.ArgumentError(
46
+ 'manager', # type: ignore
47
+ f'{value} is not a valid manager ({DRIVER_EXAMPLES_STR})',
48
+ )
47
49
 
48
- return value.split('://')
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('replica', f'{value} is not a replica value (username:password@hostname)') from exception
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('nodes', f'{value} is not a nodes value (username:password@hostname)') from exception
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('create-volumes', description='Create and sync shared volumes')
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('unmount-volumes', description='Unmount shared volumes')
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('config', description='Parse, resolve and render compose file in canonical format')
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('deploy', description='Deploy a new stack or update an existing stack')
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 = GlusterDriver if driver_name == 'gluster' else NFSDriver
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'] for item in self.client.networks.list()
49
- if item.attrs['Name'].startswith(self.driver.stack_id)
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 | None:
103
- return self.config.get('x-placement', None)
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 | None:
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
- raise f'{self.slug}://'
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.append(node.ssh_pub_key)
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[str], volumes: list[Volume]) -> None:
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[str], volumes: list[Volume]) -> None:
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
- 'ipam': {
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
- 'ipam': {
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
- } for shortname, network in GLOBAL_NETWORKS.items()
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
- f'FROM {service["image"]}',
84
- ] + [f'COPY {source} {target}' for source, target in local_volumes] + ['']
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(r'_{2,}', '_', f"{service_name}_{source.replace('/', '_').replace('.', '_')}".replace('-', '_'))
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
- 'source': config_name,
252
- 'target': target,
253
- 'mode': parse_permission_mode(mode),
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, 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[dict, list]:
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
- with open(self.pub_key_file_path, encoding='utf-8') as key_file:
118
- return key_file.read()
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') -> None:
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(f'apt-get install -y --quiet {package_name}', quiet=True)
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
- self._fs = self.ssh.open_sftp()
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