sshplex 1.0.4__tar.gz → 1.0.10__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 (35) hide show
  1. {sshplex-1.0.4/sshplex.egg-info → sshplex-1.0.10}/PKG-INFO +2 -2
  2. {sshplex-1.0.4 → sshplex-1.0.10}/README.md +1 -1
  3. {sshplex-1.0.4 → sshplex-1.0.10}/pyproject.toml +1 -1
  4. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/__init__.py +1 -1
  5. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/cli.py +2 -1
  6. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/config-template.yaml +0 -1
  7. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/config.py +3 -1
  8. sshplex-1.0.10/sshplex/lib/sot/netbox.py +278 -0
  9. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/main.py +2 -1
  10. {sshplex-1.0.4 → sshplex-1.0.10/sshplex.egg-info}/PKG-INFO +2 -2
  11. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex.egg-info/SOURCES.txt +0 -1
  12. sshplex-1.0.4/sshplex/lib/sot/netbox.py +0 -174
  13. sshplex-1.0.4/sshplex/populate_examples.py +0 -0
  14. {sshplex-1.0.4 → sshplex-1.0.10}/LICENSE +0 -0
  15. {sshplex-1.0.4 → sshplex-1.0.10}/MANIFEST.in +0 -0
  16. {sshplex-1.0.4 → sshplex-1.0.10}/setup.cfg +0 -0
  17. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/__init__.py +0 -0
  18. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/logger.py +0 -0
  19. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/multiplexer/__init__.py +0 -0
  20. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/multiplexer/base.py +0 -0
  21. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/multiplexer/tmux.py +0 -0
  22. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/sot/__init__.py +0 -0
  23. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/sot/base.py +0 -0
  24. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/ssh/__init__.py +0 -0
  25. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/ssh/connection.py +0 -0
  26. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/ssh/manager.py +0 -0
  27. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/ui/__init__.py +0 -0
  28. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/ui/host_selector.py +0 -0
  29. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/lib/ui/session_manager.py +0 -0
  30. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex/sshplex_connector.py +0 -0
  31. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex.egg-info/dependency_links.txt +0 -0
  32. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex.egg-info/entry_points.txt +0 -0
  33. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex.egg-info/requires.txt +0 -0
  34. {sshplex-1.0.4 → sshplex-1.0.10}/sshplex.egg-info/top_level.txt +0 -0
  35. {sshplex-1.0.4 → sshplex-1.0.10}/tests/test_config.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sshplex
3
- Version: 1.0.4
3
+ Version: 1.0.10
4
4
  Summary: Multiplex your SSH connections with style
5
5
  Author-email: MJAHED Sabri <contact@sabrimjahed.com>
6
6
  License: MIT
@@ -44,7 +44,7 @@ Requires-Dist: mypy>=1.0.0; extra == "dev"
44
44
  Requires-Dist: types-PyYAML>=6.0.0; extra == "dev"
45
45
  Dynamic: license-file
46
46
 
47
- # SSHplex
47
+ ![logo.png](https://raw.githubusercontent.com/sabrimjd/sshplex/master/.github/images/logo.png)
48
48
 
49
49
  **Multiplex your SSH connections with style**
50
50
 
@@ -1,4 +1,4 @@
1
- # SSHplex
1
+ ![logo.png](https://raw.githubusercontent.com/sabrimjd/sshplex/master/.github/images/logo.png)
2
2
 
3
3
  **Multiplex your SSH connections with style**
4
4
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sshplex"
7
- version = "1.0.4"
7
+ version = "1.0.10"
8
8
  description = "Multiplex your SSH connections with style"
9
9
  authors = [{name = "MJAHED Sabri", email = "contact@sabrimjahed.com"}]
10
10
  readme = "README.md"
@@ -1,4 +1,4 @@
1
1
  """SSHplex - SSH Connection Multiplexer"""
2
- __version__ = "1.0.4"
2
+ __version__ = "1.0.11"
3
3
  __author__ = "MJAHED Sabri"
4
4
  __email__ = "contact@sabrimjahed.com"
@@ -4,6 +4,7 @@ import sys
4
4
  import argparse
5
5
  from typing import Any
6
6
 
7
+ from . import __version__
7
8
  from .lib.config import load_config
8
9
  from .lib.logger import setup_logging, get_logger
9
10
  from .lib.sot.netbox import NetBoxProvider
@@ -16,7 +17,7 @@ def main() -> int:
16
17
  # Parse command line arguments
17
18
  parser = argparse.ArgumentParser(description="SSHplex CLI: Debug interface for NetBox connectivity testing.")
18
19
  parser.add_argument('--config', type=str, default=None, help='Path to the configuration file (default: ~/.config/sshplex/sshplex.yaml)')
19
- parser.add_argument('--version', action='version', version='SSHplex 1.0.4')
20
+ parser.add_argument('--version', action='version', version=f'SSHplex {__version__}')
20
21
  args = parser.parse_args()
21
22
 
22
23
  # Load configuration (will use default path if none specified)
@@ -1,6 +1,5 @@
1
1
  # SSHplex Configuration - tmux support
2
2
  sshplex:
3
- version: "1.0.4"
4
3
  session_prefix: "sshplex"
5
4
 
6
5
  netbox:
@@ -7,10 +7,12 @@ import shutil
7
7
  import os
8
8
  from pydantic import BaseModel, Field, validator
9
9
 
10
+ from .. import __version__
11
+
10
12
 
11
13
  class SSHplexConfig(BaseModel):
12
14
  """SSHplex main configuration."""
13
- version: str = "1.0.4"
15
+ version: str = __version__
14
16
  session_prefix: str = "sshplex"
15
17
 
16
18
 
@@ -0,0 +1,278 @@
1
+ """NetBox Source of Truth provider for SSHplex."""
2
+
3
+ import pynetbox # type: ignore
4
+ from typing import List, Dict, Any, Optional
5
+ from ..logger import get_logger
6
+ from .base import SoTProvider, Host
7
+
8
+
9
+ class NetBoxProvider(SoTProvider):
10
+ """NetBox implementation of SoT provider."""
11
+
12
+ def __init__(self, url: str, token: str, verify_ssl: bool = True, timeout: int = 30) -> None:
13
+ """Initialize NetBox provider.
14
+
15
+ Args:
16
+ url: NetBox instance URL
17
+ token: API token for authentication
18
+ verify_ssl: Whether to verify SSL certificates
19
+ timeout: Request timeout in seconds
20
+ """
21
+ self.url = url
22
+ self.token = token
23
+ self.verify_ssl = verify_ssl
24
+ self.timeout = timeout
25
+ self.api: Optional[Any] = None
26
+ self.logger = get_logger()
27
+
28
+ def connect(self) -> bool:
29
+ """Establish connection to NetBox API.
30
+
31
+ Returns:
32
+ True if connection successful, False otherwise
33
+ """
34
+ try:
35
+ self.logger.info(f"Connecting to NetBox at {self.url}")
36
+
37
+ self.api = pynetbox.api(
38
+ url=self.url,
39
+ token=self.token
40
+ )
41
+
42
+ # Configure SSL verification and timeout
43
+ if self.api is not None:
44
+ self.api.http_session.verify = self.verify_ssl
45
+ self.api.http_session.timeout = self.timeout
46
+
47
+ # Log SSL verification status
48
+ if not self.verify_ssl:
49
+ self.logger.warning("SSL certificate verification is DISABLED")
50
+ try:
51
+ import urllib3 # type: ignore
52
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
53
+ except ImportError:
54
+ pass # urllib3 not available, continue anyway
55
+
56
+ # Test the connection
57
+ if self.test_connection():
58
+ self.logger.info("Successfully connected to NetBox")
59
+ return True
60
+ else:
61
+ self.logger.error("Failed to establish NetBox connection")
62
+ return False
63
+
64
+ except Exception as e:
65
+ self.logger.error(f"NetBox connection failed: {e}")
66
+ return False
67
+
68
+ def test_connection(self) -> bool:
69
+ """Test connection to NetBox API.
70
+
71
+ Returns:
72
+ True if connection is healthy, False otherwise
73
+ """
74
+ try:
75
+ if not self.api:
76
+ return False
77
+
78
+ # Try to get NetBox status
79
+ status = self.api.status()
80
+ self.logger.debug(f"NetBox status: {status}")
81
+ return True
82
+
83
+ except Exception as e:
84
+ self.logger.error(f"NetBox connection test failed: {e}")
85
+ return False
86
+
87
+ def get_hosts(self, filters: Optional[Dict[str, Any]] = None) -> List[Host]:
88
+ """Retrieve virtual machines and physical devices from NetBox.
89
+
90
+ Args:
91
+ filters: Optional filters to apply (status, role, etc.)
92
+
93
+ Returns:
94
+ List of Host objects
95
+ """
96
+ if not self.api:
97
+ self.logger.error("NetBox API not connected. Call connect() first.")
98
+ return []
99
+
100
+ try:
101
+ self.logger.info("Retrieving VMs and devices from NetBox")
102
+
103
+ # Build filter parameters
104
+ filter_params = {}
105
+ if filters:
106
+ filter_params.update(filters)
107
+ self.logger.info(f"Applying filters: {filter_params}")
108
+
109
+ hosts = []
110
+ vm_count = 0
111
+ device_count = 0
112
+
113
+ # Get virtual machines
114
+ self.logger.info("Querying virtual machines...")
115
+ vms = list(self.api.virtualization.virtual_machines.filter(**filter_params))
116
+ self.logger.info(f"Found {len(vms)} virtual machines")
117
+
118
+ for vm in vms:
119
+ host = self._process_vm(vm)
120
+ if host:
121
+ hosts.append(host)
122
+ vm_count += 1
123
+
124
+ # Get physical devices
125
+ self.logger.info("Querying physical devices...")
126
+ devices = list(self.api.dcim.devices.filter(**filter_params))
127
+ self.logger.info(f"Found {len(devices)} physical devices")
128
+
129
+ for device in devices:
130
+ host = self._process_device(device)
131
+ if host:
132
+ hosts.append(host)
133
+ device_count += 1
134
+
135
+ self.logger.info(f"Retrieved {len(hosts)} total hosts from NetBox ({vm_count} VMs, {device_count} devices)")
136
+ return hosts
137
+
138
+ except Exception as e:
139
+ self.logger.error(f"Failed to retrieve hosts from NetBox: {e}")
140
+ return []
141
+
142
+ def _get_primary_ip(self, vm: Any) -> Optional[str]:
143
+ """Extract primary IP address from VM object.
144
+
145
+ Args:
146
+ vm: NetBox VM object
147
+
148
+ Returns:
149
+ Primary IP address as string, or None if not found
150
+ """
151
+ try:
152
+ if vm.primary_ip4:
153
+ # Remove CIDR notation if present
154
+ ip = str(vm.primary_ip4).split('/')[0]
155
+ return ip
156
+ elif vm.primary_ip6:
157
+ # Use IPv6 if no IPv4
158
+ ip = str(vm.primary_ip6).split('/')[0]
159
+ return ip
160
+ else:
161
+ return None
162
+ except Exception as e:
163
+ self.logger.debug(f"Error extracting IP for VM {vm.name}: {e}")
164
+ return None
165
+
166
+ def _process_vm(self, vm: Any) -> Optional[Host]:
167
+ """Process a virtual machine object into a Host.
168
+
169
+ Args:
170
+ vm: NetBox VM object
171
+
172
+ Returns:
173
+ Host object or None if processing fails
174
+ """
175
+ try:
176
+ # Extract VM details
177
+ name = vm.name
178
+ ip = self._get_primary_ip(vm)
179
+
180
+ if not ip:
181
+ self.logger.warning(f"VM {name} has no primary IP, skipping")
182
+ return None
183
+
184
+ # Get tags as a comma-separated string
185
+ tags = ""
186
+ if hasattr(vm, 'tags') and vm.tags:
187
+ try:
188
+ tags = ", ".join([str(tag) for tag in vm.tags])
189
+ except Exception as e:
190
+ self.logger.debug(f"Error processing tags for VM {name}: {e}")
191
+
192
+ # Create host with metadata
193
+ host = Host(
194
+ name=name,
195
+ ip=ip,
196
+ status=str(vm.status) if vm.status else "unknown",
197
+ role=str(vm.role) if vm.role else "unknown",
198
+ platform="vm", # Mark as virtual machine
199
+ cluster=str(vm.cluster) if vm.cluster else "unknown",
200
+ tags=tags,
201
+ description=str(vm.description) if vm.description else ""
202
+ )
203
+
204
+ self.logger.debug(f"Added VM host: {host}")
205
+ return host
206
+
207
+ except Exception as e:
208
+ self.logger.error(f"Error processing VM {vm.name}: {e}")
209
+ return None
210
+
211
+ def _process_device(self, device: Any) -> Optional[Host]:
212
+ """Process a physical device object into a Host.
213
+
214
+ Args:
215
+ device: NetBox device object
216
+
217
+ Returns:
218
+ Host object or None if processing fails
219
+ """
220
+ try:
221
+ # Extract device details
222
+ name = device.name
223
+ ip = self._get_device_primary_ip(device)
224
+
225
+ if not ip:
226
+ self.logger.warning(f"Device {name} has no primary IP, skipping")
227
+ return None
228
+
229
+ # Get tags as a comma-separated string
230
+ tags = ""
231
+ if hasattr(device, 'tags') and device.tags:
232
+ try:
233
+ tags = ", ".join([str(tag) for tag in device.tags])
234
+ except Exception as e:
235
+ self.logger.debug(f"Error processing tags for device {name}: {e}")
236
+
237
+ # Create host with metadata
238
+ host = Host(
239
+ name=name,
240
+ ip=ip,
241
+ status=str(device.status) if device.status else "unknown",
242
+ role=str(device.role) if device.role else "unknown",
243
+ platform=str(device.platform) if device.platform else "device",
244
+ cluster=str(device.rack) if device.rack else "unknown", # Use rack as cluster for devices
245
+ tags=tags,
246
+ description=str(device.comments) if device.comments else ""
247
+ )
248
+
249
+ self.logger.debug(f"Added device host: {host}")
250
+ return host
251
+
252
+ except Exception as e:
253
+ self.logger.error(f"Error processing device {device.name}: {e}")
254
+ return None
255
+
256
+ def _get_device_primary_ip(self, device: Any) -> Optional[str]:
257
+ """Extract primary IP address from device object.
258
+
259
+ Args:
260
+ device: NetBox device object
261
+
262
+ Returns:
263
+ Primary IP address as string, or None if not found
264
+ """
265
+ try:
266
+ if device.primary_ip4:
267
+ # Remove CIDR notation if present
268
+ ip = str(device.primary_ip4).split('/')[0]
269
+ return ip
270
+ elif device.primary_ip6:
271
+ # Use IPv6 if no IPv4
272
+ ip = str(device.primary_ip6).split('/')[0]
273
+ return ip
274
+ else:
275
+ return None
276
+ except Exception as e:
277
+ self.logger.debug(f"Error extracting IP for device {device.name}: {e}")
278
+ return None
@@ -7,6 +7,7 @@ from pathlib import Path
7
7
  from datetime import datetime
8
8
  from typing import Any
9
9
 
10
+ from . import __version__
10
11
  from .lib.config import load_config
11
12
  from .lib.logger import setup_logging, get_logger
12
13
  from .lib.sot.netbox import NetBoxProvider
@@ -41,7 +42,7 @@ def main() -> int:
41
42
  # Parse command line arguments
42
43
  parser = argparse.ArgumentParser(description="SSHplex: Multiplex your SSH connections with style.")
43
44
  parser.add_argument('--config', type=str, default=None, help='Path to the configuration file (default: ~/.config/sshplex/sshplex.yaml)')
44
- parser.add_argument('--version', action='version', version='SSHplex 1.0.4')
45
+ parser.add_argument('--version', action='version', version=f'SSHplex {__version__}')
45
46
  parser.add_argument('--debug', action='store_true', help='Run in debug mode (CLI only, no TUI)')
46
47
  args = parser.parse_args()
47
48
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sshplex
3
- Version: 1.0.4
3
+ Version: 1.0.10
4
4
  Summary: Multiplex your SSH connections with style
5
5
  Author-email: MJAHED Sabri <contact@sabrimjahed.com>
6
6
  License: MIT
@@ -44,7 +44,7 @@ Requires-Dist: mypy>=1.0.0; extra == "dev"
44
44
  Requires-Dist: types-PyYAML>=6.0.0; extra == "dev"
45
45
  Dynamic: license-file
46
46
 
47
- # SSHplex
47
+ ![logo.png](https://raw.githubusercontent.com/sabrimjd/sshplex/master/.github/images/logo.png)
48
48
 
49
49
  **Multiplex your SSH connections with style**
50
50
 
@@ -6,7 +6,6 @@ sshplex/__init__.py
6
6
  sshplex/cli.py
7
7
  sshplex/config-template.yaml
8
8
  sshplex/main.py
9
- sshplex/populate_examples.py
10
9
  sshplex/sshplex_connector.py
11
10
  sshplex.egg-info/PKG-INFO
12
11
  sshplex.egg-info/SOURCES.txt
@@ -1,174 +0,0 @@
1
- """NetBox Source of Truth provider for SSHplex."""
2
-
3
- import pynetbox
4
- from typing import List, Dict, Any, Optional
5
- from ..logger import get_logger
6
- from .base import SoTProvider, Host
7
-
8
-
9
- class NetBoxProvider(SoTProvider):
10
- """NetBox implementation of SoT provider."""
11
-
12
- def __init__(self, url: str, token: str, verify_ssl: bool = True, timeout: int = 30) -> None:
13
- """Initialize NetBox provider.
14
-
15
- Args:
16
- url: NetBox instance URL
17
- token: API token for authentication
18
- verify_ssl: Whether to verify SSL certificates
19
- timeout: Request timeout in seconds
20
- """
21
- self.url = url
22
- self.token = token
23
- self.verify_ssl = verify_ssl
24
- self.timeout = timeout
25
- self.api: Optional[Any] = None
26
- self.logger = get_logger()
27
-
28
- def connect(self) -> bool:
29
- """Establish connection to NetBox API.
30
-
31
- Returns:
32
- True if connection successful, False otherwise
33
- """
34
- try:
35
- self.logger.info(f"Connecting to NetBox at {self.url}")
36
-
37
- self.api = pynetbox.api(
38
- url=self.url,
39
- token=self.token
40
- )
41
-
42
- # Configure SSL verification and timeout
43
- if self.api is not None:
44
- self.api.http_session.verify = self.verify_ssl
45
- self.api.http_session.timeout = self.timeout
46
-
47
- # Log SSL verification status
48
- if not self.verify_ssl:
49
- self.logger.warning("SSL certificate verification is DISABLED")
50
- try:
51
- import urllib3
52
- urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
53
- except ImportError:
54
- pass # urllib3 not available, continue anyway
55
-
56
- # Test the connection
57
- if self.test_connection():
58
- self.logger.info("Successfully connected to NetBox")
59
- return True
60
- else:
61
- self.logger.error("Failed to establish NetBox connection")
62
- return False
63
-
64
- except Exception as e:
65
- self.logger.error(f"NetBox connection failed: {e}")
66
- return False
67
-
68
- def test_connection(self) -> bool:
69
- """Test connection to NetBox API.
70
-
71
- Returns:
72
- True if connection is healthy, False otherwise
73
- """
74
- try:
75
- if not self.api:
76
- return False
77
-
78
- # Try to get NetBox status
79
- status = self.api.status()
80
- self.logger.debug(f"NetBox status: {status}")
81
- return True
82
-
83
- except Exception as e:
84
- self.logger.error(f"NetBox connection test failed: {e}")
85
- return False
86
-
87
- def get_hosts(self, filters: Optional[Dict[str, Any]] = None) -> List[Host]:
88
- """Retrieve virtual machines from NetBox.
89
-
90
- Args:
91
- filters: Optional filters to apply (status, role, etc.)
92
-
93
- Returns:
94
- List of Host objects
95
- """
96
- if not self.api:
97
- self.logger.error("NetBox API not connected. Call connect() first.")
98
- return []
99
-
100
- try:
101
- self.logger.info("Retrieving VMs from NetBox")
102
-
103
- # Build filter parameters
104
- filter_params = {}
105
- if filters:
106
- filter_params.update(filters)
107
- self.logger.info(f"Applying filters: {filter_params}")
108
-
109
- # Get virtual machines
110
- vms = list(self.api.virtualization.virtual_machines.filter(**filter_params))
111
-
112
- hosts = []
113
- for vm in vms:
114
- # Extract VM details
115
- name = vm.name
116
- ip = self._get_primary_ip(vm)
117
-
118
- if not ip:
119
- self.logger.warning(f"VM {name} has no primary IP, skipping")
120
- continue
121
-
122
- # Get tags as a comma-separated string
123
- tags = ""
124
- if hasattr(vm, 'tags') and vm.tags:
125
- try:
126
- tags = ", ".join([str(tag) for tag in vm.tags])
127
- except Exception as e:
128
- self.logger.debug(f"Error processing tags for VM {name}: {e}")
129
-
130
- # Create host with metadata
131
- host = Host(
132
- name=name,
133
- ip=ip,
134
- status=str(vm.status) if vm.status else "unknown",
135
- role=str(vm.role) if vm.role else "unknown",
136
- platform=str(vm.platform) if vm.platform else "unknown",
137
- cluster=str(vm.cluster) if vm.cluster else "unknown",
138
- tags=tags,
139
- description=str(vm.description) if vm.description else ""
140
- )
141
-
142
- hosts.append(host)
143
- self.logger.debug(f"Added host: {host}")
144
-
145
- self.logger.info(f"Retrieved {len(hosts)} VMs from NetBox")
146
- return hosts
147
-
148
- except Exception as e:
149
- self.logger.error(f"Failed to retrieve VMs from NetBox: {e}")
150
- return []
151
-
152
- def _get_primary_ip(self, vm: Any) -> Optional[str]:
153
- """Extract primary IP address from VM object.
154
-
155
- Args:
156
- vm: NetBox VM object
157
-
158
- Returns:
159
- Primary IP address as string, or None if not found
160
- """
161
- try:
162
- if vm.primary_ip4:
163
- # Remove CIDR notation if present
164
- ip = str(vm.primary_ip4).split('/')[0]
165
- return ip
166
- elif vm.primary_ip6:
167
- # Use IPv6 if no IPv4
168
- ip = str(vm.primary_ip6).split('/')[0]
169
- return ip
170
- else:
171
- return None
172
- except Exception as e:
173
- self.logger.debug(f"Error extracting IP for VM {vm.name}: {e}")
174
- return None
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes