osism 0.20250505.0__py3-none-any.whl → 0.20250514.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/api.py CHANGED
@@ -41,7 +41,7 @@ class WebhookNetboxData(BaseModel):
41
41
  class LogConfig(BaseModel):
42
42
  """Logging configuration to be set for the server"""
43
43
 
44
- LOGGER_NAME: str = "mycoolapp"
44
+ LOGGER_NAME: str = "osism"
45
45
  LOG_FORMAT: str = "%(levelprefix)s | %(asctime)s | %(message)s"
46
46
  LOG_LEVEL: str = "DEBUG"
47
47
 
osism/commands/apply.py CHANGED
@@ -116,7 +116,7 @@ class Run(Command):
116
116
  if format == "log":
117
117
  for c in t.children:
118
118
  logger.info(
119
- f"Task {c.task_id} is running in background. No more output. Check ARA for logs."
119
+ f"Task {c.task_id} (loadbalancer) is running in background. No more output. Check ARA for logs."
120
120
  )
121
121
 
122
122
  # As explained above, it is neceesary to wait for all tasks.
@@ -23,9 +23,11 @@ class Sync(Command):
23
23
  "manager", "configuration", arguments, auto_release_time=60
24
24
  )
25
25
 
26
- logger.info(f"Task {t.task_id} was prepared for execution.")
27
26
  logger.info(
28
- f"It takes a moment until task {t.task_id} has been started and output is visible here."
27
+ f"Task {t.task_id} (sync configuration) was prepared for execution."
28
+ )
29
+ logger.info(
30
+ f"It takes a moment until task {t.task_id} (sync configuration) has been started and output is visible here."
29
31
  )
30
32
 
31
33
  rc = handle_task(t, True, format, 60)
osism/commands/manage.py CHANGED
@@ -12,7 +12,7 @@ import requests
12
12
  from osism.data import TEMPLATE_IMAGE_CLUSTERAPI, TEMPLATE_IMAGE_OCTAVIA
13
13
  from osism.tasks import openstack, handle_task
14
14
 
15
- SUPPORTED_CLUSTERAPI_K8S_IMAGES = ["1.30", "1.31", "1.32"]
15
+ SUPPORTED_CLUSTERAPI_K8S_IMAGES = ["1.31", "1.32", "1.33"]
16
16
 
17
17
 
18
18
  class ImageClusterapi(Command):
osism/commands/netbox.py CHANGED
@@ -1,9 +1,13 @@
1
1
  # SPDX-License-Identifier: Apache-2.0
2
2
 
3
+ import os
4
+ import subprocess
5
+
3
6
  from cliff.command import Command
4
7
  from loguru import logger
8
+ import yaml
5
9
 
6
- from osism.tasks import conductor, netbox, reconciler, handle_task
10
+ from osism.tasks import conductor, netbox, handle_task
7
11
 
8
12
 
9
13
  class Ironic(Command):
@@ -28,26 +32,9 @@ class Ironic(Command):
28
32
  force_update=parsed_args.force_update
29
33
  )
30
34
  if wait:
31
- logger.info(f"Task {task.task_id} is running. Wait. No more output.")
32
- task.wait(timeout=None, interval=0.5)
33
-
34
-
35
- class Sync(Command):
36
- def get_parser(self, prog_name):
37
- parser = super(Sync, self).get_parser(prog_name)
38
- parser.add_argument(
39
- "--no-wait",
40
- help="Do not wait until the sync has been completed",
41
- action="store_true",
42
- )
43
- return parser
44
-
45
- def take_action(self, parsed_args):
46
- wait = not parsed_args.no_wait
47
-
48
- task = reconciler.sync_inventory_with_netbox.delay()
49
- if wait:
50
- logger.info(f"Task {task.task_id} is running. Wait. No more output.")
35
+ logger.info(
36
+ f"Task {task.task_id} (sync ironic) is running. Wait. No more output."
37
+ )
51
38
  task.wait(timeout=None, interval=0.5)
52
39
 
53
40
 
@@ -140,9 +127,9 @@ class Manage(Command):
140
127
  return handle_task(task, wait, format="script", timeout=3600)
141
128
 
142
129
 
143
- class Ping(Command):
130
+ class Versions(Command):
144
131
  def get_parser(self, prog_name):
145
- parser = super(Ping, self).get_parser(prog_name)
132
+ parser = super(Versions, self).get_parser(prog_name)
146
133
  return parser
147
134
 
148
135
  def take_action(self, parsed_args):
@@ -150,3 +137,65 @@ class Ping(Command):
150
137
  task.wait(timeout=None, interval=0.5)
151
138
  result = task.get()
152
139
  print(result)
140
+
141
+
142
+ class Console(Command):
143
+ def get_parser(self, prog_name):
144
+ parser = super(Console, self).get_parser(prog_name)
145
+ parser.add_argument(
146
+ "type",
147
+ nargs=1,
148
+ choices=["info", "search", "filter", "shell"],
149
+ help="Type of the console (default: %(default)s)",
150
+ )
151
+ parser.add_argument(
152
+ "arguments", nargs="*", type=str, default="", help="Additional arguments"
153
+ )
154
+
155
+ return parser
156
+
157
+ def take_action(self, parsed_args):
158
+ type_console = parsed_args.type[0]
159
+ arguments = " ".join(
160
+ [f"'{item}'" if " " in item else item for item in parsed_args.arguments]
161
+ )
162
+
163
+ home_dir = os.path.expanduser("~")
164
+ nbcli_dir = os.path.join(home_dir, ".nbcli")
165
+ if not os.path.exists(nbcli_dir):
166
+ os.mkdir(nbcli_dir)
167
+
168
+ nbcli_file = os.path.join(nbcli_dir, "user_config.yml")
169
+ if not os.path.exists(nbcli_file):
170
+ try:
171
+ with open("/run/secrets/NETBOX_TOKEN", "r") as fp:
172
+ token = fp.read().strip()
173
+ except FileNotFoundError:
174
+ token = None
175
+
176
+ url = os.environ.get("NETBOX_API", None)
177
+
178
+ if not token or not url:
179
+ logger.error("Netbox integration not configured.")
180
+ return
181
+
182
+ subprocess.call(
183
+ ["/usr/local/bin/nbcli", "init"],
184
+ stdout=subprocess.DEVNULL,
185
+ stderr=subprocess.DEVNULL,
186
+ )
187
+ os.remove(nbcli_file)
188
+
189
+ nbcli_config = {
190
+ "pynetbox": {
191
+ "url": url,
192
+ "token": token,
193
+ },
194
+ "requests": {"verify": False},
195
+ "nbcli": {"filter_limit": 50},
196
+ "user": {},
197
+ }
198
+ with open(nbcli_file, "w") as fp:
199
+ yaml.dump(nbcli_config, fp, default_flow_style=False)
200
+
201
+ subprocess.call(f"/usr/local/bin/nbcli {type_console} {arguments}", shell=True)
@@ -47,7 +47,7 @@ class Sync(Command):
47
47
  t = reconciler.run.delay(publish=wait)
48
48
  if wait:
49
49
  logger.info(
50
- f"Task {t.task_id} is running in background. Output coming soon."
50
+ f"Task {t.task_id} (sync inventory) is running in background. Output coming soon."
51
51
  )
52
52
  rc = 0
53
53
  stoptime = time.time() + task_timeout
@@ -77,4 +77,6 @@ class Sync(Command):
77
77
  redis.close()
78
78
  return rc
79
79
  else:
80
- logger.info(f"Task {t.task_id} is running in background. No more output.")
80
+ logger.info(
81
+ f"Task {t.task_id} (sync inventory) is running in background. No more output."
82
+ )
@@ -52,7 +52,7 @@ class Run(Command):
52
52
  )
53
53
  return parser
54
54
 
55
- def _handle_task(self, t, wait, format, timeout):
55
+ def _handle_task(self, t, wait, format, timeout, playbook):
56
56
  rc = 0
57
57
  if wait:
58
58
  stoptime = time.time() + timeout
@@ -85,7 +85,7 @@ class Run(Command):
85
85
  else:
86
86
  if format == "log":
87
87
  logger.info(
88
- f"Task {t.task_id} is running in background. No more output. Check ARA for logs."
88
+ f"Task {t.task_id} (validate {playbook}) is running in background. No more output. Check ARA for logs."
89
89
  )
90
90
  elif format == "script":
91
91
  print(f"{t.task_id}")
@@ -120,6 +120,6 @@ class Run(Command):
120
120
  environment = VALIDATE_PLAYBOOKS[validator]["environment"]
121
121
  t = ansible.run.delay(environment, playbook, arguments)
122
122
 
123
- rc = self._handle_task(t, wait, format, timeout)
123
+ rc = self._handle_task(t, wait, format, timeout, playbook)
124
124
 
125
125
  return rc
osism/settings.py CHANGED
@@ -23,7 +23,7 @@ REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379"))
23
23
  REDIS_DB: int = int(os.getenv("REDIS_DB", "0"))
24
24
 
25
25
 
26
- NETBOX_URL = os.getenv("NETBOX_API")
26
+ NETBOX_URL = os.getenv("NETBOX_API", os.getenv("NETBOX_URL"))
27
27
  NETBOX_TOKEN = os.getenv("NETBOX_TOKEN", read_secret("NETBOX_TOKEN"))
28
28
  IGNORE_SSL_ERRORS = os.getenv("IGNORE_SSL_ERRORS", "True") == "True"
29
29
 
@@ -35,8 +35,8 @@ INVENTORY_RECONCILER_SCHEDULE = float(
35
35
 
36
36
  OSISM_API_URL = os.getenv("OSISM_API_URL", None)
37
37
 
38
- OSISM_CONDUCTOR_NETBOX_FILTER_LIST = os.getenv(
39
- "OSISM_CONDUCTOR_NETBOX_FILTER_LIST",
38
+ NETBOX_FILTER_LIST = os.getenv(
39
+ "NETBOX_FILTER_LIST",
40
40
  "[{'state': 'active', 'tag': ['managed-by-ironic']}]",
41
41
  )
42
42
 
osism/tasks/conductor.py CHANGED
@@ -1,5 +1,7 @@
1
1
  # SPDX-License-Identifier: Apache-2.0
2
2
 
3
+ from ansible import constants as ansible_constants
4
+ from ansible.parsing.vault import VaultLib, VaultSecret
3
5
  from celery import Celery
4
6
  from celery.signals import worker_process_init
5
7
  import copy
@@ -18,83 +20,9 @@ app.config_from_object(Config)
18
20
 
19
21
 
20
22
  configuration = {}
21
- nb_device_query_list = None
22
23
 
23
24
 
24
- @worker_process_init.connect
25
- def celery_init_worker(**kwargs):
26
- global configuration
27
-
28
- with open("/etc/conductor.yml") as fp:
29
- configuration = yaml.load(fp, Loader=yaml.SafeLoader)
30
-
31
- if not configuration:
32
- logger.warning(
33
- "The conductor configuration is empty. That's probably wrong"
34
- )
35
- configuration = {}
36
- return
37
-
38
- # Resolve all IDs in the conductor.yml
39
- if Config.enable_ironic.lower() in ["true", "yes"]:
40
- if "ironic_parameters" not in configuration:
41
- logger.error(
42
- "ironic_parameters not found in the conductor configuration"
43
- )
44
- return
45
-
46
- if "driver_info" in configuration["ironic_parameters"]:
47
- if "deploy_kernel" in configuration["ironic_parameters"]["driver_info"]:
48
- result = openstack.image_get(
49
- configuration["ironic_parameters"]["driver_info"][
50
- "deploy_kernel"
51
- ]
52
- )
53
- configuration["ironic_parameters"]["driver_info"][
54
- "deploy_kernel"
55
- ] = result.id
56
-
57
- if (
58
- "deploy_ramdisk"
59
- in configuration["ironic_parameters"]["driver_info"]
60
- ):
61
- result = openstack.image_get(
62
- configuration["ironic_parameters"]["driver_info"][
63
- "deploy_ramdisk"
64
- ]
65
- )
66
- configuration["ironic_parameters"]["driver_info"][
67
- "deploy_ramdisk"
68
- ] = result.id
69
-
70
- if (
71
- "cleaning_network"
72
- in configuration["ironic_parameters"]["driver_info"]
73
- ):
74
- result = openstack.network_get(
75
- configuration["ironic_parameters"]["driver_info"][
76
- "cleaning_network"
77
- ]
78
- )
79
- configuration["ironic_parameters"]["driver_info"][
80
- "cleaning_network"
81
- ] = result.id
82
-
83
- if (
84
- "provisioning_network"
85
- in configuration["ironic_parameters"]["driver_info"]
86
- ):
87
- result = openstack.network_get(
88
- configuration["ironic_parameters"]["driver_info"][
89
- "provisioning_network"
90
- ]
91
- )
92
- configuration["ironic_parameters"]["driver_info"][
93
- "provisioning_network"
94
- ] = result.id
95
-
96
- global nb_device_query_list
97
-
25
+ def get_nb_device_query_list():
98
26
  try:
99
27
  supported_nb_device_filters = [
100
28
  "site",
@@ -105,9 +33,7 @@ def celery_init_worker(**kwargs):
105
33
  "tag",
106
34
  "state",
107
35
  ]
108
- nb_device_query_list = yaml.safe_load(
109
- settings.OSISM_CONDUCTOR_NETBOX_FILTER_LIST
110
- )
36
+ nb_device_query_list = yaml.safe_load(settings.NETBOX_FILTER_LIST)
111
37
  if type(nb_device_query_list) is not list:
112
38
  raise TypeError
113
39
  for nb_device_query in nb_device_query_list:
@@ -129,13 +55,81 @@ def celery_init_worker(**kwargs):
129
55
  raise ValueError(f"Invalid name {value_name} for {key}")
130
56
  except (yaml.YAMLError, TypeError):
131
57
  logger.error(
132
- f"Setting OSISM_CONDUCTOR_NETBOX_FILTER_LIST needs to be an array of mappings containing supported netbox device filters: {supported_nb_device_filters}"
58
+ f"Setting NETBOX_FILTER_LIST needs to be an array of mappings containing supported netbox device filters: {supported_nb_device_filters}"
133
59
  )
134
60
  nb_device_query_list = []
135
61
  except ValueError as exc:
136
- logger.error(f"Unknown value in OSISM_CONDUCTOR_NETBOX_FILTER_LIST: {exc}")
62
+ logger.error(f"Unknown value in NETBOX_FILTER_LIST: {exc}")
137
63
  nb_device_query_list = []
138
64
 
65
+ return nb_device_query_list
66
+
67
+
68
+ def get_configuration():
69
+ with open("/etc/conductor.yml") as fp:
70
+ configuration = yaml.load(fp, Loader=yaml.SafeLoader)
71
+
72
+ if not configuration:
73
+ logger.warning(
74
+ "The conductor configuration is empty. That's probably wrong"
75
+ )
76
+ return {}
77
+
78
+ if Config.enable_ironic.lower() not in ["true", "yes"]:
79
+ return configuration
80
+
81
+ if "ironic_parameters" not in configuration:
82
+ logger.error("ironic_parameters not found in the conductor configuration")
83
+ return configuration
84
+
85
+ if "driver_info" in configuration["ironic_parameters"]:
86
+ if "deploy_kernel" in configuration["ironic_parameters"]["driver_info"]:
87
+ result = openstack.image_get(
88
+ configuration["ironic_parameters"]["driver_info"]["deploy_kernel"]
89
+ )
90
+ configuration["ironic_parameters"]["driver_info"][
91
+ "deploy_kernel"
92
+ ] = result.id
93
+
94
+ if "deploy_ramdisk" in configuration["ironic_parameters"]["driver_info"]:
95
+ result = openstack.image_get(
96
+ configuration["ironic_parameters"]["driver_info"]["deploy_ramdisk"]
97
+ )
98
+ configuration["ironic_parameters"]["driver_info"][
99
+ "deploy_ramdisk"
100
+ ] = result.id
101
+
102
+ if "cleaning_network" in configuration["ironic_parameters"]["driver_info"]:
103
+ result = openstack.network_get(
104
+ configuration["ironic_parameters"]["driver_info"][
105
+ "cleaning_network"
106
+ ]
107
+ )
108
+ configuration["ironic_parameters"]["driver_info"][
109
+ "cleaning_network"
110
+ ] = result.id
111
+
112
+ if (
113
+ "provisioning_network"
114
+ in configuration["ironic_parameters"]["driver_info"]
115
+ ):
116
+ result = openstack.network_get(
117
+ configuration["ironic_parameters"]["driver_info"][
118
+ "provisioning_network"
119
+ ]
120
+ )
121
+ configuration["ironic_parameters"]["driver_info"][
122
+ "provisioning_network"
123
+ ] = result.id
124
+
125
+ return configuration
126
+
127
+
128
+ @worker_process_init.connect
129
+ def celery_init_worker(**kwargs):
130
+ global configuration
131
+ configuration = get_configuration()
132
+
139
133
 
140
134
  @app.on_after_configure.connect
141
135
  def setup_periodic_tasks(sender, **kwargs):
@@ -168,6 +162,31 @@ def sync_netbox_with_ironic(self, force_update=False):
168
162
  if not updates[key]:
169
163
  updates.pop(key)
170
164
 
165
+ def deep_merge(a, b):
166
+ for key, value in b.items():
167
+ if value == "DELETE":
168
+ # NOTE: Use special string to remove keys
169
+ a.pop(key, None)
170
+ elif (
171
+ key not in a.keys()
172
+ or not isinstance(a[key], dict)
173
+ or not isinstance(value, dict)
174
+ ):
175
+ a[key] = value
176
+ else:
177
+ deep_merge(a[key], value)
178
+
179
+ def deep_decrypt(a, vault):
180
+ for key, value in list(a.items()):
181
+ if not isinstance(value, dict):
182
+ if vault.is_encrypted(value):
183
+ try:
184
+ a[key] = vault.decrypt(value).decode()
185
+ except Exception:
186
+ a.pop(key, None)
187
+ else:
188
+ deep_decrypt(a[key], vault)
189
+
171
190
  driver_params = {
172
191
  "ipmi": {
173
192
  "address": "ipmi_address",
@@ -181,6 +200,7 @@ def sync_netbox_with_ironic(self, force_update=False):
181
200
  }
182
201
 
183
202
  devices = set()
203
+ nb_device_query_list = get_nb_device_query_list()
184
204
  for nb_device_query in nb_device_query_list:
185
205
  devices |= set(netbox.get_devices(**nb_device_query))
186
206
 
@@ -211,20 +231,51 @@ def sync_netbox_with_ironic(self, force_update=False):
211
231
  # NOTE: Find nodes in netbox which are not present in Ironic and add them
212
232
  for device in devices:
213
233
  logger.info(f"Looking for {device.name} in ironic")
234
+ logger.info(device)
214
235
 
215
236
  node_interfaces = list(netbox.get_interfaces_by_device(device.name))
216
237
 
217
238
  node_attributes = get_ironic_parameters()
239
+ if (
240
+ "ironic_parameters" in device.custom_fields
241
+ and device.custom_fields["ironic_parameters"]
242
+ ):
243
+ # NOTE: Update node attributes with overrides from netbox device
244
+ deep_merge(node_attributes, device.custom_fields["ironic_parameters"])
245
+ # NOTE: Decrypt ansible vaulted secrets
246
+ try:
247
+ vault_secret = utils.get_ansible_vault_password()
248
+ vault = VaultLib(
249
+ [
250
+ (
251
+ ansible_constants.DEFAULT_VAULT_ID_MATCH,
252
+ VaultSecret(vault_secret.encode()),
253
+ )
254
+ ]
255
+ )
256
+ except Exception:
257
+ logger.error("Unable to get vault secret. Dropping encrypted entries")
258
+ vault = VaultLib()
259
+ deep_decrypt(node_attributes, vault)
218
260
  if (
219
261
  "driver" in node_attributes
220
262
  and node_attributes["driver"] in driver_params.keys()
221
263
  ):
222
264
  if "driver_info" in node_attributes:
265
+ # NOTE: Pop all fields belonging to a different driver
266
+ unused_drivers = [
267
+ driver
268
+ for driver in driver_params.keys()
269
+ if driver != node_attributes["driver"]
270
+ ]
271
+ for key in list(node_attributes["driver_info"].keys()):
272
+ for driver in unused_drivers:
273
+ if key.startswith(driver + "_"):
274
+ node_attributes["driver_info"].pop(key, None)
275
+ # NOTE: Render driver address field
223
276
  address_key = driver_params[node_attributes["driver"]]["address"]
224
277
  if address_key in node_attributes["driver_info"]:
225
- if "oob_address" in device.custom_fields:
226
- node_mgmt_address = device.custom_fields["oob_address"]
227
- elif "address" in device.oob_ip:
278
+ if device.oob_ip and "address" in device.oob_ip:
228
279
  node_mgmt_address = device.oob_ip["address"]
229
280
  else:
230
281
  node_mgmt_addresses = [
@@ -248,18 +299,6 @@ def sync_netbox_with_ironic(self, force_update=False):
248
299
  )
249
300
  )
250
301
  )
251
- else:
252
- logger.error(f"Could not find out-of-band address for {device}")
253
- node_attributes["driver_info"].pop(address_key, None)
254
- if (
255
- "port" in driver_params[node_attributes["driver"]]
256
- and "oob_port" in device.custom_fields
257
- and device.custom_fields["oob_port"]
258
- ):
259
- port_key = driver_params[node_attributes["driver"]]["port"]
260
- node_attributes["driver_info"].update(
261
- {port_key: device.custom_fields["oob_port"]}
262
- )
263
302
  node_attributes.update({"resource_class": device.name})
264
303
  ports_attributes = [
265
304
  dict(address=interface.mac_address)
@@ -270,9 +309,9 @@ def sync_netbox_with_ironic(self, force_update=False):
270
309
  lock = Redlock(
271
310
  key=f"lock_osism_tasks_conductor_sync_netbox_with_ironic-{device.name}",
272
311
  masters={utils.redis},
273
- auto_release_time=60,
312
+ auto_release_time=600,
274
313
  )
275
- if lock.acquire(timeout=20):
314
+ if lock.acquire(timeout=120):
276
315
  try:
277
316
  logger.info(f"Processing device {device.name}")
278
317
  node = openstack.baremetal_node_show(device.name, ignore_missing=True)
osism/tasks/reconciler.py CHANGED
@@ -63,33 +63,3 @@ def run_on_change(self):
63
63
  p.wait()
64
64
 
65
65
  lock.release()
66
-
67
-
68
- @app.task(bind=True, name="osism.tasks.reconciler.sync_inventory_with_netbox")
69
- def sync_inventory_with_netbox(self):
70
- lock = Redlock(
71
- key="lock_osism_tasks_reconciler_sync_inventory_with_netbox",
72
- masters={utils.redis},
73
- auto_release_time=60,
74
- )
75
-
76
- if lock.acquire(timeout=20):
77
- p = subprocess.Popen(
78
- "/sync-inventory-with-netbox.sh",
79
- shell=True,
80
- stdout=subprocess.PIPE,
81
- stderr=subprocess.STDOUT,
82
- )
83
-
84
- for line in io.TextIOWrapper(p.stdout, encoding="utf-8"):
85
- # NOTE: use task_id or request_id in future
86
- utils.redis.publish(
87
- "netbox-sync-inventory-with-netbox", {"type": "stdout", "content": line}
88
- )
89
-
90
- lock.release()
91
-
92
- # NOTE: use task_id or request_id in future
93
- utils.redis.publish(
94
- "netbox-sync-inventory-with-netbox", {"type": "action", "content": "quit"}
95
- )
osism/utils/__init__.py CHANGED
@@ -1,5 +1,6 @@
1
1
  # SPDX-License-Identifier: Apache-2.0
2
2
 
3
+ from cryptography.fernet import Fernet
3
4
  import keystoneauth1
4
5
  from loguru import logger
5
6
  import openstack
@@ -95,6 +96,22 @@ def get_openstack_connection():
95
96
  return conn
96
97
 
97
98
 
99
+ def get_ansible_vault_password():
100
+ keyfile = "/share/ansible_vault_password.key"
101
+
102
+ try:
103
+ with open(keyfile, "r") as fp:
104
+ key = fp.read()
105
+ f = Fernet(key)
106
+
107
+ encrypted_ansible_vault_password = redis.get("ansible_vault_password")
108
+ ansible_vault_password = f.decrypt(encrypted_ansible_vault_password)
109
+ return ansible_vault_password.decode("utf-8")
110
+ except Exception as exc:
111
+ logger.error("Unable to get ansible vault password")
112
+ raise exc
113
+
114
+
98
115
  # https://stackoverflow.com/questions/2361426/get-the-first-item-from-an-iterable-that-matches-a-condition
99
116
  def first(iterable, condition=lambda x: True):
100
117
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: osism
3
- Version: 0.20250505.0
3
+ Version: 0.20250514.0
4
4
  Summary: OSISM manager interface
5
5
  Home-page: https://github.com/osism/python-osism
6
6
  Author: OSISM GmbH
@@ -29,20 +29,19 @@ Requires-Dist: PyYAML==6.0.2
29
29
  Requires-Dist: ara==1.7.2
30
30
  Requires-Dist: celery[redis]==5.5.2
31
31
  Requires-Dist: cliff==4.9.1
32
- Requires-Dist: deepdiff==8.4.2
32
+ Requires-Dist: deepdiff==8.5.0
33
33
  Requires-Dist: docker==7.1.0
34
34
  Requires-Dist: dtrack-auditor==1.5.0
35
35
  Requires-Dist: fastapi==0.115.12
36
36
  Requires-Dist: flower==2.0.1
37
- Requires-Dist: hiredis==3.1.0
38
- Requires-Dist: jc==1.25.4
37
+ Requires-Dist: hiredis==3.1.1
38
+ Requires-Dist: jc==1.25.5
39
39
  Requires-Dist: keystoneauth1==5.10.0
40
40
  Requires-Dist: kombu==5.5.3
41
41
  Requires-Dist: kubernetes==32.0.1
42
42
  Requires-Dist: loguru==0.7.3
43
+ Requires-Dist: nbcli==0.10.0.dev2
43
44
  Requires-Dist: netmiko==4.5.0
44
- Requires-Dist: nornir-ansible==2023.12.28
45
- Requires-Dist: nornir==3.5.0
46
45
  Requires-Dist: openstacksdk==4.5.0
47
46
  Requires-Dist: pottery==3.0.1
48
47
  Requires-Dist: prompt-toolkit==3.0.51
@@ -50,7 +49,7 @@ Requires-Dist: pydantic==1.10.22
50
49
  Requires-Dist: pynetbox==7.4.1
51
50
  Requires-Dist: pytest-testinfra==10.2.2
52
51
  Requires-Dist: python-dateutil==2.9.0.post0
53
- Requires-Dist: setuptools==80.3.1
52
+ Requires-Dist: setuptools==80.4.0
54
53
  Requires-Dist: sqlmodel==0.0.24
55
54
  Requires-Dist: sushy==5.5.0
56
55
  Requires-Dist: tabulate==0.9.0
@@ -61,7 +60,7 @@ Provides-Extra: ansible
61
60
  Requires-Dist: ansible-runner==2.4.1; extra == "ansible"
62
61
  Requires-Dist: ansible-core==2.18.5; extra == "ansible"
63
62
  Provides-Extra: openstack-image-manager
64
- Requires-Dist: openstack-image-manager==0.20250423.0; extra == "openstack-image-manager"
63
+ Requires-Dist: openstack-image-manager==0.20250508.0; extra == "openstack-image-manager"
65
64
  Dynamic: author
66
65
  Dynamic: author-email
67
66
  Dynamic: classifier
@@ -1,29 +1,29 @@
1
1
  osism/__init__.py,sha256=1UiNTBus0V0f2AbZQzAtVtu6zkfCCrw0OTq--NwFAqY,341
2
2
  osism/__main__.py,sha256=ILe4gu61xEISiBsxanqTQIdSkV-YhpZXTRlguCYyssk,141
3
- osism/api.py,sha256=xJC6RyC1TU54PU2C06rlialh2SmTCgM1L0MSgyUlubU,4331
3
+ osism/api.py,sha256=_d7KvjvTK3PA-ZcAJKD7kXiSZrEQ8YV8NF7MYYriW3U,4327
4
4
  osism/main.py,sha256=Dt2-9sLXcS-Ny4DAz7hrha-KRc7zd7BFUTRdfs_X8z4,893
5
- osism/settings.py,sha256=xzFRbf5mdl_MUAUFqPavE14vaVuTr5z6b969vJQvF2E,1306
5
+ osism/settings.py,sha256=09nVwFNWrx2LY-kw8kDG_gMEG72o49BvU8eqoqgQvuc,1299
6
6
  osism/actions/__init__.py,sha256=bG7Ffen4LvQtgnYPFEpFccsWs81t4zqqeqn9ZeirH6E,38
7
7
  osism/commands/__init__.py,sha256=Ag4wX_DCgXRdoLn6t069jqb3DdRylsX2nyYkiyCx4uk,456
8
- osism/commands/apply.py,sha256=n3lLb1cS3GahQqRT0723di98hg47MjVzDzkAoeZX7qU,16780
8
+ osism/commands/apply.py,sha256=mH3-NctgevVzP_1IW92FQeiYMCPB49K5hXbxmTY2vnA,16795
9
9
  osism/commands/compose.py,sha256=iqzG7mS9E1VWaLNN6yQowjOqiHn3BMdj-yfXb3Dc4Ok,1200
10
10
  osism/commands/compute.py,sha256=XqOdnTqQ5Yqz__4rXbr64ctfe9Qq342IVFLnKmvjVpI,17226
11
- osism/commands/configuration.py,sha256=bvwBuc27glcZ5xDXQkKrgMDoXEdYgJnyjKd8fRj6hHk,890
11
+ osism/commands/configuration.py,sha256=sPe8b0dVKFRbr30xoeVdAnHbGwCwgUh0xa_Vzv5pSQQ,954
12
12
  osism/commands/console.py,sha256=8BPz1hio5Wi6kONVAWFuSqkDRrMcLEYeFIY8dbtN6e4,3218
13
13
  osism/commands/container.py,sha256=Fku2GaCM3Idq_FxExUtNqjrEM0XYjpVvXmueSVO8S_c,1601
14
14
  osism/commands/get.py,sha256=ryytjtXWmlMV0NucP5tGkMZu0nIlC4xVtjRk4iMZ06c,8967
15
15
  osism/commands/log.py,sha256=2IpYuosC7FZwwLvM8HmKSU1NRNIelVVYzqjjVMCrOJk,4072
16
- osism/commands/manage.py,sha256=5ypZzA91QGgWCt35rVNJSXVC9l189IntS4UnsD8LRZY,11971
17
- osism/commands/netbox.py,sha256=_2-j6XM9JvH0DXnbct6rG9T6hT8KEpm3vazQC28Rt7I,4529
16
+ osism/commands/manage.py,sha256=WxUZEhylZj2IhydAe3BAr3S5ED6opG243skfSq5q41s,11971
17
+ osism/commands/netbox.py,sha256=XDJVuoh2pk7XKGAElnxIkJ_v8bZ2glk0FYynVzNnSPE,6059
18
18
  osism/commands/noset.py,sha256=7zDFuFMyNpo7DUOKcNiYV8nodtdMOYFp5LDPcuJhlZ8,1481
19
- osism/commands/reconciler.py,sha256=Ja_b86gX6-_Pr3DmrUUvskmEnnJpHQ-XJNQLycMJeyc,2818
19
+ osism/commands/reconciler.py,sha256=xOyPzQj66xwjdQd2ysCTHX2yBvmMVMppUDZTas6voXc,2882
20
20
  osism/commands/server.py,sha256=avmoOv5rjOi-fN2A-27cPwOtiy2Q2j6UFtCh3QrfWAI,7512
21
21
  osism/commands/service.py,sha256=A1lgAlGeCJpbFFqF55DRWPcCirIgpU0dzjzVLZ0mz3k,2649
22
22
  osism/commands/set.py,sha256=xLBi2DzbVQo2jb3-cOIE9In5UB3vFxquQJkDN-EsfhM,1425
23
23
  osism/commands/status.py,sha256=X-Rcj-XuNPDBoxsGkf96NswwpmTognxz1V6E2NX2ZgY,1997
24
24
  osism/commands/sync.py,sha256=Vf9k7uVQTIu-8kK1u7Gjs3et3RRBEkmnNikot_PFJIE,484
25
25
  osism/commands/task.py,sha256=mwJJ7a71Lw3o_FX7j3rR0-NbPdPwMDOjbOAiiXE4uGc,543
26
- osism/commands/validate.py,sha256=hIQB0zk4xIBZJORtBp_tWrXTRKKhB2qi6j-mznDxKR4,4191
26
+ osism/commands/validate.py,sha256=cA0CSvcbTr0K_6C5EofULrJSEp5xthpRC0TZgb_eazU,4233
27
27
  osism/commands/vault.py,sha256=Ip0IMR7zaBkPbLJenXr4ZwxM6FnozZ9wn9rwHmFHo8s,1818
28
28
  osism/commands/volume.py,sha256=l6oAk__dFM8KKdLTWOvuSiI7tLh9wAPZp8hwmYF-NX0,6595
29
29
  osism/commands/wait.py,sha256=mKFDqEXcaLlKw1T3MuBEZpNh7CeL3lpUXgubD2_f8es,6580
@@ -38,18 +38,18 @@ osism/services/listener.py,sha256=eEamlQsJqCuU9K2QFmk3yM9LAJZEanVcTLtGMsNCKjs,97
38
38
  osism/tasks/__init__.py,sha256=ZEu_KYsapTYp0etr-rLqie_NT_LndHDDpx53xITru5Y,8691
39
39
  osism/tasks/ansible.py,sha256=RcLxLrjzL5_X6OjNHm3H0lZlmKKlYKIANB0M4_d4chE,1109
40
40
  osism/tasks/ceph.py,sha256=eIQkah3Kj4INtOkF9kTjHbXJ3_J2lg48EWJKfHc-UYw,615
41
- osism/tasks/conductor.py,sha256=jtvw5UfhPOPEbIBkIyBkiHbaPJsNKbEABbc3FtHYOo4,18210
41
+ osism/tasks/conductor.py,sha256=Rxx8LrHVMksVpywpzi-av8Nj8qiBTmOFG9P_ksUe6SE,19477
42
42
  osism/tasks/kolla.py,sha256=wJQpWn_01iWLkr7l7T7RNrQGfRgsgmYi4WQlTmNGvew,618
43
43
  osism/tasks/kubernetes.py,sha256=VzXq_VrYU_CLm4cOruqnE3Kq2ydfO9glZ3p0bp3OYoc,625
44
44
  osism/tasks/netbox.py,sha256=QVOLiTH2Su237YAS0QfXbQ86E-OA1JzrFDfyi9JBmvk,5658
45
45
  osism/tasks/openstack.py,sha256=g15tCll5vP1pC6ysxRCTZxplsdGmXbxaCH3k1Qdv5Xg,6367
46
- osism/tasks/reconciler.py,sha256=RGUcax2gDuyVLw1nGRQn5izXclnPBo9MRl0ndLDiiYQ,2707
47
- osism/utils/__init__.py,sha256=_uhe9ghqAJ2me0p187X-vzJ2Nh_Hpmfw3D6HU4kKa10,3759
48
- osism-0.20250505.0.dist-info/licenses/AUTHORS,sha256=DJIRsjyrFxKjFvmpUNDRDBS04nRiJ5B6FpKcDcfnoGM,36
49
- osism-0.20250505.0.dist-info/licenses/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
50
- osism-0.20250505.0.dist-info/METADATA,sha256=9UPuB22oJMzuSpvDxVYVXTun60Wz4uBvtE7fMUKJKBY,2972
51
- osism-0.20250505.0.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
52
- osism-0.20250505.0.dist-info/entry_points.txt,sha256=DlfrvU14rI55WuTrwNRoce9FY3ric4HeZKZx_Z3NzCw,3015
53
- osism-0.20250505.0.dist-info/pbr.json,sha256=xWRK5iE40MX99mSkANXdqP33gzUrHwuP4JmJG0hUNMA,47
54
- osism-0.20250505.0.dist-info/top_level.txt,sha256=8L8dsI9hcaGHsdnR4k_LN9EM78EhwrXRFHyAryPXZtY,6
55
- osism-0.20250505.0.dist-info/RECORD,,
46
+ osism/tasks/reconciler.py,sha256=b6IRJBYvG_lRlQ6cQ46jSZJPNJeTI7igaCJ_7AzgIDQ,1767
47
+ osism/utils/__init__.py,sha256=_Y4qchR5yyI_JKhBWd_jcsvDLYZjxO0c3iMA_VRQl58,4304
48
+ osism-0.20250514.0.dist-info/licenses/AUTHORS,sha256=oWotd63qsnNR945QLJP9mEXaXNtCMaesfo8ZNuLjwpU,39
49
+ osism-0.20250514.0.dist-info/licenses/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
50
+ osism-0.20250514.0.dist-info/METADATA,sha256=xH8NKJUuInnfv4yHUIA00DMY_SGxYjaqXvDt4_OW-Jo,2935
51
+ osism-0.20250514.0.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
52
+ osism-0.20250514.0.dist-info/entry_points.txt,sha256=IEmaQFJyKGN4mtNTLf9ogw7RXjEma4mSOUy_CEC4wzA,2975
53
+ osism-0.20250514.0.dist-info/pbr.json,sha256=PlM5t0PTJqXXvUbF4gDXHHJ-HyaeT0oUE17z93Yqg7A,47
54
+ osism-0.20250514.0.dist-info/top_level.txt,sha256=8L8dsI9hcaGHsdnR4k_LN9EM78EhwrXRFHyAryPXZtY,6
55
+ osism-0.20250514.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.3.1)
2
+ Generator: setuptools (80.4.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -21,6 +21,7 @@ get states = osism.commands.get:States
21
21
  get status = osism.commands.status:Run
22
22
  get tasks = osism.commands.get:Tasks
23
23
  get versions manager = osism.commands.get:VersionsManager
24
+ get versions netbox = osism.commands.netbox:Versions
24
25
  log ansible = osism.commands.log:Ansible
25
26
  log container = osism.commands.log:Container
26
27
  log file = osism.commands.log:File
@@ -40,9 +41,7 @@ manage netbox = osism.commands.netbox:Manage
40
41
  manage server list = osism.commands.server:ServerList
41
42
  manage server migrate = osism.commands.server:ServerMigrate
42
43
  manage volume list = osism.commands.volume:VolumeList
43
- netbox ping = osism.commands.netbox:Ping
44
- netbox sync = osism.commands.netbox:Sync
45
- netbox sync ironic = osism.commands.netbox:Ironic
44
+ netbox = osism.commands.netbox:Console
46
45
  noset bootstrap = osism.commands.noset:NoBootstrap
47
46
  noset maintenance = osism.commands.noset:NoMaintenance
48
47
  noset vault password = osism.commands.vault:UnsetPassword
@@ -0,0 +1 @@
1
+ Christian Berendt <berendt@osism.tech>
@@ -0,0 +1 @@
1
+ {"git_version": "6bd0c0e", "is_release": false}
@@ -1 +0,0 @@
1
- janhorstmann <horstmann@osism.tech>
@@ -1 +0,0 @@
1
- {"git_version": "6139ca4", "is_release": false}