Skip to content

Commit 60272c4

Browse files
committed
Add update command and status check to CLI; enhance Director for update messaging
1 parent 9cf3851 commit 60272c4

File tree

6 files changed

+116
-4
lines changed

6 files changed

+116
-4
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased]
9+
### Added
10+
- Add `iop --update` command to update a production from the command line
11+
- New info displayed in `iop --help` command showing current namespace
12+
- `iop --status` command now shows if production needs update, if so a message is displayed
13+
914
### Fixed
1015
- Fix issue with boolean `response_required` parameter in `send_request_async` method of BusinessProcess
1116
- Now converts boolean to integer (1 or 0) to ensure compatibility with IRIS API

src/iop/_cli.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class CommandType(Enum):
2828
LOG = auto()
2929
INIT = auto()
3030
HELP = auto()
31+
UPDATE = auto()
3132

3233
@dataclass
3334
class CommandArgs:
@@ -50,6 +51,7 @@ class CommandArgs:
5051
body: Optional[str] = None
5152
namespace: Optional[str] = None
5253
force_local: bool = False
54+
update: bool = False
5355

5456
class Command:
5557
def __init__(self, args: CommandArgs):
@@ -75,7 +77,8 @@ def execute(self) -> None:
7577
CommandType.MIGRATE: self._handle_migrate,
7678
CommandType.LOG: self._handle_log,
7779
CommandType.INIT: self._handle_init,
78-
CommandType.HELP: self._handle_help
80+
CommandType.HELP: self._handle_help,
81+
CommandType.UPDATE: self._handle_update,
7982
}
8083
handler = command_handlers.get(command_type)
8184
if handler:
@@ -95,6 +98,7 @@ def _determine_command_type(self) -> CommandType:
9598
if self.args.migrate: return CommandType.MIGRATE
9699
if self.args.log: return CommandType.LOG
97100
if self.args.init: return CommandType.INIT
101+
if self.args.update: return CommandType.UPDATE
98102
return CommandType.HELP
99103

100104
def _handle_default(self) -> None:
@@ -128,6 +132,9 @@ def _handle_restart(self) -> None:
128132
def _handle_status(self) -> None:
129133
print(json.dumps(_Director.status_production(), indent=4))
130134

135+
def _handle_update(self) -> None:
136+
_Director.update_production()
137+
131138
def _handle_test(self) -> None:
132139
test_name = None if self.args.test == 'not_set' else self.args.test
133140
response = _Director.test_component(
@@ -164,6 +171,7 @@ def _handle_help(self) -> None:
164171
create_parser().print_help()
165172
try:
166173
print(f"\nDefault production: {_Director.get_default_production()}")
174+
print(f"\nNamespace: {os.getenv('IRISNAMESPACE','not set')}")
167175
except Exception:
168176
logging.warning("Could not retrieve default production.")
169177

@@ -186,6 +194,7 @@ def create_parser() -> argparse.ArgumentParser:
186194
parser.add_argument('-L', '--log', help='display log', nargs='?', const='not_set')
187195
parser.add_argument('-i', '--init', help='init the pex module in iris', nargs='?', const='not_set')
188196
parser.add_argument('-t', '--test', help='test the pex module in iris', nargs='?', const='not_set')
197+
parser.add_argument('-u', '--update', help='update a production', action='store_true')
189198

190199
# Command groups
191200
start = main_parser.add_argument_group('start arguments')

src/iop/cls/IOP/Director.cls

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ ClassMethod StatusProduction() As %String
5656
$$$eProductionStateSuspended:"suspended",
5757
$$$eProductionStateTroubled:"toubled",
5858
:"unknown"))
59+
// Get if production needs update
60+
set tNeedsUpdate = ##class(Ens.Director).ProductionNeedsUpdate(.tMsgUpdate)
61+
do tInfo."__setitem__"("NeedsUpdate",tNeedsUpdate)
62+
// If needs update get message
63+
if tNeedsUpdate {
64+
do tInfo."__setitem__"("UpdateMessage",tMsgUpdate)
65+
}
66+
// Get last start time
5967
Return tInfo
6068
}
6169

src/tests/test_cli.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ def test_help_and_basic_commands(self):
1717
self.assertEqual(cm.exception.code, 0)
1818

1919
# Test without arguments
20-
with self.assertRaises(SystemExit) as cm:
21-
main([])
22-
self.assertEqual(cm.exception.code, 0)
20+
with patch('sys.stdout', new=StringIO()) as fake_out:
21+
with self.assertRaises(SystemExit) as cm:
22+
main([])
23+
self.assertEqual(cm.exception.code, 0)
24+
self.assertIn('Namespace:', fake_out.getvalue())
2325

2426
def test_default_settings(self):
2527
"""Test default production settings."""
@@ -95,6 +97,25 @@ def test_migration(self):
9597
self.assertEqual(cm.exception.code, 0)
9698
mock_migrate.assert_called_once_with('/tmp/settings.json', force_local=True)
9799

100+
def test_status_and_update(self):
101+
"""Test status and update commands."""
102+
# Test status
103+
with patch('iop._director._Director.status_production') as mock_status:
104+
mock_status.return_value = {"Production": "TestProd", "Status": "running"}
105+
with patch('sys.stdout', new=StringIO()) as fake_out:
106+
with self.assertRaises(SystemExit) as cm:
107+
main(['--status'])
108+
self.assertEqual(cm.exception.code, 0)
109+
mock_status.assert_called_once()
110+
self.assertIn('"Production": "TestProd"', fake_out.getvalue())
111+
112+
# Test update
113+
with patch('iop._director._Director.update_production') as mock_update:
114+
with self.assertRaises(SystemExit) as cm:
115+
main(['--update'])
116+
self.assertEqual(cm.exception.code, 0)
117+
mock_update.assert_called_once()
118+
98119
def test_initialization(self):
99120
"""Test initialization command."""
100121
with patch('iop._utils._Utils.setup') as mock_setup:

src/tests/test_director.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,17 @@ def test_status_production(self):
123123
result = _Director.status_production()
124124
assert result == mock_status
125125

126+
def test_status_production_needs_update(self):
127+
mock_status = {
128+
'Production': 'test_prod',
129+
'Status': 'running',
130+
'NeedsUpdate': True,
131+
'UpdateMessage': 'Update available'
132+
}
133+
iris.cls('IOP.Director').StatusProduction = MagicMock(return_value=mock_status)
134+
result = _Director.status_production()
135+
assert result == mock_status
136+
126137
class TestLogging:
127138
def test_format_log(self):
128139
test_row = [1, 'Config1', 'Job1', 'Msg1', 'Session1', 'Source1', 'Method1',

src/tests/test_utils.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,61 @@ def test_register_schema(self):
140140
with patch('iris.cls') as mock_cls:
141141
_Utils.register_schema("test.schema", "{}", "test")
142142
mock_cls.return_value.Import.assert_called_once()
143+
144+
145+
class TestRemoteMigration:
146+
@patch('requests.put')
147+
@patch('iop._utils._Utils._load_settings')
148+
@patch('os.walk')
149+
def test_migrate_remote_verify_ssl_true(self, mock_walk, mock_load_settings, mock_put):
150+
# Setup mocks
151+
mock_load_settings.return_value = (
152+
MagicMock(
153+
REMOTE_SETTINGS = {
154+
'url': 'http://test.com',
155+
'verify_ssl': True,
156+
'username': 'user',
157+
'password': 'password',
158+
'namespace': 'USER',
159+
'remote_folder': '/remote'
160+
}
161+
),
162+
'/path/to/sys'
163+
)
164+
mock_walk.return_value = [] # No files
165+
mock_put.return_value.status_code = 200
166+
mock_put.return_value.text = '{"status": "OK"}'
167+
168+
# Run
169+
_Utils.migrate_remote('settings.py')
170+
171+
# Assert verify=True
172+
mock_put.assert_called_once()
173+
assert mock_put.call_args[1]['verify'] == True
174+
175+
@patch('requests.put')
176+
@patch('iop._utils._Utils._load_settings')
177+
@patch('os.walk')
178+
def test_migrate_remote_verify_ssl_false(self, mock_walk, mock_load_settings, mock_put):
179+
# Setup mocks
180+
mock_load_settings.return_value = (
181+
MagicMock(
182+
REMOTE_SETTINGS = {
183+
'url': 'http://test.com',
184+
'verify_ssl': False,
185+
'username': 'user',
186+
'password': 'password'
187+
}
188+
),
189+
'/path/to/sys'
190+
)
191+
mock_walk.return_value = []
192+
mock_put.return_value.status_code = 200
193+
mock_put.return_value.text = '{"status": "OK"}'
194+
195+
# Run
196+
_Utils.migrate_remote('settings.py')
197+
198+
# Assert verify=False
199+
mock_put.assert_called_once()
200+
assert mock_put.call_args[1]['verify'] == False

0 commit comments

Comments
 (0)