osism 0.20250407.0__py3-none-any.whl → 0.20250505.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
@@ -7,11 +7,10 @@ from uuid import UUID
7
7
 
8
8
  from fastapi import FastAPI, Header, Request, Response
9
9
  from pydantic import BaseModel
10
- import pynetbox
11
10
  from starlette.middleware.cors import CORSMiddleware
12
11
 
13
12
  from osism.tasks import reconciler
14
- from osism import settings, utils
13
+ from osism import utils
15
14
  from osism.services.listener import BaremetalEvents
16
15
 
17
16
 
@@ -78,20 +77,6 @@ logger = logging.getLogger("api")
78
77
  baremetal_events = BaremetalEvents()
79
78
 
80
79
 
81
- @app.on_event("startup")
82
- async def startup_event():
83
- if settings.NETBOX_URL and settings.NETBOX_TOKEN:
84
- utils.nb = pynetbox.api(settings.NETBOX_URL, token=settings.NETBOX_TOKEN)
85
-
86
- if settings.IGNORE_SSL_ERRORS:
87
- import requests
88
-
89
- requests.packages.urllib3.disable_warnings()
90
- session = requests.Session()
91
- session.verify = False
92
- utils.nb.http_session = session
93
-
94
-
95
80
  @app.get("/")
96
81
  async def root():
97
82
  return {"message": "Hello World"}
osism/commands/manage.py CHANGED
@@ -188,7 +188,9 @@ class ImageOctavia(Command):
188
188
  "--deactivate",
189
189
  ]
190
190
 
191
- task_signature = openstack.image_manager.si(*arguments, configs=result)
191
+ task_signature = openstack.image_manager.si(
192
+ *arguments, configs=result, ignore_env=True
193
+ )
192
194
  task = task_signature.apply_async()
193
195
  if wait:
194
196
  logger.info(
osism/commands/server.py CHANGED
@@ -93,36 +93,131 @@ class ServerMigrate(Command):
93
93
  class ServerList(Command):
94
94
  def get_parser(self, prog_name):
95
95
  parser = super(ServerList, self).get_parser(prog_name)
96
+ parser.add_argument(
97
+ "--domain",
98
+ help="List all servers of a specific domain",
99
+ type=str,
100
+ default=None,
101
+ )
102
+ parser.add_argument(
103
+ "--project",
104
+ help="List all servers of a specific project",
105
+ type=str,
106
+ default=None,
107
+ )
108
+ parser.add_argument(
109
+ "--project-domain", help="Domain of the project", type=str, default=None
110
+ )
96
111
  return parser
97
112
 
98
113
  def take_action(self, parsed_args):
99
114
  conn = get_cloud_connection()
115
+ domain = parsed_args.domain
116
+ project = parsed_args.project
117
+ project_domain = parsed_args.project_domain
100
118
 
101
119
  result = []
102
- for server in conn.compute.servers(all_projects=True, status="build"):
103
- duration = datetime.now(timezone.utc) - dateutil.parser.parse(
104
- server.created_at
120
+ if domain:
121
+ _domain = conn.identity.find_domain(domain)
122
+ if not _domain:
123
+ logger.error(f"Domain {domain} not found")
124
+ return
125
+ projects = list(conn.identity.projects(domain_id=_domain.id))
126
+
127
+ for project in projects:
128
+ query = {"project_id": project.id}
129
+ for server in conn.compute.servers(all_projects=True, **query):
130
+ result.append(
131
+ [
132
+ project.name,
133
+ project.id,
134
+ server.id,
135
+ server.name,
136
+ server.flavor["original_name"],
137
+ server.status,
138
+ ]
139
+ )
140
+
141
+ print(
142
+ tabulate(
143
+ result,
144
+ headers=["Project", "Project ID", "ID", "Name", "Flavor", "Status"],
145
+ tablefmt="psql",
146
+ )
105
147
  )
106
- if duration.total_seconds() > 7200:
107
- logger.info(
108
- f"Server {server.id} hangs in BUILD status for more than 2 hours"
148
+
149
+ elif project:
150
+ if project_domain:
151
+ _project_domain = conn.identity.find_domain(project_domain)
152
+ if not _project_domain:
153
+ logger.error(f"Project domain {project_domain} not found")
154
+ return
155
+ query = {"domain_id": _project_domain.id}
156
+ _project = conn.identity.find_project(project, **query)
157
+ else:
158
+ _project = conn.identity.find_project(project)
159
+ if not _project:
160
+ logger.error(f"Project {project} not found")
161
+ return
162
+ query = {"project_id": _project.id}
163
+
164
+ for server in conn.compute.servers(all_projects=True, **query):
165
+ result.append(
166
+ [
167
+ server.id,
168
+ server.name,
169
+ server.flavor["original_name"],
170
+ server.status,
171
+ ]
109
172
  )
110
- result.append([server.id, server.name, server.status])
111
173
 
112
- for server in conn.compute.servers(all_projects=True, status="error"):
113
- duration = datetime.now(timezone.utc) - dateutil.parser.parse(
114
- server.created_at
115
- )
116
- if duration.total_seconds() > 7200:
117
- logger.info(
118
- f"Server {server.id} hangs in ERRORstatus for more than 2 hours"
174
+ print(
175
+ tabulate(
176
+ result,
177
+ headers=["ID", "Name", "Flavor", "Status"],
178
+ tablefmt="psql",
119
179
  )
120
- result.append([server.id, server.name, server.status])
180
+ )
121
181
 
122
- print(
123
- tabulate(
124
- result,
125
- headers=["ID", "Name", "Status"],
126
- tablefmt="psql",
182
+ else:
183
+ for server in conn.compute.servers(all_projects=True, status="build"):
184
+ duration = datetime.now(timezone.utc) - dateutil.parser.parse(
185
+ server.created_at
186
+ )
187
+ if duration.total_seconds() > 7200:
188
+ logger.info(
189
+ f"Server {server.id} hangs in BUILD status for more than 2 hours"
190
+ )
191
+ result.append(
192
+ [
193
+ server.id,
194
+ server.name,
195
+ server.flavor["original_name"],
196
+ server.status,
197
+ ]
198
+ )
199
+
200
+ for server in conn.compute.servers(all_projects=True, status="error"):
201
+ duration = datetime.now(timezone.utc) - dateutil.parser.parse(
202
+ server.created_at
203
+ )
204
+ if duration.total_seconds() > 7200:
205
+ logger.info(
206
+ f"Server {server.id} hangs in ERRORstatus for more than 2 hours"
207
+ )
208
+ result.append(
209
+ [
210
+ server.id,
211
+ server.name,
212
+ server.flavor["original_name"],
213
+ server.status,
214
+ ]
215
+ )
216
+
217
+ print(
218
+ tabulate(
219
+ result,
220
+ headers=["ID", "Name", "Flavor", "Status"],
221
+ tablefmt="psql",
222
+ )
127
223
  )
128
- )
osism/commands/volume.py CHANGED
@@ -14,63 +14,160 @@ from osism.commands import get_cloud_connection
14
14
  class VolumeList(Command):
15
15
  def get_parser(self, prog_name):
16
16
  parser = super(VolumeList, self).get_parser(prog_name)
17
+ parser.add_argument(
18
+ "--domain",
19
+ help="List all volumes of a specific domain",
20
+ type=str,
21
+ default=None,
22
+ )
23
+ parser.add_argument(
24
+ "--project",
25
+ help="List all volumes of a specific project",
26
+ type=str,
27
+ default=None,
28
+ )
29
+ parser.add_argument(
30
+ "--project-domain", help="Domain of the project", type=str, default=None
31
+ )
17
32
  return parser
18
33
 
19
34
  def take_action(self, parsed_args):
20
35
  conn = get_cloud_connection()
36
+ domain = parsed_args.domain
37
+ project = parsed_args.project
38
+ project_domain = parsed_args.project_domain
21
39
 
22
40
  result = []
23
- for volume in conn.block_storage.volumes(all_projects=True, status="detaching"):
24
- created_at = pytz.utc.localize(dateutil.parser.parse(volume.created_at))
25
- duration = datetime.now(timezone.utc) - created_at
26
- if duration.total_seconds() > 7200:
27
- logger.info(
28
- f"Volume {volume.id} hangs in DETACHING status for more than 2 hours"
29
- )
30
- result.append([volume.id, volume.name, volume.status])
31
-
32
- for volume in conn.block_storage.volumes(all_projects=True, status="creating"):
33
- created_at = pytz.utc.localize(dateutil.parser.parse(volume.created_at))
34
- duration = datetime.now(timezone.utc) - created_at
35
- if duration.total_seconds() > 7200:
36
- logger.info(
37
- f"Volume {volume.id} hangs in CREATING status for more than 2 hours"
38
- )
39
- result.append([volume.id, volume.name, volume.status])
40
-
41
- for volume in conn.block_storage.volumes(
42
- all_projects=True, status="error_deleting"
43
- ):
44
- created_at = pytz.utc.localize(dateutil.parser.parse(volume.created_at))
45
- duration = datetime.now(timezone.utc) - created_at
46
- if duration.total_seconds() > 7200:
47
- logger.info(
48
- f"Volume {volume.id} hangs in ERROR_DELETING status for more than 2 hours"
41
+ if domain:
42
+ _domain = conn.identity.find_domain(domain)
43
+ if not _domain:
44
+ logger.error(f"Domain {domain} not found")
45
+ return
46
+ projects = list(conn.identity.projects(domain_id=_domain.id))
47
+
48
+ for project in projects:
49
+ query = {"project_id": project.id}
50
+ for volume in conn.block_storage.volumes(all_projects=True, **query):
51
+ result.append(
52
+ [
53
+ project.name,
54
+ project.id,
55
+ volume.id,
56
+ volume.name,
57
+ volume.volume_type,
58
+ volume.status,
59
+ ]
60
+ )
61
+
62
+ print(
63
+ tabulate(
64
+ result,
65
+ headers=["Project", "Project ID", "ID", "Name", "Type", "Status"],
66
+ tablefmt="psql",
49
67
  )
50
- result.append([volume.id, volume.name, volume.status])
51
-
52
- for volume in conn.block_storage.volumes(all_projects=True, status="deleting"):
53
- created_at = pytz.utc.localize(dateutil.parser.parse(volume.created_at))
54
- duration = datetime.now(timezone.utc) - created_at
55
- if duration.total_seconds() > 7200:
56
- logger.info(
57
- f"Volume {volume.id} hangs in DELETING status for more than 2 hours"
68
+ )
69
+
70
+ elif project:
71
+ if project_domain:
72
+ _project_domain = conn.identity.find_domain(project_domain)
73
+ if not _project_domain:
74
+ logger.error(f"Project domain {project_domain} not found")
75
+ return
76
+ query = {"domain_id": _project_domain.id}
77
+ _project = conn.identity.find_project(project, **query)
78
+ else:
79
+ _project = conn.identity.find_project(project)
80
+ if not _project:
81
+ logger.error(f"Project {project} not found")
82
+ return
83
+ query = {"project_id": _project.id}
84
+
85
+ for volume in conn.block_storage.volumes(all_projects=True, **query):
86
+ result.append(
87
+ [
88
+ volume.id,
89
+ volume.name,
90
+ volume.volume_type,
91
+ volume.status,
92
+ ]
58
93
  )
59
- result.append([volume.id, volume.name, volume.status])
60
-
61
- for volume in conn.block_storage.volumes(all_projects=True, status="error"):
62
- created_at = pytz.utc.localize(dateutil.parser.parse(volume.created_at))
63
- duration = datetime.now(timezone.utc) - created_at
64
- if duration.total_seconds() > 7200:
65
- logger.info(
66
- f"Volume {volume.id} hangs in ERROR status for more than 2 hours"
94
+
95
+ print(
96
+ tabulate(
97
+ result,
98
+ headers=["ID", "Name", "Type", "Status"],
99
+ tablefmt="psql",
67
100
  )
68
- result.append([volume.id, volume.name, volume.status])
101
+ )
102
+
103
+ else:
104
+ for volume in conn.block_storage.volumes(
105
+ all_projects=True, status="detaching"
106
+ ):
107
+ created_at = pytz.utc.localize(dateutil.parser.parse(volume.created_at))
108
+ duration = datetime.now(timezone.utc) - created_at
109
+ if duration.total_seconds() > 7200:
110
+ logger.info(
111
+ f"Volume {volume.id} hangs in DETACHING status for more than 2 hours"
112
+ )
113
+ result.append(
114
+ [volume.id, volume.name, volume.volume_type, volume.status]
115
+ )
116
+
117
+ for volume in conn.block_storage.volumes(
118
+ all_projects=True, status="creating"
119
+ ):
120
+ created_at = pytz.utc.localize(dateutil.parser.parse(volume.created_at))
121
+ duration = datetime.now(timezone.utc) - created_at
122
+ if duration.total_seconds() > 7200:
123
+ logger.info(
124
+ f"Volume {volume.id} hangs in CREATING status for more than 2 hours"
125
+ )
126
+ result.append(
127
+ [volume.id, volume.name, volume.volume_type, volume.status]
128
+ )
129
+
130
+ for volume in conn.block_storage.volumes(
131
+ all_projects=True, status="error_deleting"
132
+ ):
133
+ created_at = pytz.utc.localize(dateutil.parser.parse(volume.created_at))
134
+ duration = datetime.now(timezone.utc) - created_at
135
+ if duration.total_seconds() > 7200:
136
+ logger.info(
137
+ f"Volume {volume.id} hangs in ERROR_DELETING status for more than 2 hours"
138
+ )
139
+ result.append(
140
+ [volume.id, volume.name, volume.volume_type, volume.status]
141
+ )
69
142
 
70
- print(
71
- tabulate(
72
- result,
73
- headers=["ID", "Name", "Status"],
74
- tablefmt="psql",
143
+ for volume in conn.block_storage.volumes(
144
+ all_projects=True, status="deleting"
145
+ ):
146
+ created_at = pytz.utc.localize(dateutil.parser.parse(volume.created_at))
147
+ duration = datetime.now(timezone.utc) - created_at
148
+ if duration.total_seconds() > 7200:
149
+ logger.info(
150
+ f"Volume {volume.id} hangs in DELETING status for more than 2 hours"
151
+ )
152
+ result.append(
153
+ [volume.id, volume.name, volume.volume_type, volume.status]
154
+ )
155
+
156
+ for volume in conn.block_storage.volumes(all_projects=True, status="error"):
157
+ created_at = pytz.utc.localize(dateutil.parser.parse(volume.created_at))
158
+ duration = datetime.now(timezone.utc) - created_at
159
+ if duration.total_seconds() > 7200:
160
+ logger.info(
161
+ f"Volume {volume.id} hangs in ERROR status for more than 2 hours"
162
+ )
163
+ result.append(
164
+ [volume.id, volume.name, volume.volume_type, volume.status]
165
+ )
166
+
167
+ print(
168
+ tabulate(
169
+ result,
170
+ headers=["ID", "Name", "Type", "Status"],
171
+ tablefmt="psql",
172
+ )
75
173
  )
76
- )
osism/settings.py CHANGED
@@ -34,3 +34,12 @@ INVENTORY_RECONCILER_SCHEDULE = float(
34
34
  )
35
35
 
36
36
  OSISM_API_URL = os.getenv("OSISM_API_URL", None)
37
+
38
+ OSISM_CONDUCTOR_NETBOX_FILTER_LIST = os.getenv(
39
+ "OSISM_CONDUCTOR_NETBOX_FILTER_LIST",
40
+ "[{'state': 'active', 'tag': ['managed-by-ironic']}]",
41
+ )
42
+
43
+ NETBOX_SECONDARIES = (
44
+ os.getenv("NETBOX_SECONDARIES", read_secret("NETBOX_SECONDARIES")) or "[]"
45
+ )
osism/tasks/__init__.py CHANGED
@@ -183,11 +183,16 @@ def run_command(
183
183
  *arguments,
184
184
  publish=True,
185
185
  locking=False,
186
+ ignore_env=False,
186
187
  auto_release_time=3600,
187
188
  ):
188
189
  result = ""
189
- command_env = os.environ.copy()
190
- command_env.update(env)
190
+
191
+ if ignore_env:
192
+ command_env = env
193
+ else:
194
+ command_env = os.environ.copy()
195
+ command_env.update(env)
191
196
 
192
197
  if locking:
193
198
  lock = Redlock(
osism/tasks/conductor.py CHANGED
@@ -9,6 +9,7 @@ from loguru import logger
9
9
  from pottery import Redlock
10
10
  import yaml
11
11
 
12
+ from osism import settings
12
13
  from osism import utils
13
14
  from osism.tasks import Config, netbox, openstack
14
15
 
@@ -17,6 +18,7 @@ app.config_from_object(Config)
17
18
 
18
19
 
19
20
  configuration = {}
21
+ nb_device_query_list = None
20
22
 
21
23
 
22
24
  @worker_process_init.connect
@@ -91,6 +93,49 @@ def celery_init_worker(**kwargs):
91
93
  "provisioning_network"
92
94
  ] = result.id
93
95
 
96
+ global nb_device_query_list
97
+
98
+ try:
99
+ supported_nb_device_filters = [
100
+ "site",
101
+ "region",
102
+ "site_group",
103
+ "location",
104
+ "rack",
105
+ "tag",
106
+ "state",
107
+ ]
108
+ nb_device_query_list = yaml.safe_load(
109
+ settings.OSISM_CONDUCTOR_NETBOX_FILTER_LIST
110
+ )
111
+ if type(nb_device_query_list) is not list:
112
+ raise TypeError
113
+ for nb_device_query in nb_device_query_list:
114
+ if type(nb_device_query) is not dict:
115
+ raise TypeError
116
+ for key in list(nb_device_query.keys()):
117
+ if key not in supported_nb_device_filters:
118
+ raise ValueError
119
+ # NOTE: Only "location_id" and "rack_id" are supported by netbox
120
+ if key in ["location", "rack"]:
121
+ value_name = nb_device_query.pop(key, "")
122
+ if key == "location":
123
+ value_id = netbox.get_location_id(value_name)
124
+ elif key == "rack":
125
+ value_id = netbox.get_rack_id(value_name)
126
+ if value_id:
127
+ nb_device_query.update({key + "_id": value_id})
128
+ else:
129
+ raise ValueError(f"Invalid name {value_name} for {key}")
130
+ except (yaml.YAMLError, TypeError):
131
+ 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}"
133
+ )
134
+ nb_device_query_list = []
135
+ except ValueError as exc:
136
+ logger.error(f"Unknown value in OSISM_CONDUCTOR_NETBOX_FILTER_LIST: {exc}")
137
+ nb_device_query_list = []
138
+
94
139
 
95
140
  @app.on_after_configure.connect
96
141
  def setup_periodic_tasks(sender, **kwargs):
@@ -135,10 +180,12 @@ def sync_netbox_with_ironic(self, force_update=False):
135
180
  },
136
181
  }
137
182
 
138
- devices = list(netbox.get_devices_by_tags(["managed-by-ironic"]))
183
+ devices = set()
184
+ for nb_device_query in nb_device_query_list:
185
+ devices |= set(netbox.get_devices(**nb_device_query))
139
186
 
140
187
  # NOTE: Find nodes in Ironic which are no longer present in netbox and remove them
141
- device_names = [dev.name for dev in devices]
188
+ device_names = {dev.name for dev in devices}
142
189
  nodes = openstack.baremetal_node_list()
143
190
  for node in nodes:
144
191
  logger.info(f"Looking for {node['Name']} in netbox")
@@ -151,11 +198,6 @@ def sync_netbox_with_ironic(self, force_update=False):
151
198
  logger.info(
152
199
  f"Cleaning up baremetal node not found in netbox: {node['Name']}"
153
200
  )
154
- flavor_name = "osism-" + node["Name"]
155
- flavor = openstack.compute_flavor_get(flavor_name)
156
- if flavor:
157
- logger.info(f"Deleting flavor {flavor_name}")
158
- openstack.compute_flavor_delete(flavor)
159
201
  for port in openstack.baremetal_port_list(
160
202
  details=False, attributes=dict(node_uuid=node["UUID"])
161
203
  ):
@@ -224,19 +266,6 @@ def sync_netbox_with_ironic(self, force_update=False):
224
266
  for interface in node_interfaces
225
267
  if interface.enabled and not interface.mgmt_only and interface.mac_address
226
268
  ]
227
- flavor_attributes = {
228
- "ram": 1,
229
- "disk": 0,
230
- "vcpus": 1,
231
- "is_public": False,
232
- "extra_specs": {
233
- "resources:CUSTOM_"
234
- + device.name.upper().replace("-", "_").replace(".", "_"): "1",
235
- "resources:VCPU": "0",
236
- "resources:MEMORY_MB": "0",
237
- "resources:DISK_GB": "0",
238
- },
239
- }
240
269
 
241
270
  lock = Redlock(
242
271
  key=f"lock_osism_tasks_conductor_sync_netbox_with_ironic-{device.name}",
@@ -366,48 +395,6 @@ def sync_netbox_with_ironic(self, force_update=False):
366
395
  logger.info(
367
396
  f"Validation of management interface failed for baremetal node for {device.name}\nReason: {node_validation['management'].reason}"
368
397
  )
369
-
370
- flavor_name = "osism-" + device.name
371
- flavor = openstack.compute_flavor_get(flavor_name)
372
- if not flavor:
373
- logger.info(f"Creating flavor for {flavor_name}")
374
- flavor = openstack.compute_flavor_create(
375
- flavor_name, flavor_attributes
376
- )
377
- else:
378
- flavor_updates = {}
379
- deep_compare(flavor_attributes, flavor, flavor_updates)
380
- flavor_updates_extra_specs = flavor_updates.pop("extra_specs", None)
381
- if flavor_updates:
382
- logger.info(
383
- f"Updating flavor for {device.name} with {flavor_updates}"
384
- )
385
- openstack.compute_flavor_delete(flavor)
386
- flavor = openstack.compute_flavor_create(
387
- flavor_name, flavor_attributes
388
- )
389
- elif flavor_updates_extra_specs:
390
- logger.info(
391
- f"Updating flavor extra_specs for {device.name} with {flavor_updates_extra_specs}"
392
- )
393
- openstack.compute_flavor_update_extra_specs(
394
- flavor, flavor_updates_extra_specs
395
- )
396
- flavor = openstack.compute_flavor_get(flavor_name)
397
- for extra_specs_key in flavor["extra_specs"].keys():
398
- if (
399
- extra_specs_key
400
- not in flavor_attributes["extra_specs"].keys()
401
- ):
402
- logger.info(
403
- f"Deleting flavor extra_specs property {extra_specs_key} for {device.name}"
404
- )
405
- flavor = (
406
- openstack.compute_flavor_delete_extra_specs_property(
407
- flavor, extra_specs_key
408
- )
409
- )
410
-
411
398
  except Exception as exc:
412
399
  logger.info(
413
400
  f"Could not fully synchronize device {device.name} with ironic: {exc}"
osism/tasks/netbox.py CHANGED
@@ -1,10 +1,8 @@
1
1
  # SPDX-License-Identifier: Apache-2.0
2
2
 
3
3
  from celery import Celery
4
- from celery.signals import worker_process_init
5
4
  from loguru import logger
6
5
  from pottery import Redlock
7
- import pynetbox
8
6
 
9
7
  from osism import settings, utils
10
8
  from osism.tasks import Config, run_command
@@ -13,20 +11,6 @@ app = Celery("netbox")
13
11
  app.config_from_object(Config)
14
12
 
15
13
 
16
- @worker_process_init.connect
17
- def celery_init_worker(**kwargs):
18
- if settings.NETBOX_URL and settings.NETBOX_TOKEN:
19
- utils.nb = pynetbox.api(settings.NETBOX_URL, token=settings.NETBOX_TOKEN)
20
-
21
- if settings.IGNORE_SSL_ERRORS:
22
- import requests
23
-
24
- requests.packages.urllib3.disable_warnings()
25
- session = requests.Session()
26
- session.verify = False
27
- utils.nb.http_session = session
28
-
29
-
30
14
  @app.on_after_configure.connect
31
15
  def setup_periodic_tasks(sender, **kwargs):
32
16
  pass
@@ -37,6 +21,9 @@ def run(self, action, arguments):
37
21
  pass
38
22
 
39
23
 
24
+ # NOTE: While `get_*` tasks only operate on the netbox configured in NETBOX_URL, `set_*` tasks additionally operate on all netbox instances listed in NETBOX_SECONDARIES
25
+
26
+
40
27
  @app.task(bind=True, name="osism.tasks.netbox.set_maintenance")
41
28
  def set_maintenance(self, device_name, state=True):
42
29
  """Set the maintenance state for a device in the Netbox."""
@@ -48,14 +35,18 @@ def set_maintenance(self, device_name, state=True):
48
35
  )
49
36
  if lock.acquire(timeout=20):
50
37
  try:
51
- logger.info(f"Set maintenance state of device {device_name} = {state}")
52
-
53
- device = utils.nb.dcim.devices.get(name=device_name)
54
- if device:
55
- device.custom_fields.update({"maintenance": state})
56
- device.save()
57
- else:
58
- logger.error(f"Could not set maintenance for {device_name}")
38
+ for nb in [utils.nb] + utils.secondary_nb_list:
39
+ logger.info(
40
+ f"Set maintenance state of device {device_name} = {state} on {nb.base_url}"
41
+ )
42
+ device = nb.dcim.devices.get(name=device_name)
43
+ if device:
44
+ device.custom_fields.update({"maintenance": state})
45
+ device.save()
46
+ else:
47
+ logger.error(
48
+ f"Could not set maintenance for {device_name} on {nb.base_url}"
49
+ )
59
50
  finally:
60
51
  lock.release()
61
52
  else:
@@ -73,14 +64,19 @@ def set_provision_state(self, device_name, state):
73
64
  )
74
65
  if lock.acquire(timeout=20):
75
66
  try:
76
- logger.info(f"Set provision state of device {device_name} = {state}")
77
-
78
- device = utils.nb.dcim.devices.get(name=device_name)
79
- if device:
80
- device.custom_fields.update({"provision_state": state})
81
- device.save()
82
- else:
83
- logger.error(f"Could not set provision state for {device_name}")
67
+
68
+ for nb in [utils.nb] + utils.secondary_nb_list:
69
+ logger.info(
70
+ f"Set provision state of device {device_name} = {state} on {nb.base_url}"
71
+ )
72
+ device = nb.dcim.devices.get(name=device_name)
73
+ if device:
74
+ device.custom_fields.update({"provision_state": state})
75
+ device.save()
76
+ else:
77
+ logger.error(
78
+ f"Could not set provision state for {device_name} on {nb.base_url}"
79
+ )
84
80
  finally:
85
81
  lock.release()
86
82
  else:
@@ -98,26 +94,54 @@ def set_power_state(self, device_name, state):
98
94
  )
99
95
  if lock.acquire(timeout=20):
100
96
  try:
101
- logger.info(f"Set power state of device {device_name} = {state}")
102
-
103
- device = utils.nb.dcim.devices.get(name=device_name)
104
- if device:
105
- device.custom_fields.update({"power_state": state})
106
- device.save()
107
- else:
108
- logger.error(f"Could not set power state for {device_name}")
97
+ for nb in [utils.nb] + utils.secondary_nb_list:
98
+ logger.info(
99
+ f"Set power state of device {device_name} = {state} on {nb.base_url}"
100
+ )
101
+ device = nb.dcim.devices.get(name=device_name)
102
+ if device:
103
+ device.custom_fields.update({"power_state": state})
104
+ device.save()
105
+ else:
106
+ logger.error(
107
+ f"Could not set power state for {device_name} on {nb.base_url}"
108
+ )
109
109
  finally:
110
110
  lock.release()
111
111
  else:
112
112
  logger.error("Could not acquire lock for node {device_name}")
113
113
 
114
114
 
115
- @app.task(bind=True, name="osism.tasks.netbox.get_devices")
116
- def get_devices_by_tags(self, tags, state="active"):
117
- return utils.nb.dcim.devices.filter(tag=tags, state=state)
115
+ @app.task(bind=True, name="osism.tasks.netbox.get_location_id")
116
+ def get_location_id(self, location_name):
117
+ try:
118
+ location = utils.nb.dcim.locations.get(name=location_name)
119
+ except ValueError:
120
+ return None
121
+ if location:
122
+ return location.id
123
+ else:
124
+ return None
125
+
126
+
127
+ @app.task(bind=True, name="osism.tasks.netbox.get_rack_id")
128
+ def get_rack_id(self, rack_name):
129
+ try:
130
+ rack = utils.nb.dcim.racks.get(name=rack_name)
131
+ except ValueError:
132
+ return None
133
+ if rack:
134
+ return rack.id
135
+ else:
136
+ return None
118
137
 
119
138
 
120
139
  @app.task(bind=True, name="osism.tasks.netbox.get_devices")
140
+ def get_devices(self, **query):
141
+ return utils.nb.dcim.devices.filter(**query)
142
+
143
+
144
+ @app.task(bind=True, name="osism.tasks.netbox.get_device_by_name")
121
145
  def get_device_by_name(self, name):
122
146
  return utils.nb.dcim.devices.get(name=name)
123
147
 
osism/tasks/openstack.py CHANGED
@@ -136,50 +136,15 @@ def baremetal_port_delete(self, port_or_id):
136
136
  return result
137
137
 
138
138
 
139
- @app.task(bind=True, name="osism.tasks.openstack.compute_flavor_get")
140
- def compute_flavor_get(self, name_or_id):
141
- conn = utils.get_openstack_connection()
142
- result = conn.compute.find_flavor(
143
- name_or_id, ignore_missing=True, get_extra_specs=True
144
- )
145
- return result
146
-
147
-
148
- @app.task(bind=True, name="osism.tasks.openstack.compute_flavor_create")
149
- def compute_flavor_create(self, name, attributes=None):
150
- if attributes is None:
151
- attributes = {}
152
- attributes.update({"name": name})
153
- extra_specs = attributes.pop("extra_specs", None)
154
- conn = utils.get_openstack_connection()
155
- flavor = conn.compute.create_flavor(**attributes)
156
- if extra_specs:
157
- flavor = conn.compute.create_flavor_extra_specs(flavor, extra_specs)
158
- return flavor
159
-
160
-
161
- @app.task(bind=True, name="osism.tasks.openstack.compute_flavor_delete")
162
- def compute_flavor_delete(self, flavor):
163
- conn = utils.get_openstack_connection()
164
- conn.compute.delete_flavor(flavor, ignore_missing=True)
165
-
166
-
167
- @app.task(bind=True, name="osism.tasks.openstack.compute_flavor_update_extra_specs")
168
- def compute_flavor_update_extra_specs(self, flavor, extra_specs={}):
169
- conn = utils.get_openstack_connection()
170
- for key, value in extra_specs.items():
171
- conn.compute.update_flavor_extra_specs_property(flavor, key, value)
172
-
173
-
174
- @app.task(bind=True, name="osism.tasks.openstack.compute_flavor_delete_extra_specs")
175
- def compute_flavor_delete_extra_specs_property(self, flavor, prop):
176
- conn = utils.get_openstack_connection()
177
- conn.compute.delete_flavor_extra_specs_property(flavor, prop)
178
-
179
-
180
139
  @app.task(bind=True, name="osism.tasks.openstack.image_manager")
181
140
  def image_manager(
182
- self, *arguments, configs=None, publish=True, locking=False, auto_release_time=3600
141
+ self,
142
+ *arguments,
143
+ configs=None,
144
+ publish=True,
145
+ locking=False,
146
+ auto_release_time=3600,
147
+ ignore_env=False
183
148
  ):
184
149
  command = "/usr/local/bin/openstack-image-manager"
185
150
  if configs:
@@ -209,6 +174,7 @@ def image_manager(
209
174
  publish=publish,
210
175
  locking=locking,
211
176
  auto_release_time=auto_release_time,
177
+ ignore_env=ignore_env,
212
178
  )
213
179
  return rc
214
180
  else:
@@ -220,6 +186,7 @@ def image_manager(
220
186
  publish=publish,
221
187
  locking=locking,
222
188
  auto_release_time=auto_release_time,
189
+ ignore_env=ignore_env,
223
190
  )
224
191
 
225
192
 
osism/utils/__init__.py CHANGED
@@ -1,13 +1,34 @@
1
1
  # SPDX-License-Identifier: Apache-2.0
2
2
 
3
3
  import keystoneauth1
4
+ from loguru import logger
4
5
  import openstack
5
6
  import pynetbox
6
7
  from redis import Redis
7
8
  import urllib3
9
+ import yaml
8
10
 
9
11
  from osism import settings
10
12
 
13
+
14
+ def get_netbox_connection(netbox_url, netbox_token, ignore_ssl_errors=False):
15
+ if netbox_url and netbox_token:
16
+ nb = pynetbox.api(netbox_url, token=netbox_token)
17
+
18
+ if ignore_ssl_errors and nb:
19
+ import requests
20
+
21
+ urllib3.disable_warnings()
22
+ session = requests.Session()
23
+ session.verify = False
24
+ nb.http_session = session
25
+
26
+ else:
27
+ nb = None
28
+
29
+ return nb
30
+
31
+
11
32
  redis = Redis(
12
33
  host=settings.REDIS_HOST,
13
34
  port=settings.REDIS_PORT,
@@ -16,19 +37,53 @@ redis = Redis(
16
37
  )
17
38
  redis.ping()
18
39
 
19
- if settings.NETBOX_URL and settings.NETBOX_TOKEN:
20
- nb = pynetbox.api(settings.NETBOX_URL, token=settings.NETBOX_TOKEN)
21
-
22
- if settings.IGNORE_SSL_ERRORS and nb:
23
- import requests
24
-
25
- urllib3.disable_warnings()
26
- session = requests.Session()
27
- session.verify = False
28
- nb.http_session = session
40
+ nb = get_netbox_connection(
41
+ settings.NETBOX_URL, settings.NETBOX_TOKEN, settings.IGNORE_SSL_ERRORS
42
+ )
29
43
 
30
- else:
31
- nb = None
44
+ try:
45
+ secondary_nb_settings_list = yaml.safe_load(settings.NETBOX_SECONDARIES)
46
+ supported_secondary_nb_keys = ["NETBOX_URL", "NETBOX_TOKEN", "IGNORE_SSL_ERRORS"]
47
+ secondary_nb_list = []
48
+ if type(secondary_nb_settings_list) is not list:
49
+ raise TypeError(
50
+ f"Setting NETBOX_SECONDARIES needs to be an array of mappings containing supported netbox API configuration: {supported_secondary_nb_keys}"
51
+ )
52
+ for secondary_nb_settings in secondary_nb_settings_list:
53
+ if type(secondary_nb_settings) is not dict:
54
+ raise TypeError(
55
+ f"Elements in setting NETBOX_SECONDARIES need to be mappings containing supported netbox API configuration: {supported_secondary_nb_keys}"
56
+ )
57
+ for key in list(secondary_nb_settings.keys()):
58
+ if key not in supported_secondary_nb_keys:
59
+ raise ValueError(
60
+ f"Unknown key in element of setting NETBOX_SECONDARIES. Supported keys: {supported_secondary_nb_keys}"
61
+ )
62
+ if (
63
+ "NETBOX_URL" not in secondary_nb_settings
64
+ or not secondary_nb_settings["NETBOX_URL"]
65
+ ):
66
+ raise ValueError(
67
+ "All NETBOX_URL values in the elements of setting NETBOX_SECONDARIES need to be valid netbox URLs"
68
+ )
69
+ if (
70
+ "NETBOX_TOKEN" not in secondary_nb_settings
71
+ or not secondary_nb_settings["NETBOX_TOKEN"]
72
+ ):
73
+ raise ValueError(
74
+ "All NETBOX_TOKEN values in the elements of setting NETBOX_SECONDARIES need to be valid netbox tokens"
75
+ )
76
+
77
+ secondary_nb_list.append(
78
+ get_netbox_connection(
79
+ secondary_nb_settings["NETBOX_URL"],
80
+ secondary_nb_settings["NETBOX_TOKEN"],
81
+ secondary_nb_settings.get("IGNORE_SSL_ERRORS", True),
82
+ )
83
+ )
84
+ except (yaml.YAMLError, TypeError, ValueError) as exc:
85
+ logger.error(f"Error parsing settings NETBOX_SECONDARIES: {exc}")
86
+ secondary_nb_list = []
32
87
 
33
88
 
34
89
  def get_openstack_connection():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: osism
3
- Version: 0.20250407.0
3
+ Version: 0.20250505.0
4
4
  Summary: OSISM manager interface
5
5
  Home-page: https://github.com/osism/python-osism
6
6
  Author: OSISM GmbH
@@ -27,7 +27,7 @@ Requires-Dist: GitPython==3.1.44
27
27
  Requires-Dist: Jinja2==3.1.6
28
28
  Requires-Dist: PyYAML==6.0.2
29
29
  Requires-Dist: ara==1.7.2
30
- Requires-Dist: celery[redis]==5.5.0
30
+ Requires-Dist: celery[redis]==5.5.2
31
31
  Requires-Dist: cliff==4.9.1
32
32
  Requires-Dist: deepdiff==8.4.2
33
33
  Requires-Dist: docker==7.1.0
@@ -37,31 +37,31 @@ Requires-Dist: flower==2.0.1
37
37
  Requires-Dist: hiredis==3.1.0
38
38
  Requires-Dist: jc==1.25.4
39
39
  Requires-Dist: keystoneauth1==5.10.0
40
- Requires-Dist: kombu==5.5.2
40
+ Requires-Dist: kombu==5.5.3
41
41
  Requires-Dist: kubernetes==32.0.1
42
42
  Requires-Dist: loguru==0.7.3
43
43
  Requires-Dist: netmiko==4.5.0
44
44
  Requires-Dist: nornir-ansible==2023.12.28
45
45
  Requires-Dist: nornir==3.5.0
46
- Requires-Dist: openstacksdk==4.4.0
46
+ Requires-Dist: openstacksdk==4.5.0
47
47
  Requires-Dist: pottery==3.0.1
48
- Requires-Dist: prompt-toolkit==3.0.50
49
- Requires-Dist: pydantic==1.10.21
48
+ Requires-Dist: prompt-toolkit==3.0.51
49
+ Requires-Dist: pydantic==1.10.22
50
50
  Requires-Dist: pynetbox==7.4.1
51
51
  Requires-Dist: pytest-testinfra==10.2.2
52
52
  Requires-Dist: python-dateutil==2.9.0.post0
53
- Requires-Dist: setuptools==78.1.0
53
+ Requires-Dist: setuptools==80.3.1
54
54
  Requires-Dist: sqlmodel==0.0.24
55
55
  Requires-Dist: sushy==5.5.0
56
56
  Requires-Dist: tabulate==0.9.0
57
57
  Requires-Dist: transitions==0.9.2
58
- Requires-Dist: uvicorn[standard]==0.34.0
58
+ Requires-Dist: uvicorn[standard]==0.34.2
59
59
  Requires-Dist: watchdog==6.0.0
60
60
  Provides-Extra: ansible
61
61
  Requires-Dist: ansible-runner==2.4.1; extra == "ansible"
62
- Requires-Dist: ansible-core==2.18.4; extra == "ansible"
62
+ Requires-Dist: ansible-core==2.18.5; extra == "ansible"
63
63
  Provides-Extra: openstack-image-manager
64
- Requires-Dist: openstack-image-manager==0.20250407.0; extra == "openstack-image-manager"
64
+ Requires-Dist: openstack-image-manager==0.20250423.0; extra == "openstack-image-manager"
65
65
  Dynamic: author
66
66
  Dynamic: author-email
67
67
  Dynamic: classifier
@@ -1,8 +1,8 @@
1
1
  osism/__init__.py,sha256=1UiNTBus0V0f2AbZQzAtVtu6zkfCCrw0OTq--NwFAqY,341
2
2
  osism/__main__.py,sha256=ILe4gu61xEISiBsxanqTQIdSkV-YhpZXTRlguCYyssk,141
3
- osism/api.py,sha256=Lvkdd92tvv9RtoMs9RtvqsN3DiSKPdSll24J3wRzbBY,4793
3
+ osism/api.py,sha256=xJC6RyC1TU54PU2C06rlialh2SmTCgM1L0MSgyUlubU,4331
4
4
  osism/main.py,sha256=Dt2-9sLXcS-Ny4DAz7hrha-KRc7zd7BFUTRdfs_X8z4,893
5
- osism/settings.py,sha256=m__DltxKQo5D-vDKKwY8RNBVs5bverYdJmtyVyln_6o,1049
5
+ osism/settings.py,sha256=xzFRbf5mdl_MUAUFqPavE14vaVuTr5z6b969vJQvF2E,1306
6
6
  osism/actions/__init__.py,sha256=bG7Ffen4LvQtgnYPFEpFccsWs81t4zqqeqn9ZeirH6E,38
7
7
  osism/commands/__init__.py,sha256=Ag4wX_DCgXRdoLn6t069jqb3DdRylsX2nyYkiyCx4uk,456
8
8
  osism/commands/apply.py,sha256=n3lLb1cS3GahQqRT0723di98hg47MjVzDzkAoeZX7qU,16780
@@ -13,11 +13,11 @@ osism/commands/console.py,sha256=8BPz1hio5Wi6kONVAWFuSqkDRrMcLEYeFIY8dbtN6e4,321
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=E0ZF4Bf91cgttkSVt1dOQ4nQbRUSDAgsgPOjJMDsGBk,11932
16
+ osism/commands/manage.py,sha256=5ypZzA91QGgWCt35rVNJSXVC9l189IntS4UnsD8LRZY,11971
17
17
  osism/commands/netbox.py,sha256=_2-j6XM9JvH0DXnbct6rG9T6hT8KEpm3vazQC28Rt7I,4529
18
18
  osism/commands/noset.py,sha256=7zDFuFMyNpo7DUOKcNiYV8nodtdMOYFp5LDPcuJhlZ8,1481
19
19
  osism/commands/reconciler.py,sha256=Ja_b86gX6-_Pr3DmrUUvskmEnnJpHQ-XJNQLycMJeyc,2818
20
- osism/commands/server.py,sha256=zFXRdYoj4ZNDJNPSaGddMPEWxt8G2GyMomPOcCOaN3c,4137
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
@@ -25,7 +25,7 @@ osism/commands/sync.py,sha256=Vf9k7uVQTIu-8kK1u7Gjs3et3RRBEkmnNikot_PFJIE,484
25
25
  osism/commands/task.py,sha256=mwJJ7a71Lw3o_FX7j3rR0-NbPdPwMDOjbOAiiXE4uGc,543
26
26
  osism/commands/validate.py,sha256=hIQB0zk4xIBZJORtBp_tWrXTRKKhB2qi6j-mznDxKR4,4191
27
27
  osism/commands/vault.py,sha256=Ip0IMR7zaBkPbLJenXr4ZwxM6FnozZ9wn9rwHmFHo8s,1818
28
- osism/commands/volume.py,sha256=SqD9pYgtcYnMu6sB2pG8lfrLHRq6GzOb_-RkWOOVZPo,3156
28
+ osism/commands/volume.py,sha256=l6oAk__dFM8KKdLTWOvuSiI7tLh9wAPZp8hwmYF-NX0,6595
29
29
  osism/commands/wait.py,sha256=mKFDqEXcaLlKw1T3MuBEZpNh7CeL3lpUXgubD2_f8es,6580
30
30
  osism/commands/worker.py,sha256=iraCOEhCp7WgfjfZ0-12XQYQPUjpi9rSJK5Z9JfNJk4,1651
31
31
  osism/core/__init__.py,sha256=bG7Ffen4LvQtgnYPFEpFccsWs81t4zqqeqn9ZeirH6E,38
@@ -35,21 +35,21 @@ osism/data/__init__.py,sha256=izXdh0J3vPLQI7kBhJI7ibJQzPqU_nlONP0L4Cf_k6A,1504
35
35
  osism/plugins/__init__.py,sha256=bG7Ffen4LvQtgnYPFEpFccsWs81t4zqqeqn9ZeirH6E,38
36
36
  osism/services/__init__.py,sha256=bG7Ffen4LvQtgnYPFEpFccsWs81t4zqqeqn9ZeirH6E,38
37
37
  osism/services/listener.py,sha256=eEamlQsJqCuU9K2QFmk3yM9LAJZEanVcTLtGMsNCKjs,9783
38
- osism/tasks/__init__.py,sha256=lrSkcZtbzhWsLS4hWadKfpP_tCd1pX1IhvrBU3EhKmM,8605
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=Qg4ic9j5khHGumXCRaosrDiphs4-Eqk02BCb78zuTkM,19162
41
+ osism/tasks/conductor.py,sha256=jtvw5UfhPOPEbIBkIyBkiHbaPJsNKbEABbc3FtHYOo4,18210
42
42
  osism/tasks/kolla.py,sha256=wJQpWn_01iWLkr7l7T7RNrQGfRgsgmYi4WQlTmNGvew,618
43
43
  osism/tasks/kubernetes.py,sha256=VzXq_VrYU_CLm4cOruqnE3Kq2ydfO9glZ3p0bp3OYoc,625
44
- osism/tasks/netbox.py,sha256=qT3-0GWDPCnejLGAhNp7InMSxBTk7qmKwfdNn1in3FM,4857
45
- osism/tasks/openstack.py,sha256=ZFdgudp02a9I4AiJae2Pu0_k9REYi4P7wTLA5rzx8is,7825
44
+ osism/tasks/netbox.py,sha256=QVOLiTH2Su237YAS0QfXbQ86E-OA1JzrFDfyi9JBmvk,5658
45
+ osism/tasks/openstack.py,sha256=g15tCll5vP1pC6ysxRCTZxplsdGmXbxaCH3k1Qdv5Xg,6367
46
46
  osism/tasks/reconciler.py,sha256=RGUcax2gDuyVLw1nGRQn5izXclnPBo9MRl0ndLDiiYQ,2707
47
- osism/utils/__init__.py,sha256=DP2D7xyXnfWuH-c26elIwdwrMSY-oSkVsLFKsQfna9w,1477
48
- osism-0.20250407.0.dist-info/licenses/AUTHORS,sha256=EKFIR9F27AvoEXp1cA6FkGbjEOFt4Rcbipr5RJc7jSs,64
49
- osism-0.20250407.0.dist-info/licenses/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
50
- osism-0.20250407.0.dist-info/METADATA,sha256=TIDKl4LiJsyH7yJLztBxKbVc3BWnaKMdHxXRZ377Reg,2972
51
- osism-0.20250407.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
52
- osism-0.20250407.0.dist-info/entry_points.txt,sha256=DlfrvU14rI55WuTrwNRoce9FY3ric4HeZKZx_Z3NzCw,3015
53
- osism-0.20250407.0.dist-info/pbr.json,sha256=7g2xfwKFA8HvM2aPcqC5h0SXZ_VjJYyEL03mJcdLU6A,47
54
- osism-0.20250407.0.dist-info/top_level.txt,sha256=8L8dsI9hcaGHsdnR4k_LN9EM78EhwrXRFHyAryPXZtY,6
55
- osism-0.20250407.0.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (80.3.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1 @@
1
+ janhorstmann <horstmann@osism.tech>
@@ -0,0 +1 @@
1
+ {"git_version": "6139ca4", "is_release": false}
@@ -1 +0,0 @@
1
- renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
@@ -1 +0,0 @@
1
- {"git_version": "976a50a", "is_release": false}