mantis-cli 17.0.0__tar.gz → 19.0.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.
- {mantis-cli-17.0.0/mantis_cli.egg-info → mantis-cli-19.0.0}/PKG-INFO +3 -6
- mantis-cli-19.0.0/mantis/__init__.py +1 -0
- {mantis-cli-17.0.0 → mantis-cli-19.0.0}/mantis/logic.py +7 -3
- {mantis-cli-17.0.0 → mantis-cli-19.0.0}/mantis/managers.py +104 -64
- {mantis-cli-17.0.0 → mantis-cli-19.0.0}/mantis/mantis.tpl +7 -7
- {mantis-cli-17.0.0 → mantis-cli-19.0.0/mantis_cli.egg-info}/PKG-INFO +3 -6
- {mantis-cli-17.0.0 → mantis-cli-19.0.0}/mantis_cli.egg-info/entry_points.txt +1 -0
- mantis-cli-17.0.0/mantis/__init__.py +0 -1
- {mantis-cli-17.0.0 → mantis-cli-19.0.0}/LICENSE +0 -0
- {mantis-cli-17.0.0 → mantis-cli-19.0.0}/MANIFEST.in +0 -0
- {mantis-cli-17.0.0 → mantis-cli-19.0.0}/README.md +0 -0
- {mantis-cli-17.0.0 → mantis-cli-19.0.0}/mantis/__main__.py +0 -0
- {mantis-cli-17.0.0 → mantis-cli-19.0.0}/mantis/command_line.py +0 -0
- {mantis-cli-17.0.0 → mantis-cli-19.0.0}/mantis/crypto.py +0 -0
- {mantis-cli-17.0.0 → mantis-cli-19.0.0}/mantis/environment.py +0 -0
- {mantis-cli-17.0.0 → mantis-cli-19.0.0}/mantis/extensions/__init__.py +0 -0
- {mantis-cli-17.0.0 → mantis-cli-19.0.0}/mantis/extensions/django.py +0 -0
- {mantis-cli-17.0.0 → mantis-cli-19.0.0}/mantis/extensions/nginx.py +0 -0
- {mantis-cli-17.0.0 → mantis-cli-19.0.0}/mantis/extensions/postgres.py +0 -0
- {mantis-cli-17.0.0 → mantis-cli-19.0.0}/mantis/helpers.py +0 -0
- {mantis-cli-17.0.0 → mantis-cli-19.0.0}/mantis_cli.egg-info/SOURCES.txt +0 -0
- {mantis-cli-17.0.0 → mantis-cli-19.0.0}/mantis_cli.egg-info/dependency_links.txt +0 -0
- {mantis-cli-17.0.0 → mantis-cli-19.0.0}/mantis_cli.egg-info/requires.txt +0 -0
- {mantis-cli-17.0.0 → mantis-cli-19.0.0}/mantis_cli.egg-info/top_level.txt +0 -0
- {mantis-cli-17.0.0 → mantis-cli-19.0.0}/setup.cfg +0 -0
- {mantis-cli-17.0.0 → mantis-cli-19.0.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mantis-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 19.0.0
|
|
4
4
|
Summary: Management command to build and deploy webapps, especially based on Django
|
|
5
5
|
Home-page: https://github.com/PragmaticMates/mantis-cli
|
|
6
6
|
Author: Erik Telepovský
|
|
@@ -9,6 +9,7 @@ Maintainer: Pragmatic Mates
|
|
|
9
9
|
Maintainer-email: info@pragmaticmates.com
|
|
10
10
|
License: GNU General Public License (GPL)
|
|
11
11
|
Keywords: management deployment docker command
|
|
12
|
+
Platform: UNKNOWN
|
|
12
13
|
Classifier: Programming Language :: Python
|
|
13
14
|
Classifier: Operating System :: OS Independent
|
|
14
15
|
Classifier: Environment :: Web Environment
|
|
@@ -18,11 +19,6 @@ Classifier: License :: OSI Approved :: GNU General Public License (GPL)
|
|
|
18
19
|
Classifier: Development Status :: 5 - Production/Stable
|
|
19
20
|
Description-Content-Type: text/markdown
|
|
20
21
|
License-File: LICENSE
|
|
21
|
-
Requires-Dist: cffi
|
|
22
|
-
Requires-Dist: cryptography
|
|
23
|
-
Requires-Dist: pycryptodome
|
|
24
|
-
Requires-Dist: PyYAML
|
|
25
|
-
Requires-Dist: prettytable
|
|
26
22
|
|
|
27
23
|
# mantis-cli
|
|
28
24
|
|
|
@@ -341,3 +337,4 @@ Works as follows:
|
|
|
341
337
|
## Release notes
|
|
342
338
|
|
|
343
339
|
Mantis uses semantic versioning. See more in [changelog](https://github.com/PragmaticMates/mantis-cli/blob/master/CHANGES.md).
|
|
340
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = '19.0.0'
|
|
@@ -102,11 +102,15 @@ def load_config(config_file):
|
|
|
102
102
|
CLI.error(f"Failed to load config from file {config_file}: {e}")
|
|
103
103
|
|
|
104
104
|
|
|
105
|
-
def
|
|
106
|
-
# Load config template file
|
|
105
|
+
def load_template_config():
|
|
107
106
|
current_directory = dirname(abspath(__file__))
|
|
108
107
|
template_path = normpath(f'{current_directory}/mantis.tpl')
|
|
109
|
-
|
|
108
|
+
return load_config(template_path)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def check_config(config):
|
|
112
|
+
# Load config template file
|
|
113
|
+
template = load_template_config()
|
|
110
114
|
|
|
111
115
|
# validate config file
|
|
112
116
|
config_keys_only = find_keys_only_in_config(config, template)
|
|
@@ -2,6 +2,7 @@ import json
|
|
|
2
2
|
import os
|
|
3
3
|
import time
|
|
4
4
|
import yaml
|
|
5
|
+
from collections import defaultdict
|
|
5
6
|
from os import path
|
|
6
7
|
from os.path import dirname, normpath
|
|
7
8
|
from time import sleep
|
|
@@ -9,7 +10,7 @@ from time import sleep
|
|
|
9
10
|
from mantis.crypto import Crypto
|
|
10
11
|
from mantis.environment import Environment
|
|
11
12
|
from mantis.helpers import CLI, Colors
|
|
12
|
-
from mantis.logic import find_config, load_config, check_config
|
|
13
|
+
from mantis.logic import find_config, load_config, check_config, load_template_config
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class AbstractManager(object):
|
|
@@ -37,7 +38,7 @@ class AbstractManager(object):
|
|
|
37
38
|
self.init_environment()
|
|
38
39
|
|
|
39
40
|
self.KEY = self.read_key()
|
|
40
|
-
self.encrypt_deterministically = self.config
|
|
41
|
+
self.encrypt_deterministically = self.config['encryption']['deterministic']
|
|
41
42
|
|
|
42
43
|
@property
|
|
43
44
|
def host(self):
|
|
@@ -101,7 +102,7 @@ class AbstractManager(object):
|
|
|
101
102
|
setattr(self, property_name, details)
|
|
102
103
|
|
|
103
104
|
# set project path
|
|
104
|
-
self.project_path = self.config
|
|
105
|
+
self.project_path = self.config['project_path']
|
|
105
106
|
|
|
106
107
|
return details
|
|
107
108
|
|
|
@@ -122,19 +123,24 @@ class AbstractManager(object):
|
|
|
122
123
|
return ''
|
|
123
124
|
|
|
124
125
|
def init_config(self, config):
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
self.config_file_path = path.normpath(path.join(self.config_file, os.pardir))
|
|
126
|
+
check_config(config)
|
|
127
|
+
config_file_path = path.normpath(path.join(self.config_file, os.pardir))
|
|
128
128
|
|
|
129
129
|
def normalize(path):
|
|
130
|
-
return os.path.normpath(path.replace('<MANTIS>',
|
|
130
|
+
return os.path.normpath(path.replace('<MANTIS>', config_file_path))
|
|
131
|
+
|
|
132
|
+
# Load config template file
|
|
133
|
+
defaults = load_template_config()
|
|
134
|
+
|
|
135
|
+
# Merge custom config and default values
|
|
136
|
+
defaults.update(config)
|
|
131
137
|
|
|
132
|
-
|
|
133
|
-
self.
|
|
134
|
-
|
|
135
|
-
self.
|
|
136
|
-
self.
|
|
137
|
-
self.
|
|
138
|
+
# Save merged config to variable
|
|
139
|
+
self.config = defaults.copy()
|
|
140
|
+
|
|
141
|
+
self.key_file = normalize(path.join(self.config['encryption']['folder'], 'mantis.key'))
|
|
142
|
+
self.environment_path = normalize(self.config['environment']['folder'])
|
|
143
|
+
self.compose_path = normalize(path.join(self.config['compose']['folder'], self.environment_id))
|
|
138
144
|
|
|
139
145
|
def init_environment(self):
|
|
140
146
|
self.env = Environment(
|
|
@@ -143,16 +149,13 @@ class AbstractManager(object):
|
|
|
143
149
|
)
|
|
144
150
|
|
|
145
151
|
# connection
|
|
146
|
-
self.connection = self.config
|
|
152
|
+
self.connection = self.config['connections'].get(self.env.id, None)
|
|
147
153
|
|
|
148
|
-
# compose
|
|
149
|
-
|
|
150
|
-
self.compose_file = os.path.join(self.compose_path, f'{compose_prefix}.{self.env.id}.yml')
|
|
154
|
+
# compose files
|
|
155
|
+
compose_file_paths = os.popen(f'find {self.compose_path} -name "*.yml" -o -name "*.yaml"').read().strip().split('\n')
|
|
151
156
|
|
|
152
|
-
#
|
|
153
|
-
self.
|
|
154
|
-
self.CONTAINER_PREFIX = self.PROJECT_NAME
|
|
155
|
-
self.IMAGE_PREFIX = self.PROJECT_NAME
|
|
157
|
+
# Remove empty strings
|
|
158
|
+
self.compose_files = list(filter(None, compose_file_paths))
|
|
156
159
|
|
|
157
160
|
def check_environment_encryption(self, env_file):
|
|
158
161
|
decrypted_environment = self.decrypt_env(env_file=env_file, return_value=True) # .env.encrypted
|
|
@@ -233,8 +236,12 @@ class AbstractManager(object):
|
|
|
233
236
|
)
|
|
234
237
|
|
|
235
238
|
def docker_compose(self, command, return_output=False, use_connection=True):
|
|
239
|
+
compose_command = self.config['compose']['command']
|
|
240
|
+
|
|
241
|
+
compose_files = ' '.join([f'-f {compose_file}' for compose_file in self.compose_files])
|
|
242
|
+
|
|
236
243
|
return self.docker_command(
|
|
237
|
-
command=f'{
|
|
244
|
+
command=f'{compose_command} {compose_files} {command}',
|
|
238
245
|
return_output=return_output,
|
|
239
246
|
use_connection=use_connection
|
|
240
247
|
)
|
|
@@ -267,7 +274,7 @@ class AbstractManager(object):
|
|
|
267
274
|
containers = list(filter(None, containers))
|
|
268
275
|
|
|
269
276
|
# get project containers only
|
|
270
|
-
containers = list(filter(lambda c: self.get_container_project(c)
|
|
277
|
+
containers = list(filter(lambda c: self.get_container_project(c) in self.project_services().keys(), containers))
|
|
271
278
|
|
|
272
279
|
# find containers starting with custom prefix
|
|
273
280
|
containers = list(filter(lambda s: s.startswith(prefix), containers))
|
|
@@ -526,7 +533,8 @@ class BaseManager(AbstractManager):
|
|
|
526
533
|
Constructs container name with project prefix for given service
|
|
527
534
|
"""
|
|
528
535
|
suffix = self.get_container_suffix(service)
|
|
529
|
-
|
|
536
|
+
prefix = self.get_project_by_service(service)
|
|
537
|
+
return f'{prefix}{suffix}'.replace('_', '-')
|
|
530
538
|
|
|
531
539
|
def get_service_containers(self, service):
|
|
532
540
|
"""
|
|
@@ -553,7 +561,8 @@ class BaseManager(AbstractManager):
|
|
|
553
561
|
Constructs image name for given service
|
|
554
562
|
"""
|
|
555
563
|
suffix = self.get_image_suffix(service)
|
|
556
|
-
|
|
564
|
+
prefix = self.get_project_by_service(service)
|
|
565
|
+
return f'{prefix}{suffix}'.replace('-', '_')
|
|
557
566
|
|
|
558
567
|
def has_healthcheck(self, container):
|
|
559
568
|
"""
|
|
@@ -647,7 +656,7 @@ class BaseManager(AbstractManager):
|
|
|
647
656
|
CLI.info(f'Params = {params}')
|
|
648
657
|
|
|
649
658
|
# Construct build args from config
|
|
650
|
-
build_args = self.config
|
|
659
|
+
build_args = self.config['build']['args']
|
|
651
660
|
build_args = ','.join(map('='.join, build_args.items()))
|
|
652
661
|
|
|
653
662
|
if build_args != '':
|
|
@@ -657,7 +666,7 @@ class BaseManager(AbstractManager):
|
|
|
657
666
|
|
|
658
667
|
CLI.info(f'Args = {build_args}')
|
|
659
668
|
|
|
660
|
-
build_tool = self.config
|
|
669
|
+
build_tool = self.config['build']['tool']
|
|
661
670
|
available_tools = ['compose', 'docker']
|
|
662
671
|
|
|
663
672
|
if build_tool == 'compose':
|
|
@@ -671,7 +680,7 @@ class BaseManager(AbstractManager):
|
|
|
671
680
|
image = info['image'] if info['image'] != '' else f"{info['project_name']}-{service}".lstrip('-')
|
|
672
681
|
|
|
673
682
|
# build paths for docker build command (paths in compose are relative to compose file, but paths for docker command are relative to $PWD)
|
|
674
|
-
context = normpath(path.join(dirname(self.
|
|
683
|
+
context = normpath(path.join(dirname(self.compose_path), info['context']))
|
|
675
684
|
dockerfile = normpath(path.join(context, info['dockerfile']))
|
|
676
685
|
|
|
677
686
|
# Build service using docker
|
|
@@ -680,47 +689,73 @@ class BaseManager(AbstractManager):
|
|
|
680
689
|
else:
|
|
681
690
|
CLI.error(f'Unknown build tool: {build_tool}. Available tools: {", ".join(available_tools)}')
|
|
682
691
|
|
|
683
|
-
def
|
|
692
|
+
def project_services(self):
|
|
684
693
|
"""
|
|
685
|
-
Returns project
|
|
694
|
+
Returns project names by compose files
|
|
686
695
|
"""
|
|
687
|
-
|
|
688
|
-
|
|
696
|
+
projects = defaultdict(list)
|
|
697
|
+
|
|
698
|
+
for compose_file in self.compose_files:
|
|
699
|
+
with open(compose_file, 'r') as file:
|
|
700
|
+
compose_data = yaml.safe_load(file)
|
|
701
|
+
name = compose_data.get('name', '')
|
|
702
|
+
services = compose_data.get('services', {}).keys()
|
|
703
|
+
projects[name].extend(services)
|
|
704
|
+
|
|
705
|
+
return projects
|
|
706
|
+
|
|
707
|
+
def get_project_by_service(self, service):
|
|
708
|
+
project_services = self.project_services()
|
|
709
|
+
|
|
710
|
+
for project, services in project_services.items():
|
|
711
|
+
if service in services:
|
|
712
|
+
return project
|
|
689
713
|
|
|
690
|
-
return
|
|
714
|
+
return None
|
|
691
715
|
|
|
692
|
-
def services(self):
|
|
716
|
+
def services(self, compose_file=None):
|
|
693
717
|
"""
|
|
694
|
-
|
|
718
|
+
Returns all defined services
|
|
695
719
|
"""
|
|
696
|
-
|
|
697
|
-
|
|
720
|
+
services = []
|
|
721
|
+
|
|
722
|
+
compose_files = [compose_file] if compose_file else self.compose_files
|
|
723
|
+
|
|
724
|
+
for compose_file in compose_files:
|
|
725
|
+
with open(compose_file, 'r') as file:
|
|
726
|
+
compose_data = yaml.safe_load(file)
|
|
727
|
+
compose_services = compose_data.get('services', {}).keys()
|
|
698
728
|
|
|
699
|
-
|
|
729
|
+
services += compose_services
|
|
700
730
|
|
|
701
|
-
|
|
731
|
+
return services
|
|
732
|
+
|
|
733
|
+
def services_to_build(self, compose_file=None):
|
|
702
734
|
"""
|
|
703
735
|
Prints all services which will be build
|
|
704
736
|
"""
|
|
705
|
-
with open(self.compose_file, 'r') as file:
|
|
706
|
-
compose_data = yaml.safe_load(file)
|
|
707
|
-
|
|
708
737
|
data = {}
|
|
709
738
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
739
|
+
compose_files = [compose_file] if compose_file else self.compose_files
|
|
740
|
+
|
|
741
|
+
for compose_file in compose_files:
|
|
742
|
+
with open(compose_file, 'r') as file:
|
|
743
|
+
compose_data = yaml.safe_load(file)
|
|
744
|
+
|
|
745
|
+
services = compose_data.get('services', {})
|
|
746
|
+
for service_name, service_config in services.items():
|
|
747
|
+
build = service_config.get('build', None)
|
|
748
|
+
|
|
749
|
+
if build:
|
|
750
|
+
data[service_name] = {
|
|
751
|
+
'project_name': compose_data.get('name', ''),
|
|
752
|
+
'dockerfile': build.get('dockerfile', 'Dockerfile'),
|
|
753
|
+
'context': build.get('context', '.'),
|
|
754
|
+
'cache_from': build.get('cache_from', []),
|
|
755
|
+
'args': build.get('args', {}),
|
|
756
|
+
'image': service_config.get('image', ''),
|
|
757
|
+
'platform': service_config.get('platform', '')
|
|
758
|
+
}
|
|
724
759
|
|
|
725
760
|
return data
|
|
726
761
|
|
|
@@ -757,7 +792,7 @@ class BaseManager(AbstractManager):
|
|
|
757
792
|
elif self.mode == 'ssh':
|
|
758
793
|
CLI.info('Uploading docker compose configs, environment files and mantis')
|
|
759
794
|
|
|
760
|
-
files_to_upload = [self.config_file
|
|
795
|
+
files_to_upload = [self.config_file] + self.compose_files + self.env.files
|
|
761
796
|
|
|
762
797
|
# mantis config file
|
|
763
798
|
for file in files_to_upload:
|
|
@@ -834,7 +869,7 @@ class BaseManager(AbstractManager):
|
|
|
834
869
|
Runs zero-downtime deployment of services (or given service)
|
|
835
870
|
"""
|
|
836
871
|
if not service:
|
|
837
|
-
zero_downtime_services = self.config
|
|
872
|
+
zero_downtime_services = self.config['zero_downtime']
|
|
838
873
|
for index, service in enumerate(zero_downtime_services):
|
|
839
874
|
CLI.step(index + 1, len(zero_downtime_services), f'Zero downtime services: {zero_downtime_services}')
|
|
840
875
|
self.zero_downtime(service)
|
|
@@ -1127,10 +1162,15 @@ class BaseManager(AbstractManager):
|
|
|
1127
1162
|
"""
|
|
1128
1163
|
Returns default number of deploy replicas of given services
|
|
1129
1164
|
"""
|
|
1130
|
-
|
|
1131
|
-
compose_data = yaml.safe_load(file)
|
|
1165
|
+
replicas = 1
|
|
1132
1166
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1167
|
+
for compose_file in self.compose_files:
|
|
1168
|
+
with open(compose_file, 'r') as file:
|
|
1169
|
+
compose_data = yaml.safe_load(file)
|
|
1170
|
+
|
|
1171
|
+
try:
|
|
1172
|
+
replicas = compose_data['services'][service]['deploy']['replicas']
|
|
1173
|
+
except KeyError:
|
|
1174
|
+
pass
|
|
1175
|
+
|
|
1176
|
+
return replicas
|
|
@@ -12,25 +12,25 @@
|
|
|
12
12
|
},
|
|
13
13
|
"encryption": {
|
|
14
14
|
"deterministic": true,
|
|
15
|
-
"folder": ""
|
|
15
|
+
"folder": "<MANTIS>"
|
|
16
16
|
},
|
|
17
17
|
"configs": {
|
|
18
|
-
"folder": "
|
|
18
|
+
"folder": "<MANTIS>/.."
|
|
19
19
|
},
|
|
20
20
|
"build": {
|
|
21
|
-
"tool": "
|
|
21
|
+
"tool": "compose",
|
|
22
|
+
"args": {}
|
|
22
23
|
},
|
|
23
24
|
"compose": {
|
|
24
25
|
"command": "docker-compose",
|
|
25
|
-
"
|
|
26
|
-
"folder": ""
|
|
26
|
+
"folder": "<MANTIS>/../compose"
|
|
27
27
|
},
|
|
28
28
|
"environment": {
|
|
29
|
-
"folder": "
|
|
29
|
+
"folder": "<MANTIS>/../environments",
|
|
30
30
|
"file_prefix": ""
|
|
31
31
|
},
|
|
32
32
|
"zero_downtime": [],
|
|
33
|
-
"project_path": "",
|
|
33
|
+
"project_path": "~",
|
|
34
34
|
"connections": {
|
|
35
35
|
}
|
|
36
36
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mantis-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 19.0.0
|
|
4
4
|
Summary: Management command to build and deploy webapps, especially based on Django
|
|
5
5
|
Home-page: https://github.com/PragmaticMates/mantis-cli
|
|
6
6
|
Author: Erik Telepovský
|
|
@@ -9,6 +9,7 @@ Maintainer: Pragmatic Mates
|
|
|
9
9
|
Maintainer-email: info@pragmaticmates.com
|
|
10
10
|
License: GNU General Public License (GPL)
|
|
11
11
|
Keywords: management deployment docker command
|
|
12
|
+
Platform: UNKNOWN
|
|
12
13
|
Classifier: Programming Language :: Python
|
|
13
14
|
Classifier: Operating System :: OS Independent
|
|
14
15
|
Classifier: Environment :: Web Environment
|
|
@@ -18,11 +19,6 @@ Classifier: License :: OSI Approved :: GNU General Public License (GPL)
|
|
|
18
19
|
Classifier: Development Status :: 5 - Production/Stable
|
|
19
20
|
Description-Content-Type: text/markdown
|
|
20
21
|
License-File: LICENSE
|
|
21
|
-
Requires-Dist: cffi
|
|
22
|
-
Requires-Dist: cryptography
|
|
23
|
-
Requires-Dist: pycryptodome
|
|
24
|
-
Requires-Dist: PyYAML
|
|
25
|
-
Requires-Dist: prettytable
|
|
26
22
|
|
|
27
23
|
# mantis-cli
|
|
28
24
|
|
|
@@ -341,3 +337,4 @@ Works as follows:
|
|
|
341
337
|
## Release notes
|
|
342
338
|
|
|
343
339
|
Mantis uses semantic versioning. See more in [changelog](https://github.com/PragmaticMates/mantis-cli/blob/master/CHANGES.md).
|
|
340
|
+
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
VERSION = '17.0.0'
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|