osism 0.20250701.0__py3-none-any.whl → 0.20250804.0__py3-none-any.whl

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.
osism/tasks/__init__.py CHANGED
@@ -1,12 +1,10 @@
1
1
  # SPDX-License-Identifier: Apache-2.0
2
2
 
3
- import logging
4
3
  import os
5
4
  import re
6
5
  import subprocess
7
6
 
8
7
  from loguru import logger
9
- from pottery import Redlock
10
8
 
11
9
  from osism import utils
12
10
 
@@ -77,10 +75,8 @@ def run_ansible_in_environment(
77
75
 
78
76
  # NOTE: Consider arguments in the future
79
77
  if locking:
80
- logging.getLogger("redlock").setLevel(logging.WARNING)
81
- lock = Redlock(
78
+ lock = utils.create_redlock(
82
79
  key=f"lock-ansible-{environment}-{role}",
83
- masters={utils.redis},
84
80
  auto_release_time=auto_release_time,
85
81
  )
86
82
 
@@ -195,9 +191,8 @@ def run_command(
195
191
  command_env.update(env)
196
192
 
197
193
  if locking:
198
- lock = Redlock(
194
+ lock = utils.create_redlock(
199
195
  key=f"lock-{command}",
200
- masters={utils.redis},
201
196
  auto_release_time=auto_release_time,
202
197
  )
203
198
 
@@ -243,6 +238,49 @@ def handle_task(t, wait=True, format="log", timeout=3600):
243
238
  f"osism wait --output --live --delay 2 {t.task_id}"
244
239
  )
245
240
  return 1
241
+ except KeyboardInterrupt:
242
+ logger.info(f"\nTask {t.task_id} interrupted by user (CTRL+C)")
243
+
244
+ # Prompt user for task revocation in interactive mode using prompt-toolkit
245
+ try:
246
+ from prompt_toolkit import prompt
247
+
248
+ # Use prompt-toolkit for better UX with yes/no options and default
249
+ response = (
250
+ prompt(
251
+ "Do you want to revoke the running task? [y/N]: ", default="n"
252
+ )
253
+ .strip()
254
+ .lower()
255
+ )
256
+
257
+ if response in ["y", "yes"]:
258
+ logger.info(f"Revoking task {t.task_id}...")
259
+ if utils.revoke_task(t.task_id):
260
+ logger.info(f"Task {t.task_id} has been revoked")
261
+ else:
262
+ logger.error(f"Failed to revoke task {t.task_id}")
263
+ else:
264
+ logger.info(f"Task {t.task_id} continues running in background")
265
+ logger.info(
266
+ "Use this command to continue waiting for this task: "
267
+ f"osism wait --output --live --delay 2 {t.task_id}"
268
+ )
269
+ except KeyboardInterrupt:
270
+ # Handle second CTRL+C during prompt
271
+ logger.info(f"\nTask {t.task_id} continues running in background")
272
+ logger.info(
273
+ "Use this command to continue waiting for this task: "
274
+ f"osism wait --output --live --delay 2 {t.task_id}"
275
+ )
276
+ except EOFError:
277
+ # Handle EOF (e.g., when input is not available)
278
+ logger.info(f"Task {t.task_id} continues running in background")
279
+ logger.info(
280
+ "Use this command to continue waiting for this task: "
281
+ f"osism wait --output --live --delay 2 {t.task_id}"
282
+ )
283
+ return 1
246
284
 
247
285
  else:
248
286
  if format == "log":
osism/tasks/ansible.py CHANGED
@@ -1,7 +1,6 @@
1
1
  # SPDX-License-Identifier: Apache-2.0
2
2
 
3
3
  from celery import Celery
4
- from pottery import Redlock
5
4
 
6
5
  from osism import settings, utils
7
6
  from osism.tasks import Config, run_ansible_in_environment
@@ -12,9 +11,8 @@ app.config_from_object(Config)
12
11
 
13
12
  @app.on_after_configure.connect
14
13
  def setup_periodic_tasks(sender, **kwargs):
15
- lock = Redlock(
14
+ lock = utils.create_redlock(
16
15
  key="lock_osism_tasks_ansible_setup_periodic_tasks",
17
- masters={utils.redis},
18
16
  )
19
17
  if settings.GATHER_FACTS_SCHEDULE > 0 and lock.acquire(timeout=10):
20
18
  sender.add_periodic_task(
@@ -44,8 +44,8 @@ def sync_netbox(self, force_update=False):
44
44
 
45
45
 
46
46
  @app.task(bind=True, name="osism.tasks.conductor.sync_ironic")
47
- def sync_ironic(self, force_update=False):
48
- _sync_ironic(self.request.id, get_ironic_parameters, force_update)
47
+ def sync_ironic(self, node_name=None, force_update=False):
48
+ _sync_ironic(self.request.id, get_ironic_parameters, node_name, force_update)
49
49
 
50
50
 
51
51
  @app.task(bind=True, name="osism.tasks.conductor.sync_sonic")
@@ -30,7 +30,7 @@ def get_configuration():
30
30
  "image_source"
31
31
  ]
32
32
  if not validators.uuid(image_source) and not validators.url(
33
- image_source
33
+ image_source, simple_host=True
34
34
  ):
35
35
  result = openstack.image_get(image_source)
36
36
  if result:
@@ -46,7 +46,7 @@ def get_configuration():
46
46
  "deploy_kernel"
47
47
  ]
48
48
  if not validators.uuid(deploy_kernel) and not validators.url(
49
- deploy_kernel
49
+ deploy_kernel, simple_host=True
50
50
  ):
51
51
  result = openstack.image_get(deploy_kernel)
52
52
  if result:
@@ -63,7 +63,7 @@ def get_configuration():
63
63
  "deploy_ramdisk"
64
64
  ]
65
65
  if not validators.uuid(deploy_ramdisk) and not validators.url(
66
- deploy_ramdisk
66
+ deploy_ramdisk, simple_host=True
67
67
  ):
68
68
  result = openstack.image_get(deploy_ramdisk)
69
69
  if result:
@@ -3,7 +3,6 @@
3
3
  import json
4
4
 
5
5
  import jinja2
6
- from pottery import Redlock
7
6
 
8
7
  from osism import utils as osism_utils
9
8
  from osism.tasks import netbox, openstack
@@ -128,19 +127,41 @@ def _prepare_node_attributes(device, get_ironic_parameters):
128
127
  return node_attributes
129
128
 
130
129
 
131
- def sync_ironic(request_id, get_ironic_parameters, force_update=False):
132
- osism_utils.push_task_output(
133
- request_id,
134
- "Starting NetBox device synchronisation with ironic\n",
135
- )
130
+ def sync_ironic(request_id, get_ironic_parameters, node_name=None, force_update=False):
131
+ if node_name:
132
+ osism_utils.push_task_output(
133
+ request_id,
134
+ f"Starting NetBox device synchronisation with ironic for node {node_name}\n",
135
+ )
136
+ else:
137
+ osism_utils.push_task_output(
138
+ request_id,
139
+ "Starting NetBox device synchronisation with ironic\n",
140
+ )
136
141
  devices = set()
137
142
  nb_device_query_list = get_nb_device_query_list_ironic()
138
143
  for nb_device_query in nb_device_query_list:
139
144
  devices |= set(netbox.get_devices(**nb_device_query))
140
145
 
146
+ # Filter devices by node_name if specified
147
+ if node_name:
148
+ devices = {dev for dev in devices if dev.name == node_name}
149
+ if not devices:
150
+ osism_utils.push_task_output(
151
+ request_id,
152
+ f"Node {node_name} not found in NetBox\n",
153
+ )
154
+ osism_utils.finish_task_output(request_id, rc=1)
155
+ return
156
+
141
157
  # NOTE: Find nodes in Ironic which are no longer present in NetBox and remove them
142
158
  device_names = {dev.name for dev in devices}
143
159
  nodes = openstack.baremetal_node_list()
160
+
161
+ # Filter nodes by node_name if specified
162
+ if node_name:
163
+ nodes = [node for node in nodes if node["Name"] == node_name]
164
+
144
165
  for node in nodes:
145
166
  osism_utils.push_task_output(
146
167
  request_id, f"Looking for {node['Name']} in NetBox\n"
@@ -180,9 +201,8 @@ def sync_ironic(request_id, get_ironic_parameters, force_update=False):
180
201
  if interface.enabled and not interface.mgmt_only and interface.mac_address
181
202
  ]
182
203
 
183
- lock = Redlock(
204
+ lock = osism_utils.create_redlock(
184
205
  key=f"lock_osism_tasks_conductor_sync_ironic-{device.name}",
185
- masters={osism_utils.redis},
186
206
  auto_release_time=600,
187
207
  )
188
208
  if lock.acquire(timeout=120):
@@ -201,6 +201,9 @@ def generate_sonic_config(device, hwsku, device_as_mapping=None):
201
201
  # Add NTP server configuration (device-specific)
202
202
  _add_ntp_configuration(config, device)
203
203
 
204
+ # Add DNS server configuration (device-specific)
205
+ _add_dns_configuration(config, device)
206
+
204
207
  # Add VLAN configuration
205
208
  _add_vlan_configuration(config, vlan_info, netbox_interfaces, device)
206
209
 
@@ -694,18 +697,20 @@ def _determine_peer_type(local_device, connected_device, device_as_mapping=None)
694
697
  return "external" # Default to external on error
695
698
 
696
699
 
697
- def _get_ntp_server_for_device(device):
698
- """Get single NTP server IP for a SONiC device based on OOB connection to metalbox.
700
+ def _get_metalbox_ip_for_device(device):
701
+ """Get Metalbox IP for a SONiC device based on OOB connection.
699
702
 
700
703
  Returns the IP address of the metalbox device interface that is connected to the
701
704
  OOB switch. If VLANs are used, returns the IP of the VLAN interface where the
702
705
  SONiC switch management interface (eth0) has access.
703
706
 
707
+ This IP is used for both NTP and DNS services.
708
+
704
709
  Args:
705
710
  device: SONiC device object
706
711
 
707
712
  Returns:
708
- str: IP address of the NTP server or None if not found
713
+ str: IP address of the Metalbox or None if not found
709
714
  """
710
715
  try:
711
716
  # Get the OOB IP configuration for this SONiC device
@@ -726,7 +731,7 @@ def _get_ntp_server_for_device(device):
726
731
  metalbox_devices = utils.nb.dcim.devices.filter(role="metalbox")
727
732
 
728
733
  for metalbox in metalbox_devices:
729
- logger.debug(f"Checking metalbox device {metalbox.name} for NTP server")
734
+ logger.debug(f"Checking metalbox device {metalbox.name} for services")
730
735
 
731
736
  # Get all interfaces on this metalbox
732
737
  interfaces = utils.nb.dcim.interfaces.filter(device_id=metalbox.id)
@@ -765,7 +770,7 @@ def _get_ntp_server_for_device(device):
765
770
  else "interface"
766
771
  )
767
772
  logger.info(
768
- f"Found NTP server {ip_only} on metalbox {metalbox.name} "
773
+ f"Found Metalbox {ip_only} on {metalbox.name} "
769
774
  f"{interface_type} {interface.name} for SONiC device {device.name}"
770
775
  )
771
776
  return ip_only
@@ -773,11 +778,11 @@ def _get_ntp_server_for_device(device):
773
778
  # Skip non-IPv4 addresses
774
779
  continue
775
780
 
776
- logger.warning(f"No suitable NTP server found for SONiC device {device.name}")
781
+ logger.warning(f"No suitable Metalbox found for SONiC device {device.name}")
777
782
  return None
778
783
 
779
784
  except Exception as e:
780
- logger.warning(f"Could not determine NTP server for device {device.name}: {e}")
785
+ logger.warning(f"Could not determine Metalbox IP for device {device.name}: {e}")
781
786
  return None
782
787
 
783
788
 
@@ -846,19 +851,17 @@ def _add_ntp_configuration(config, device):
846
851
  metalbox device interface connected to the OOB switch.
847
852
  """
848
853
  try:
849
- # Get the specific NTP server for this device
850
- ntp_server_ip = _get_ntp_server_for_device(device)
854
+ # Get the Metalbox IP for this device
855
+ metalbox_ip = _get_metalbox_ip_for_device(device)
851
856
 
852
- if ntp_server_ip:
857
+ if metalbox_ip:
853
858
  # Add single NTP server configuration
854
- config["NTP_SERVER"][ntp_server_ip] = {
859
+ config["NTP_SERVER"][metalbox_ip] = {
855
860
  "maxpoll": "10",
856
861
  "minpoll": "6",
857
862
  "prefer": "false",
858
863
  }
859
- logger.info(
860
- f"Added NTP server {ntp_server_ip} to SONiC device {device.name}"
861
- )
864
+ logger.info(f"Added NTP server {metalbox_ip} to SONiC device {device.name}")
862
865
  else:
863
866
  logger.warning(f"No NTP server found for SONiC device {device.name}")
864
867
 
@@ -873,6 +876,27 @@ def clear_ntp_cache():
873
876
  logger.debug("Cleared NTP servers cache")
874
877
 
875
878
 
879
+ def _add_dns_configuration(config, device):
880
+ """Add DNS_NAMESERVER configuration to device config.
881
+
882
+ Each SONiC switch gets exactly one DNS server - the IP address of the
883
+ metalbox device interface connected to the OOB switch.
884
+ """
885
+ try:
886
+ # Get the Metalbox IP for this device
887
+ metalbox_ip = _get_metalbox_ip_for_device(device)
888
+
889
+ if metalbox_ip:
890
+ # Add single DNS server configuration
891
+ config["DNS_NAMESERVER"][metalbox_ip] = {}
892
+ logger.info(f"Added DNS server {metalbox_ip} to SONiC device {device.name}")
893
+ else:
894
+ logger.warning(f"No DNS server found for SONiC device {device.name}")
895
+
896
+ except Exception as e:
897
+ logger.warning(f"Could not add DNS configuration to device {device.name}: {e}")
898
+
899
+
876
900
  def clear_all_caches():
877
901
  """Clear all caches in config_generator module."""
878
902
  clear_ntp_cache()
@@ -419,6 +419,16 @@ def _extract_port_number_from_alias(alias):
419
419
  if not alias:
420
420
  return None
421
421
 
422
+ # Try to extract number from Eth54(Port54) format first
423
+ paren_match = re.search(r"Eth(\d+)\(Port(\d+)\)", alias)
424
+ if paren_match:
425
+ port_number = int(paren_match.group(1))
426
+ logger.debug(
427
+ f"Extracted port number {port_number} from Eth(Port) alias '{alias}'"
428
+ )
429
+ return port_number
430
+
431
+ # Fallback to number at end of alias
422
432
  match = re.search(r"(\d+)$", alias)
423
433
  if match:
424
434
  port_number = int(match.group(1))
@@ -75,8 +75,15 @@ def get_vault():
75
75
  )
76
76
  ]
77
77
  )
78
- except Exception:
79
- logger.error("Unable to get vault secret. Dropping encrypted entries")
78
+ except ValueError as exc:
79
+ # Handle specific vault password configuration errors
80
+ logger.error(f"Vault password configuration error: {exc}")
81
+ logger.error("Please check your vault password setup in Redis")
82
+ vault = VaultLib()
83
+ except Exception as exc:
84
+ # Handle other errors (file access, decryption, etc.)
85
+ logger.error(f"Unable to get vault secret: {exc}")
86
+ logger.error("Dropping encrypted entries")
80
87
  vault = VaultLib()
81
88
  return vault
82
89
 
osism/tasks/netbox.py CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  from celery import Celery
4
4
  from loguru import logger
5
- from pottery import Redlock
6
5
 
7
6
  from osism import settings, utils
8
7
  from osism.tasks import Config, run_command
@@ -30,9 +29,8 @@ def run(self, action, arguments):
30
29
  def set_maintenance(self, device_name, state=True):
31
30
  """Set the maintenance state for a device in the NetBox."""
32
31
 
33
- lock = Redlock(
32
+ lock = utils.create_redlock(
34
33
  key=f"lock_osism_tasks_netbox_set_maintenance_{device_name}",
35
- masters={utils.redis},
36
34
  auto_release_time=60,
37
35
  )
38
36
  if lock.acquire(timeout=20):
@@ -59,9 +57,8 @@ def set_maintenance(self, device_name, state=True):
59
57
  def set_provision_state(self, device_name, state):
60
58
  """Set the provision state for a device in the NetBox."""
61
59
 
62
- lock = Redlock(
60
+ lock = utils.create_redlock(
63
61
  key=f"lock_osism_tasks_netbox_set_provision_state_{device_name}",
64
- masters={utils.redis},
65
62
  auto_release_time=60,
66
63
  )
67
64
  if lock.acquire(timeout=20):
@@ -89,9 +86,8 @@ def set_provision_state(self, device_name, state):
89
86
  def set_power_state(self, device_name, state):
90
87
  """Set the provision state for a device in the NetBox."""
91
88
 
92
- lock = Redlock(
89
+ lock = utils.create_redlock(
93
90
  key=f"lock_osism_tasks_netbox_set_provision_state_{device_name}",
94
- masters={utils.redis},
95
91
  auto_release_time=60,
96
92
  )
97
93
  if lock.acquire(timeout=20):
osism/tasks/reconciler.py CHANGED
@@ -6,7 +6,6 @@ import subprocess
6
6
 
7
7
  from celery import Celery
8
8
  from loguru import logger
9
- from pottery import Redlock
10
9
 
11
10
  from osism import settings, utils
12
11
  from osism.tasks import Config
@@ -17,9 +16,8 @@ app.config_from_object(Config)
17
16
 
18
17
  @app.on_after_configure.connect
19
18
  def setup_periodic_tasks(sender, **kwargs):
20
- lock = Redlock(
19
+ lock = utils.create_redlock(
21
20
  key="lock_osism_tasks_reconciler_setup_periodic_tasks",
22
- masters={utils.redis},
23
21
  )
24
22
  if settings.INVENTORY_RECONCILER_SCHEDULE > 0 and lock.acquire(timeout=10):
25
23
  sender.add_periodic_task(
@@ -29,9 +27,8 @@ def setup_periodic_tasks(sender, **kwargs):
29
27
 
30
28
  @app.task(bind=True, name="osism.tasks.reconciler.run")
31
29
  def run(self, publish=True, flush_cache=False):
32
- lock = Redlock(
30
+ lock = utils.create_redlock(
33
31
  key="lock_osism_tasks_reconciler_run",
34
- masters={utils.redis},
35
32
  auto_release_time=60,
36
33
  )
37
34
 
@@ -64,9 +61,8 @@ def run(self, publish=True, flush_cache=False):
64
61
 
65
62
  @app.task(bind=True, name="osism.tasks.reconciler.run_on_change")
66
63
  def run_on_change(self):
67
- lock = Redlock(
64
+ lock = utils.create_redlock(
68
65
  key="lock_osism_tasks_reconciler_run_on_change",
69
- masters={utils.redis},
70
66
  auto_release_time=60,
71
67
  )
72
68
 
osism/utils/__init__.py CHANGED
@@ -2,11 +2,13 @@
2
2
 
3
3
  import time
4
4
  import os
5
+ from contextlib import redirect_stdout, redirect_stderr
5
6
  from cryptography.fernet import Fernet
6
7
  import keystoneauth1
7
8
  from loguru import logger
8
9
  import openstack
9
10
  import pynetbox
11
+ from pottery import Redlock
10
12
  from redis import Redis
11
13
  import urllib3
12
14
  import yaml
@@ -107,8 +109,18 @@ def get_ansible_vault_password():
107
109
  f = Fernet(key)
108
110
 
109
111
  encrypted_ansible_vault_password = redis.get("ansible_vault_password")
112
+ if encrypted_ansible_vault_password is None:
113
+ raise ValueError("Ansible vault password is not set in Redis")
114
+
110
115
  ansible_vault_password = f.decrypt(encrypted_ansible_vault_password)
111
- return ansible_vault_password.decode("utf-8")
116
+ password = ansible_vault_password.decode("utf-8")
117
+
118
+ if not password or password.strip() == "":
119
+ raise ValueError(
120
+ "Ansible vault password is empty or contains only whitespace"
121
+ )
122
+
123
+ return password
112
124
  except Exception as exc:
113
125
  logger.error("Unable to get ansible vault password")
114
126
  raise exc
@@ -181,3 +193,52 @@ def finish_task_output(task_id, rc=None):
181
193
  if rc:
182
194
  redis.xadd(task_id, {"type": "rc", "content": rc})
183
195
  redis.xadd(task_id, {"type": "action", "content": "quit"})
196
+
197
+
198
+ def revoke_task(task_id):
199
+ """
200
+ Revoke a running Celery task.
201
+
202
+ Args:
203
+ task_id (str): The ID of the task to revoke
204
+
205
+ Returns:
206
+ bool: True if revocation was successful, False otherwise
207
+ """
208
+ try:
209
+ from celery import Celery
210
+ from osism.tasks import Config
211
+
212
+ app = Celery("task")
213
+ app.config_from_object(Config)
214
+ app.control.revoke(task_id, terminate=True)
215
+ return True
216
+ except Exception as e:
217
+ logger.error(f"Failed to revoke task {task_id}: {e}")
218
+ return False
219
+
220
+
221
+ def create_redlock(key, auto_release_time=3600):
222
+ """
223
+ Create a Redlock instance with output suppression during initialization.
224
+
225
+ Args:
226
+ key (str): The lock key
227
+ auto_release_time (int): Auto release time in seconds (default: 3600)
228
+
229
+ Returns:
230
+ Redlock: The configured Redlock instance
231
+ """
232
+ import logging
233
+
234
+ # Permanently suppress pottery logger output
235
+ pottery_logger = logging.getLogger("pottery")
236
+ pottery_logger.setLevel(logging.CRITICAL)
237
+
238
+ with open(os.devnull, "w") as devnull:
239
+ with redirect_stdout(devnull), redirect_stderr(devnull):
240
+ return Redlock(
241
+ key=key,
242
+ masters={redis},
243
+ auto_release_time=auto_release_time,
244
+ )