Skip to content

Commit 2a44d99

Browse files
committed
Add support for SSL verification and local migration flag
- Introduced `verify_ssl` option in remote settings to control SSL certificate verification. - Updated `migrate_remote` method to accept `force_local` flag, allowing local migration to be forced. - Enhanced documentation for using HTTPS with self-signed certificates. - Added tests for the new `force-local` migration functionality.
1 parent 7b0c808 commit 2a44d99

File tree

5 files changed

+85
-20
lines changed

5 files changed

+85
-20
lines changed

docs/getting-started/register-component.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ The dictionary has the following structure:
338338
- `namespace`: The namespace for the connection (optional, default is "USER")
339339
- `remote_folder`: The folder where the components are stored (optional, default is the routine database folder)
340340
- `package`: The package name for the components (optional, default is "python")
341+
- `verify_ssl`: Whether to verify SSL certificates (optional, default is True). Set to False for self-signed certificates
341342

342343
Example:
343344
```python
@@ -355,4 +356,30 @@ CLASSES = {
355356
}
356357
```
357358

358-
This will import `FileOperation` from the `bo` module and register it under the key `'Python.FileOperation'` in the `CLASSES` dictionary.
359+
This will import `FileOperation` from the `bo` module and register it under the key `'Python.FileOperation'` in the `CLASSES` dictionary.
360+
361+
#### Using HTTPS with Self-Signed Certificates
362+
363+
If your remote IRIS instance uses HTTPS with a self-signed certificate, you need to disable SSL verification:
364+
365+
```python
366+
REMOTE_SETTINGS = {
367+
"url": "https://localhost:8443",
368+
"username": "SuperUser",
369+
"password": "SYS",
370+
"namespace": "IRISAPP",
371+
"verify_ssl": False # Disable SSL verification for self-signed certificates
372+
}
373+
```
374+
375+
**Note:** Disabling SSL verification should only be used in development or trusted environments, as it makes the connection vulnerable to man-in-the-middle attacks.
376+
377+
#### Force Local Migration
378+
379+
You can force local migration (skip remote migration even if `REMOTE_SETTINGS` is present) by using the `--force-local` flag:
380+
381+
```bash
382+
iop -m /path/to/settings.py --force-local
383+
```
384+
385+
This is useful when you want to test local migration while keeping your `REMOTE_SETTINGS` configuration in the settings file.

src/iop/_cli.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class CommandArgs:
4848
classname: Optional[str] = None
4949
body: Optional[str] = None
5050
namespace: Optional[str] = None
51+
force_local: bool = False
5152

5253
class Command:
5354
def __init__(self, args: CommandArgs):
@@ -147,7 +148,7 @@ def _handle_migrate(self) -> None:
147148
if migrate_path is not None:
148149
if not os.path.isabs(migrate_path):
149150
migrate_path = os.path.join(os.getcwd(), migrate_path)
150-
_Utils.migrate_remote(migrate_path)
151+
_Utils.migrate_remote(migrate_path, force_local=self.args.force_local)
151152

152153
def _handle_log(self) -> None:
153154
if self.args.log == 'not_set':
@@ -190,6 +191,9 @@ def create_parser() -> argparse.ArgumentParser:
190191
test.add_argument('-C', '--classname', help='test classname', nargs='?', const='not_set')
191192
test.add_argument('-B', '--body', help='test body', nargs='?', const='not_set')
192193

194+
migrate = main_parser.add_argument_group('migrate arguments')
195+
migrate.add_argument('--force-local', help='force local migration, skip remote even if REMOTE_SETTINGS is present', action='store_true')
196+
193197
namespace = main_parser.add_argument_group('namespace arguments')
194198
namespace.add_argument('-n', '--namespace', help='set namespace', nargs='?', const='not_set')
195199

src/iop/_utils.py

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class RemoteSettings(TypedDict, total=False):
2323
remote_folder: str # Optional: the folder to use (default: '')
2424
username: str # Optional: the username to use to connect (default: '')
2525
password: str # Optional: the password to use to connect (default: '')
26+
verify_ssl: bool # Optional: verify SSL certificates (default: True, set to False for self-signed certs)
2627

2728
class _Utils():
2829
@staticmethod
@@ -261,7 +262,7 @@ def filename_to_module(filename) -> str:
261262
return module
262263

263264
@staticmethod
264-
def migrate_remote(filename=None):
265+
def migrate_remote(filename=None, force_local=False):
265266
"""
266267
Read a settings file from the filename
267268
If the settings.py file has a key 'REMOTE_SETTINGS' then it will use the value of that key
@@ -273,6 +274,7 @@ def migrate_remote(filename=None):
273274
* 'remote_folder': the folder to use (optional, default is '')
274275
* 'username': the username to use to connect (optional, default is '')
275276
* 'password': the password to use to connect (optional, default is '')
277+
* 'verify_ssl': verify SSL certificates (optional, default is True)
276278
277279
The remote host is a rest API that will be used to register the components
278280
The payload will be a json object with the following keys:
@@ -283,11 +285,15 @@ def migrate_remote(filename=None):
283285
* 'data': the data of the file, it will be an UTF-8 encoded string
284286
285287
'body' will be constructed with all the files in the folder if the folder is not empty else use root folder of settings.py
288+
289+
Args:
290+
filename: Path to the settings file
291+
force_local: If True, skip remote migration even if REMOTE_SETTINGS is present
286292
"""
287293
settings, path = _Utils._load_settings(filename)
288294
remote_settings: Optional[RemoteSettings] = getattr(settings, 'REMOTE_SETTINGS', None) if settings else None
289295

290-
if not remote_settings:
296+
if not remote_settings or force_local:
291297
_Utils.migrate(filename)
292298
return
293299

@@ -321,21 +327,35 @@ def migrate_remote(filename=None):
321327
'data': data
322328
})
323329

330+
# Get SSL verification setting (default to True for security)
331+
verify_ssl = remote_settings.get('verify_ssl', True)
332+
333+
# Disable SSL warnings if verify_ssl is False
334+
if not verify_ssl:
335+
import urllib3
336+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
337+
324338
# send the request to the remote settings
325-
response = requests.put(
326-
url=f"{remote_settings['url']}/api/iop/migrate",
327-
json=payload,
328-
headers={
329-
'Content-Type': 'application/json',
330-
'Accept': 'application/json'
331-
},
332-
auth=(remote_settings.get('username', ''), remote_settings.get('password', '')),
333-
timeout=10
334-
)
335-
336-
print(f"Response from remote migration:\n{response.text}")
337-
338-
response.raise_for_status() # Raise an error for bad responses
339+
try:
340+
response = requests.put(
341+
url=f"{remote_settings['url']}/api/iop/migrate",
342+
json=payload,
343+
headers={
344+
'Content-Type': 'application/json',
345+
'Accept': 'application/json'
346+
},
347+
auth=(remote_settings.get('username', ''), remote_settings.get('password', '')),
348+
timeout=10,
349+
verify=verify_ssl
350+
)
351+
352+
print(f"Response from remote migration:\n{response.text}")
353+
354+
response.raise_for_status() # Raise an error for bad responses
355+
except requests.exceptions.SSLError as e:
356+
print(f"SSL Error: {e}")
357+
print("If you're using a self-signed certificate, set 'verify_ssl': False in REMOTE_SETTINGS")
358+
raise
339359

340360
@staticmethod
341361
def migrate(filename=None):

src/tests/bench/bench_bo.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,10 @@ class BenchIoPOperation(BusinessOperation):
88
def on_message(self, request):
99
time.sleep(0.001) # Simulate some processing delay
1010
return request
11+
12+
if __name__ == "__main__":
13+
# This block is for testing the operation directly
14+
operation = BenchIoPOperation()
15+
test_request = {"data": "test"}
16+
response = operation.on_message(test_request)
17+
print("Response:", response)

src/tests/test_cli.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,21 @@ def test_migration(self):
7979
with self.assertRaises(SystemExit) as cm:
8080
main(['-m', 'settings.json'])
8181
self.assertEqual(cm.exception.code, 0)
82-
mock_migrate.assert_called_once_with(os.path.join(os.getcwd(), 'settings.json'))
82+
mock_migrate.assert_called_once_with(os.path.join(os.getcwd(), 'settings.json'), force_local=False)
8383

8484
# Test absolute path
8585
with patch('iop._utils._Utils.migrate_remote') as mock_migrate:
8686
with self.assertRaises(SystemExit) as cm:
8787
main(['-m', '/tmp/settings.json'])
8888
self.assertEqual(cm.exception.code, 0)
89-
mock_migrate.assert_called_once_with('/tmp/settings.json')
89+
mock_migrate.assert_called_once_with('/tmp/settings.json', force_local=False)
90+
91+
# Test with force_local flag
92+
with patch('iop._utils._Utils.migrate_remote') as mock_migrate:
93+
with self.assertRaises(SystemExit) as cm:
94+
main(['-m', '/tmp/settings.json', '--force-local'])
95+
self.assertEqual(cm.exception.code, 0)
96+
mock_migrate.assert_called_once_with('/tmp/settings.json', force_local=True)
9097

9198
def test_initialization(self):
9299
"""Test initialization command."""

0 commit comments

Comments
 (0)