osism 0.20251003.0__py3-none-any.whl → 0.20251012.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/commands/server.py CHANGED
@@ -108,6 +108,18 @@ class ServerList(Command):
108
108
  parser.add_argument(
109
109
  "--project-domain", help="Domain of the project", type=str, default=None
110
110
  )
111
+ parser.add_argument(
112
+ "--user",
113
+ default=None,
114
+ type=str,
115
+ help="Only list servers for the given user (name or ID)",
116
+ )
117
+ parser.add_argument(
118
+ "--user-domain",
119
+ default=None,
120
+ type=str,
121
+ help="Domain the user belongs to (name or ID)",
122
+ )
111
123
  return parser
112
124
 
113
125
  def take_action(self, parsed_args):
@@ -115,8 +127,31 @@ class ServerList(Command):
115
127
  domain = parsed_args.domain
116
128
  project = parsed_args.project
117
129
  project_domain = parsed_args.project_domain
130
+ user = parsed_args.user
131
+ user_domain = parsed_args.user_domain
118
132
 
119
133
  result = []
134
+
135
+ # Handle user lookup if --user is specified
136
+ user_id = None
137
+ if user:
138
+ user_query = {}
139
+
140
+ if user_domain:
141
+ u_d = conn.identity.find_domain(user_domain, ignore_missing=True)
142
+ if u_d and "id" in u_d:
143
+ user_query = dict(domain_id=u_d.id)
144
+ else:
145
+ logger.error(f"No domain found for {user_domain}")
146
+ return
147
+
148
+ u = conn.identity.find_user(user, ignore_missing=True, **user_query)
149
+ if u and "id" in u:
150
+ user_id = u.id
151
+ else:
152
+ logger.error(f"No user found for {user}")
153
+ return
154
+
120
155
  if domain:
121
156
  _domain = conn.identity.find_domain(domain)
122
157
  if not _domain:
@@ -131,6 +166,7 @@ class ServerList(Command):
131
166
  [
132
167
  project.name,
133
168
  project.id,
169
+ server.user_id if hasattr(server, "user_id") else None,
134
170
  server.id,
135
171
  server.name,
136
172
  server.flavor["original_name"],
@@ -141,7 +177,15 @@ class ServerList(Command):
141
177
  print(
142
178
  tabulate(
143
179
  result,
144
- headers=["Project", "Project ID", "ID", "Name", "Flavor", "Status"],
180
+ headers=[
181
+ "Project",
182
+ "Project ID",
183
+ "User ID",
184
+ "ID",
185
+ "Name",
186
+ "Flavor",
187
+ "Status",
188
+ ],
145
189
  tablefmt="psql",
146
190
  )
147
191
  )
@@ -161,9 +205,20 @@ class ServerList(Command):
161
205
  return
162
206
  query = {"project_id": _project.id}
163
207
 
208
+ # Get domain name from project
209
+ domain_name = None
210
+ if hasattr(_project, "domain_id") and _project.domain_id:
211
+ try:
212
+ _domain = conn.identity.get_domain(_project.domain_id)
213
+ domain_name = _domain.name if _domain else _project.domain_id
214
+ except Exception:
215
+ domain_name = _project.domain_id
216
+
164
217
  for server in conn.compute.servers(all_projects=True, **query):
165
218
  result.append(
166
219
  [
220
+ domain_name,
221
+ server.user_id if hasattr(server, "user_id") else None,
167
222
  server.id,
168
223
  server.name,
169
224
  server.flavor["original_name"],
@@ -174,7 +229,47 @@ class ServerList(Command):
174
229
  print(
175
230
  tabulate(
176
231
  result,
177
- headers=["ID", "Name", "Flavor", "Status"],
232
+ headers=["Domain", "User ID", "ID", "Name", "Flavor", "Status"],
233
+ tablefmt="psql",
234
+ )
235
+ )
236
+
237
+ elif user_id:
238
+ query = {"user_id": user_id}
239
+
240
+ for server in conn.compute.servers(all_projects=True, **query):
241
+ # Get domain name from project
242
+ domain_name = None
243
+ if hasattr(server, "project_id") and server.project_id:
244
+ try:
245
+ _project = conn.identity.get_project(server.project_id)
246
+ if (
247
+ _project
248
+ and hasattr(_project, "domain_id")
249
+ and _project.domain_id
250
+ ):
251
+ _domain = conn.identity.get_domain(_project.domain_id)
252
+ domain_name = (
253
+ _domain.name if _domain else _project.domain_id
254
+ )
255
+ except Exception:
256
+ domain_name = None
257
+
258
+ result.append(
259
+ [
260
+ domain_name,
261
+ server.project_id if hasattr(server, "project_id") else None,
262
+ server.id,
263
+ server.name,
264
+ server.flavor["original_name"],
265
+ server.status,
266
+ ]
267
+ )
268
+
269
+ print(
270
+ tabulate(
271
+ result,
272
+ headers=["Domain", "Project ID", "ID", "Name", "Flavor", "Status"],
178
273
  tablefmt="psql",
179
274
  )
180
275
  )
@@ -0,0 +1,213 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ import subprocess
4
+
5
+ from cliff.command import Command
6
+ from loguru import logger
7
+
8
+
9
+ class OpenStackStress(Command):
10
+ """Run OpenStack stress testing tool"""
11
+
12
+ def get_parser(self, prog_name):
13
+ parser = super(OpenStackStress, self).get_parser(prog_name)
14
+
15
+ # Boolean flags
16
+ parser.add_argument(
17
+ "--no-cleanup",
18
+ action="store_true",
19
+ help="Do not clean up resources after test",
20
+ )
21
+ parser.add_argument(
22
+ "--debug",
23
+ action="store_true",
24
+ help="Enable debug mode",
25
+ )
26
+ parser.add_argument(
27
+ "--no-delete",
28
+ action="store_true",
29
+ help="Do not delete resources",
30
+ )
31
+ parser.add_argument(
32
+ "--no-volume",
33
+ action="store_true",
34
+ help="Do not create volumes",
35
+ )
36
+ parser.add_argument(
37
+ "--no-boot-volume",
38
+ action="store_true",
39
+ help="Do not use boot volumes",
40
+ )
41
+ parser.add_argument(
42
+ "--no-wait",
43
+ action="store_true",
44
+ help="Do not wait for resources",
45
+ )
46
+
47
+ # Integer parameters with defaults
48
+ parser.add_argument(
49
+ "--interval",
50
+ type=int,
51
+ default=10,
52
+ help="Interval in seconds (default: %(default)s)",
53
+ )
54
+ parser.add_argument(
55
+ "--number",
56
+ type=int,
57
+ default=1,
58
+ help="Number of instances (default: %(default)s)",
59
+ )
60
+ parser.add_argument(
61
+ "--parallel",
62
+ type=int,
63
+ default=1,
64
+ help="Parallel operations (default: %(default)s)",
65
+ )
66
+ parser.add_argument(
67
+ "--timeout",
68
+ type=int,
69
+ default=600,
70
+ help="Timeout in seconds (default: %(default)s)",
71
+ )
72
+ parser.add_argument(
73
+ "--volume-number",
74
+ type=int,
75
+ default=1,
76
+ help="Number of volumes per instance (default: %(default)s)",
77
+ )
78
+ parser.add_argument(
79
+ "--volume-size",
80
+ type=int,
81
+ default=1,
82
+ help="Volume size in GB (default: %(default)s)",
83
+ )
84
+ parser.add_argument(
85
+ "--boot-volume-size",
86
+ type=int,
87
+ default=20,
88
+ help="Boot volume size in GB (default: %(default)s)",
89
+ )
90
+
91
+ # String parameters with defaults
92
+ parser.add_argument(
93
+ "--cloud",
94
+ type=str,
95
+ default="simple-stress",
96
+ help="Cloud name in clouds.yaml (default: %(default)s)",
97
+ )
98
+ parser.add_argument(
99
+ "--flavor",
100
+ type=str,
101
+ default="SCS-1V-2",
102
+ help="Flavor name (default: %(default)s)",
103
+ )
104
+ parser.add_argument(
105
+ "--image",
106
+ type=str,
107
+ default="Ubuntu 24.04",
108
+ help="Image name (default: %(default)s)",
109
+ )
110
+ parser.add_argument(
111
+ "--subnet-cidr",
112
+ type=str,
113
+ default="10.100.0.0/16",
114
+ help="Subnet CIDR (default: %(default)s)",
115
+ )
116
+ parser.add_argument(
117
+ "--prefix",
118
+ type=str,
119
+ default="simple-stress",
120
+ help="Resource name prefix (default: %(default)s)",
121
+ )
122
+ parser.add_argument(
123
+ "--compute-zone",
124
+ type=str,
125
+ default="nova",
126
+ help="Compute availability zone (default: %(default)s)",
127
+ )
128
+ parser.add_argument(
129
+ "--storage-zone",
130
+ type=str,
131
+ default="nova",
132
+ help="Storage availability zone (default: %(default)s)",
133
+ )
134
+ parser.add_argument(
135
+ "--affinity",
136
+ type=str,
137
+ default="soft-anti-affinity",
138
+ choices=[
139
+ "soft-affinity",
140
+ "soft-anti-affinity",
141
+ "affinity",
142
+ "anti-affinity",
143
+ ],
144
+ help="Server group policy (default: %(default)s)",
145
+ )
146
+ parser.add_argument(
147
+ "--volume-type",
148
+ type=str,
149
+ default="__DEFAULT__",
150
+ help="Volume type (default: %(default)s)",
151
+ )
152
+
153
+ return parser
154
+
155
+ def take_action(self, parsed_args):
156
+ """Execute the OpenStack stress testing tool"""
157
+
158
+ # Build the command
159
+ command = [
160
+ "python3",
161
+ "/openstack-simple-stress/openstack_simple_stress/main.py",
162
+ ]
163
+
164
+ # Add boolean flags
165
+ if parsed_args.no_cleanup:
166
+ command.append("--no-cleanup")
167
+ if parsed_args.debug:
168
+ command.append("--debug")
169
+ if parsed_args.no_delete:
170
+ command.append("--no-delete")
171
+ if parsed_args.no_volume:
172
+ command.append("--no-volume")
173
+ if parsed_args.no_boot_volume:
174
+ command.append("--no-boot-volume")
175
+ if parsed_args.no_wait:
176
+ command.append("--no-wait")
177
+
178
+ # Add integer parameters
179
+ command.extend(["--interval", str(parsed_args.interval)])
180
+ command.extend(["--number", str(parsed_args.number)])
181
+ command.extend(["--parallel", str(parsed_args.parallel)])
182
+ command.extend(["--timeout", str(parsed_args.timeout)])
183
+ command.extend(["--volume-number", str(parsed_args.volume_number)])
184
+ command.extend(["--volume-size", str(parsed_args.volume_size)])
185
+ command.extend(["--boot-volume-size", str(parsed_args.boot_volume_size)])
186
+
187
+ # Add string parameters
188
+ command.extend(["--cloud", parsed_args.cloud])
189
+ command.extend(["--flavor", parsed_args.flavor])
190
+ command.extend(["--image", parsed_args.image])
191
+ command.extend(["--subnet-cidr", parsed_args.subnet_cidr])
192
+ command.extend(["--prefix", parsed_args.prefix])
193
+ command.extend(["--compute-zone", parsed_args.compute_zone])
194
+ command.extend(["--storage-zone", parsed_args.storage_zone])
195
+ command.extend(["--affinity", parsed_args.affinity])
196
+ command.extend(["--volume-type", parsed_args.volume_type])
197
+
198
+ logger.debug(
199
+ f"Executing OpenStack stress test with command: {' '.join(command)}"
200
+ )
201
+
202
+ # Execute the stress tool
203
+ try:
204
+ result = subprocess.run(command, check=False)
205
+ return result.returncode
206
+ except FileNotFoundError:
207
+ logger.error(
208
+ "OpenStack stress tool not found at /openstack-simple-stress/openstack_simple_stress/main.py"
209
+ )
210
+ return 1
211
+ except Exception as e:
212
+ logger.error(f"Error executing OpenStack stress tool: {e}")
213
+ return 1
osism/data/__init__.py CHANGED
@@ -101,3 +101,37 @@ images:
101
101
  build_date: {{ image_builddate }}
102
102
 
103
103
  """
104
+
105
+ TEMPLATE_IMAGE_CLUSTERAPI_GARDENER = """---
106
+ images:
107
+ - name: ubuntu-capi-image-gardener
108
+ enable: true
109
+ keep: true
110
+ separator: "-"
111
+ format: qcow2
112
+ login: ubuntu
113
+ min_disk: 20
114
+ min_ram: 512
115
+ status: active
116
+ visibility: public
117
+ multi: false
118
+ meta:
119
+ architecture: x86_64
120
+ hw_disk_bus: scsi
121
+ hw_rng_model: virtio
122
+ hw_scsi_model: virtio-scsi
123
+ hw_watchdog_action: reset
124
+ hypervisor_type: qemu
125
+ os_distro: ubuntu
126
+ replace_frequency: never
127
+ uuid_validity: none
128
+ provided_until: none
129
+ os_purpose: k8snode
130
+ tags: []
131
+ versions:
132
+ - version: "v{{ image_version }}"
133
+ url: "{{ image_url }}"
134
+ checksum: "{{ image_checksum }}"
135
+ build_date: {{ image_builddate }}
136
+
137
+ """
osism/data/enums.py CHANGED
@@ -132,7 +132,7 @@ MAP_ROLE2ROLE = {
132
132
  [
133
133
  "keystone",
134
134
  [
135
- ["neutron", ["wait-for-nova", ["octavia"]]],
135
+ ["neutron", [["wait-for-nova", ["octavia"]]]],
136
136
  "barbican",
137
137
  "designate",
138
138
  "ironic",
@@ -207,7 +207,7 @@ MAP_ROLE2ROLE = {
207
207
  [
208
208
  "glance",
209
209
  "cinder",
210
- ["neutron", ["octavia"]],
210
+ ["neutron", [["wait-for-nova", ["octavia"]]]],
211
211
  "designate",
212
212
  ["placement", ["nova"]],
213
213
  ],
@@ -220,10 +220,9 @@ MAP_ROLE2ROLE = {
220
220
  [
221
221
  "glance",
222
222
  "cinder",
223
- "neutron",
224
223
  "barbican",
225
224
  "designate",
226
- "octavia",
225
+ ["neutron", [["wait-for-nova", ["octavia"]]]],
227
226
  "ironic",
228
227
  "kolla-ceph-rgw",
229
228
  "magnum",
@@ -300,4 +299,55 @@ MAP_ROLE2ROLE = {
300
299
  ],
301
300
  ],
302
301
  ],
302
+ "cloudpod-infrastructure": [
303
+ "openstackclient",
304
+ "phpmyadmin",
305
+ [
306
+ "common",
307
+ [
308
+ ["loadbalancer", ["letsencrypt", "opensearch", "mariadb-ng"]],
309
+ ["openvswitch", ["ovn"]],
310
+ "memcached",
311
+ "redis",
312
+ "rabbitmq-ng",
313
+ ],
314
+ ],
315
+ ],
316
+ "cloudpod-openstack": [
317
+ "horizon",
318
+ [
319
+ "keystone",
320
+ [
321
+ "glance",
322
+ "cinder",
323
+ ["neutron", [["wait-for-nova", ["octavia"]]]],
324
+ ["placement", ["nova"]],
325
+ "designate",
326
+ "skyline",
327
+ "kolla-ceph-rgw",
328
+ ],
329
+ ],
330
+ ],
331
+ "cloudpod-ceph": [
332
+ [
333
+ "ceph-create-lvm-devices",
334
+ [
335
+ "facts",
336
+ [
337
+ "ceph",
338
+ [
339
+ [
340
+ "ceph-pools",
341
+ [
342
+ [
343
+ "copy-ceph-keys",
344
+ [["cephclient", ["ceph-bootstrap-dashboard"]]],
345
+ ]
346
+ ],
347
+ ],
348
+ ],
349
+ ],
350
+ ],
351
+ ],
352
+ ],
303
353
  }
osism/tasks/__init__.py CHANGED
@@ -13,6 +13,9 @@ from loguru import logger
13
13
 
14
14
  from osism import utils
15
15
 
16
+ # Regex pattern for extracting hosts from Ansible output
17
+ HOST_PATTERN = re.compile(r"^(ok|changed|failed|skipping|unreachable):\s+\[([^\]]+)\]")
18
+
16
19
 
17
20
  class Config:
18
21
  broker_connection_retry_on_startup = True
@@ -88,7 +91,7 @@ def get_container_version(worker):
88
91
 
89
92
 
90
93
  def log_play_execution(
91
- request_id, worker, environment, role, hosts=None, result="started"
94
+ request_id, worker, environment, role, hosts=None, arguments=None, result="started"
92
95
  ):
93
96
  """Log Ansible play execution to central tracking file.
94
97
 
@@ -98,6 +101,7 @@ def log_play_execution(
98
101
  environment: The environment parameter
99
102
  role: The playbook/role that was executed
100
103
  hosts: List of hosts the play was executed against (default: empty list)
104
+ arguments: Command-line arguments passed to ansible-playbook (default: None)
101
105
  result: Execution result - "started", "success", or "failure"
102
106
  """
103
107
  log_file = Path("/share/ansible-execution-history.json")
@@ -105,6 +109,10 @@ def log_play_execution(
105
109
  # Get runtime version from YAML version file
106
110
  runtime_version = get_container_version(worker)
107
111
 
112
+ # Use provided hosts or empty list
113
+ if hosts is None:
114
+ hosts = []
115
+
108
116
  execution_record = {
109
117
  "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
110
118
  "request_id": request_id,
@@ -112,7 +120,8 @@ def log_play_execution(
112
120
  "worker_version": runtime_version,
113
121
  "environment": environment,
114
122
  "role": role,
115
- "hosts": hosts if isinstance(hosts, list) else [],
123
+ "hosts": hosts,
124
+ "arguments": arguments if arguments else "",
116
125
  "result": result,
117
126
  }
118
127
 
@@ -143,6 +152,7 @@ def run_ansible_in_environment(
143
152
  auto_release_time=3600,
144
153
  ):
145
154
  result = ""
155
+ extracted_hosts = set() # Local set for host deduplication
146
156
 
147
157
  if type(arguments) == list:
148
158
  joined_arguments = " ".join(arguments)
@@ -181,7 +191,8 @@ def run_ansible_in_environment(
181
191
  worker=worker,
182
192
  environment=environment,
183
193
  role=role,
184
- hosts=None, # Host extraction would require inventory parsing
194
+ hosts=None, # Hosts will be empty at start, filled at completion
195
+ arguments=joined_arguments,
185
196
  result="started",
186
197
  )
187
198
 
@@ -269,6 +280,13 @@ def run_ansible_in_environment(
269
280
 
270
281
  while p.poll() is None:
271
282
  line = p.stdout.readline().decode("utf-8")
283
+
284
+ # Extract hosts from Ansible output
285
+ match = HOST_PATTERN.match(line.strip())
286
+ if match:
287
+ hostname = match.group(2)
288
+ extracted_hosts.add(hostname) # Local set (automatic deduplication)
289
+
272
290
  if publish:
273
291
  utils.push_task_output(request_id, line)
274
292
  result += line
@@ -281,7 +299,8 @@ def run_ansible_in_environment(
281
299
  worker=worker,
282
300
  environment=environment,
283
301
  role=role,
284
- hosts=None, # Host extraction would require inventory parsing
302
+ hosts=sorted(list(extracted_hosts)), # Direct pass of extracted hosts
303
+ arguments=joined_arguments,
285
304
  result="success" if rc == 0 else "failure",
286
305
  )
287
306
 
osism/tasks/openstack.py CHANGED
@@ -452,3 +452,83 @@ def flavor_manager(
452
452
  locking=locking,
453
453
  auto_release_time=auto_release_time,
454
454
  )
455
+
456
+
457
+ @app.task(bind=True, name="osism.tasks.openstack.project_manager")
458
+ def project_manager(
459
+ self,
460
+ *arguments,
461
+ publish=True,
462
+ locking=False,
463
+ auto_release_time=3600,
464
+ cloud=None,
465
+ ):
466
+ # Check if tasks are locked before execution
467
+ utils.check_task_lock_and_exit()
468
+
469
+ command = "/usr/local/bin/python3"
470
+ script_path = "/openstack-project-manager/openstack_project_manager/create.py"
471
+ # Prepend script path to arguments
472
+ full_arguments = [script_path] + list(arguments)
473
+
474
+ # Setup cloud environment (changes to /tmp)
475
+ temp_files_to_cleanup, original_cwd, cloud_setup_success = setup_cloud_environment(
476
+ cloud
477
+ )
478
+
479
+ try:
480
+ # Change to working directory required by openstack-project-manager
481
+ os.chdir("/openstack-project-manager")
482
+
483
+ return run_command(
484
+ self.request.id,
485
+ command,
486
+ {},
487
+ *full_arguments,
488
+ publish=publish,
489
+ locking=locking,
490
+ auto_release_time=auto_release_time,
491
+ ignore_env=True,
492
+ )
493
+ finally:
494
+ cleanup_cloud_environment(temp_files_to_cleanup, original_cwd)
495
+
496
+
497
+ @app.task(bind=True, name="osism.tasks.openstack.project_manager_sync")
498
+ def project_manager_sync(
499
+ self,
500
+ *arguments,
501
+ publish=True,
502
+ locking=False,
503
+ auto_release_time=3600,
504
+ cloud=None,
505
+ ):
506
+ # Check if tasks are locked before execution
507
+ utils.check_task_lock_and_exit()
508
+
509
+ command = "/usr/local/bin/python3"
510
+ script_path = "/openstack-project-manager/openstack_project_manager/manage.py"
511
+ # Prepend script path to arguments
512
+ full_arguments = [script_path] + list(arguments)
513
+
514
+ # Setup cloud environment (changes to /tmp)
515
+ temp_files_to_cleanup, original_cwd, cloud_setup_success = setup_cloud_environment(
516
+ cloud
517
+ )
518
+
519
+ try:
520
+ # Change to working directory required by openstack-project-manager
521
+ os.chdir("/openstack-project-manager")
522
+
523
+ return run_command(
524
+ self.request.id,
525
+ command,
526
+ {},
527
+ *full_arguments,
528
+ publish=publish,
529
+ locking=locking,
530
+ auto_release_time=auto_release_time,
531
+ ignore_env=True,
532
+ )
533
+ finally:
534
+ cleanup_cloud_environment(temp_files_to_cleanup, original_cwd)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: osism
3
- Version: 0.20251003.0
3
+ Version: 0.20251012.0
4
4
  Summary: OSISM manager interface
5
5
  Home-page: https://github.com/osism/python-osism
6
6
  Author: OSISM GmbH
@@ -59,7 +59,7 @@ Requires-Dist: watchdog==6.0.0
59
59
  Requires-Dist: websockets==15.0.1
60
60
  Provides-Extra: ansible
61
61
  Requires-Dist: ansible-runner==2.4.1; extra == "ansible"
62
- Requires-Dist: ansible-core==2.19.2; extra == "ansible"
62
+ Requires-Dist: ansible-core==2.19.3; extra == "ansible"
63
63
  Provides-Extra: openstack-image-manager
64
64
  Requires-Dist: openstack-image-manager==0.20250912.0; extra == "openstack-image-manager"
65
65
  Dynamic: author