python-watcher 15.1.0__py3-none-any.whl → 15.1.1__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.
Files changed (26) hide show
  1. {python_watcher-15.1.0.dist-info → python_watcher-15.1.1.dist-info}/METADATA +1 -1
  2. {python_watcher-15.1.0.dist-info → python_watcher-15.1.1.dist-info}/RECORD +25 -25
  3. {python_watcher-15.1.0.dist-info → python_watcher-15.1.1.dist-info}/WHEEL +1 -1
  4. python_watcher-15.1.1.dist-info/pbr.json +1 -0
  5. watcher/common/cinder_helper.py +58 -16
  6. watcher/common/service.py +2 -2
  7. watcher/conf/grafana_client.py +1 -0
  8. watcher/conf/maas_client.py +1 -0
  9. watcher/decision_engine/model/element/base.py +4 -1
  10. watcher/decision_engine/model/model_root.py +13 -2
  11. watcher/decision_engine/strategy/strategies/zone_migration.py +34 -11
  12. watcher/tests/api/v1/test_audits.py +33 -0
  13. watcher/tests/common/test_cinder_helper.py +206 -16
  14. watcher/tests/common/test_service.py +22 -0
  15. watcher/tests/decision_engine/model/test_model.py +39 -9
  16. watcher/tests/decision_engine/strategy/strategies/test_zone_migration.py +109 -0
  17. python_watcher-15.1.0.dist-info/pbr.json +0 -1
  18. {python_watcher-15.1.0.data → python_watcher-15.1.1.data}/data/etc/apache2/watcher +0 -0
  19. {python_watcher-15.1.0.data → python_watcher-15.1.1.data}/data/etc/watcher/README-watcher.conf.txt +0 -0
  20. {python_watcher-15.1.0.data → python_watcher-15.1.1.data}/data/etc/watcher/oslo-config-generator/watcher.conf +0 -0
  21. {python_watcher-15.1.0.data → python_watcher-15.1.1.data}/data/etc/watcher/oslo-policy-generator/watcher-policy-generator.conf +0 -0
  22. {python_watcher-15.1.0.data → python_watcher-15.1.1.data}/scripts/watcher-api-wsgi +0 -0
  23. {python_watcher-15.1.0.dist-info → python_watcher-15.1.1.dist-info}/entry_points.txt +0 -0
  24. {python_watcher-15.1.0.dist-info → python_watcher-15.1.1.dist-info}/licenses/AUTHORS +0 -0
  25. {python_watcher-15.1.0.dist-info → python_watcher-15.1.1.dist-info}/licenses/LICENSE +0 -0
  26. {python_watcher-15.1.0.dist-info → python_watcher-15.1.1.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-watcher
3
- Version: 15.1.0
3
+ Version: 15.1.1
4
4
  Summary: OpenStack Watcher provides a flexible and scalable resource optimization service for multi-tenant OpenStack-based clouds.
5
5
  Home-page: https://docs.openstack.org/watcher/latest/
6
6
  Author: OpenStack
@@ -1,10 +1,10 @@
1
- python_watcher-15.1.0.data/data/etc/apache2/watcher,sha256=urybYUTb0kAVIvbm4WK5oDfXR82QSK2lyMl4Z9dCz4Q,1190
2
- python_watcher-15.1.0.data/data/etc/watcher/README-watcher.conf.txt,sha256=EIa_sjXDyl1XSg0ovsFwUjqw0euS0N2PYn4HtyBGJME,131
3
- python_watcher-15.1.0.data/data/etc/watcher/oslo-config-generator/watcher.conf,sha256=O7g1DZqgXvbQgkBfot0dR1ar1d-3SBkOU_ZK4Dz7SFc,406
4
- python_watcher-15.1.0.data/data/etc/watcher/oslo-policy-generator/watcher-policy-generator.conf,sha256=ZfIuVdtrj7IZm2BrRQvAW3JOL_kqCiILf-vBmwX3ufw,76
5
- python_watcher-15.1.0.data/scripts/watcher-api-wsgi,sha256=YcIpUO3iRVW6Fu3Md_ooAeHkmJJasG031naMdf8cv2A,1829
6
- python_watcher-15.1.0.dist-info/licenses/AUTHORS,sha256=_WO5_45pJAVeBpD3J-G8AFeMPJyA5OV0Fzms6SOsqO4,8168
7
- python_watcher-15.1.0.dist-info/licenses/LICENSE,sha256=XfKg2H1sVi8OoRxoisUlMqoo10TKvHmU_wU39ks7MyA,10143
1
+ python_watcher-15.1.1.data/data/etc/apache2/watcher,sha256=urybYUTb0kAVIvbm4WK5oDfXR82QSK2lyMl4Z9dCz4Q,1190
2
+ python_watcher-15.1.1.data/data/etc/watcher/README-watcher.conf.txt,sha256=EIa_sjXDyl1XSg0ovsFwUjqw0euS0N2PYn4HtyBGJME,131
3
+ python_watcher-15.1.1.data/data/etc/watcher/oslo-config-generator/watcher.conf,sha256=O7g1DZqgXvbQgkBfot0dR1ar1d-3SBkOU_ZK4Dz7SFc,406
4
+ python_watcher-15.1.1.data/data/etc/watcher/oslo-policy-generator/watcher-policy-generator.conf,sha256=ZfIuVdtrj7IZm2BrRQvAW3JOL_kqCiILf-vBmwX3ufw,76
5
+ python_watcher-15.1.1.data/scripts/watcher-api-wsgi,sha256=YcIpUO3iRVW6Fu3Md_ooAeHkmJJasG031naMdf8cv2A,1829
6
+ python_watcher-15.1.1.dist-info/licenses/AUTHORS,sha256=_WO5_45pJAVeBpD3J-G8AFeMPJyA5OV0Fzms6SOsqO4,8168
7
+ python_watcher-15.1.1.dist-info/licenses/LICENSE,sha256=XfKg2H1sVi8OoRxoisUlMqoo10TKvHmU_wU39ks7MyA,10143
8
8
  watcher/__init__.py,sha256=Vs2olXPZ_IAV1rukHYRl3ITzYFBNXX9mIUtFXkMWYwg,640
9
9
  watcher/_i18n.py,sha256=-25OcpwjDCiYdFIUJh86S7Sg0fs3wMIIjEreaGUyCyY,1295
10
10
  watcher/eventlet.py,sha256=z6WuDS9HJAq6vRZCg5ozYpWCs3lwhDuv9R70bj8tpQ0,1476
@@ -77,7 +77,7 @@ watcher/cmd/eventlet/api.py,sha256=n_MTSrrNGQH7EN4q9BFpeQFcfTU0OEAGdD97TdcLmxc,1
77
77
  watcher/cmd/eventlet/applier.py,sha256=5qprJMg2_ypyAawaoOiZiaeJ8cyCVCnud3F9apyXorg,1335
78
78
  watcher/cmd/eventlet/decisionengine.py,sha256=_uZFvf5KRusXcr91ZvQGgQuxgQd_Q_z-vj9xVTnCOLY,1455
79
79
  watcher/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
80
- watcher/common/cinder_helper.py,sha256=8MTEyGpXT2ybuMcryD1fk2NceWNxeIJAVevOuDfDpOo,11435
80
+ watcher/common/cinder_helper.py,sha256=fjFRddZKT2zefMLa0SttF6sNvTJAyO1V8Xir3IVNaJ0,13336
81
81
  watcher/common/clients.py,sha256=Gv6RUJ-HB-vNqriApEn6syn_3QntxmpNHuCtTeQ8998,13920
82
82
  watcher/common/config.py,sha256=2_9hklnzAICpZAIDQmxoE8d8qMFS03sjkoVdXPLchmU,1465
83
83
  watcher/common/context.py,sha256=7ZZv4FMSKZZQ00NiqZFAsmico21O05UaGlc46U50_6U,3188
@@ -92,7 +92,7 @@ watcher/common/placement_helper.py,sha256=YP02OHiZOCaJODPAE2QRojf4PKJDlctW-dFn9a
92
92
  watcher/common/policy.py,sha256=8WGBY79RJItBN4RphBEacYj7JKtSHJLoVw3WW7oM5oA,4893
93
93
  watcher/common/rpc.py,sha256=eM0yHwmMDKHq29oualMp3lbVhEol2ffGTZdIJ9CgO_4,4357
94
94
  watcher/common/scheduling.py,sha256=kIWt7-vNg7wrebKEP4Xkig5r_DKfY3EnB4E4KCo3-Bo,1986
95
- watcher/common/service.py,sha256=E7zX4m-nskz-ODbhsKoYhfKHB9-Sbj-FntikvUflFes,9740
95
+ watcher/common/service.py,sha256=nFyaWGiCrlJawrR73EKNe10jfLQbNjp47qSRiwz6gD0,9730
96
96
  watcher/common/service_manager.py,sha256=PQwa-58CTLD0bWS_5i9KigSTp9OGeP7m7D1oMg3f50A,1421
97
97
  watcher/common/utils.py,sha256=PCPgzMtVfZR9613P7guKv1DJljK_4e0TbW7LgMXmwcg,6038
98
98
  watcher/common/loader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -129,11 +129,11 @@ watcher/conf/decision_engine.py,sha256=dh-3zFzATi8H-CYnwkxCEcHb5XV3qrSpMhWXheGuC
129
129
  watcher/conf/exception.py,sha256=_VAlIawyQZGHbn8zFzXsNaOiCv5miZD9VGR3HJn2Y1Q,989
130
130
  watcher/conf/glance_client.py,sha256=7ftFKm_Y4kf9ZYy7HVZkr0_h3EfmTbNoa2z_iIcMeHs,1589
131
131
  watcher/conf/gnocchi_client.py,sha256=wb643c4R_G9oY1le3SPHQ679ErbSqtPRivC4VJWrnmA,1589
132
- watcher/conf/grafana_client.py,sha256=JMalRmtMpRwpMMr8EkkV_maUcbEw4PGzPSv8Z4DTDpc,6908
132
+ watcher/conf/grafana_client.py,sha256=FOG9dDck_PYptoX6cGsyTXdzewIoHO3kbbxLHrgjIXA,6936
133
133
  watcher/conf/grafana_translators.py,sha256=5mqq-ZUYpiOaOtm7GRbxiyiJQFw_GFb8JynGhCMAwyE,1651
134
134
  watcher/conf/ironic_client.py,sha256=6VXcRcPtkBtXrXS8xIMRYPzqCeQdXIKtFdTYq-YzC3E,1587
135
135
  watcher/conf/keystone_client.py,sha256=zELzoqwBuWLm2S27XyzDZdlmBZBkOyAfxileyjuNJaw,1350
136
- watcher/conf/maas_client.py,sha256=7KGb3yIKWDvCdYA5Kc22uszkFEOp9rcJQHpQFES2dfA,1277
136
+ watcher/conf/maas_client.py,sha256=99UaXMNg_1GPyfU2q4TjjHpRmI6zAStcrYzvo6lDyyg,1305
137
137
  watcher/conf/models.py,sha256=Fo1zQMcUjtEIroMZ4mx3kRLr2A_-3JldMZJQZ7SKIko,1390
138
138
  watcher/conf/monasca_client.py,sha256=8agQc5E5PJ2Na7MRTj9OCJbBd8coF1Ug1GvzG-GJMxw,1602
139
139
  watcher/conf/neutron_client.py,sha256=MrrdfkephVKwZjxdN7Jh1oIzR8bymsb8CB8ixs55e1Q,1604
@@ -209,7 +209,7 @@ watcher/decision_engine/messaging/audit_endpoint.py,sha256=D_GdRWpYHS8mKSvpbENd7
209
209
  watcher/decision_engine/messaging/data_model_endpoint.py,sha256=bh_ViPvF7I_Nha32eSqsRpDt8FPomM78PENZkGOyDYs,2180
210
210
  watcher/decision_engine/model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
211
211
  watcher/decision_engine/model/base.py,sha256=epNLXZ-bxuglMdyNe9oQawAfBlzip7foRzqu9FOPU-k,1131
212
- watcher/decision_engine/model/model_root.py,sha256=z7e2PkNdc3Sn4t_4TXO0ZrRAp5FsvpZR60nXUtOU8dU,24504
212
+ watcher/decision_engine/model/model_root.py,sha256=cE8B8mdwqTOHaQjqJIhtGQm--utSvC8-J75RwOt8PGQ,25079
213
213
  watcher/decision_engine/model/collector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
214
214
  watcher/decision_engine/model/collector/base.py,sha256=Nv2vbhUXU7jRw-btFKAyP_6ev0OB1R2WCQdpufRA124,9593
215
215
  watcher/decision_engine/model/collector/cinder.py,sha256=gZ_NpfoJ9BNVTELd73Tw9cLXtpti8ToxmbY3iWZNLHY,11684
@@ -218,7 +218,7 @@ watcher/decision_engine/model/collector/manager.py,sha256=3dWsClSXB1hXu8mUbTDuoa
218
218
  watcher/decision_engine/model/collector/nova.py,sha256=7Tj8OwQVgkIR2cBsbHPNxM6SFmVdD0juqerDg0eS0aY,21882
219
219
  watcher/decision_engine/model/element/__init__.py,sha256=R3XmeW4-ufWnbWruWGLR4SQJP9olmBLADRfyrvP-PaE,1320
220
220
  watcher/decision_engine/model/element/baremetal_resource.py,sha256=WC6CkGhruAXIiynWDsvKbV3VdvDihBMy3YNTa18Q5bg,924
221
- watcher/decision_engine/model/element/base.py,sha256=zQ1YrW-yUaVOCjS0N_x_Gu0-_kQ5cDdSUu0YfCOrt4E,2078
221
+ watcher/decision_engine/model/element/base.py,sha256=3J5bFef8-y3xwrrS87mVKGt5p7X3mBnUYJvDH6q4eZ8,2247
222
222
  watcher/decision_engine/model/element/compute_resource.py,sha256=yeFx-ZQxryLja0oKyJubCc2XQG4i9HL4fpspWhJaSsM,860
223
223
  watcher/decision_engine/model/element/instance.py,sha256=pZMxlfALbX34eN0tO4JmxBXEjrKvkq6NMTtj9-pgL7k,2325
224
224
  watcher/decision_engine/model/element/node.py,sha256=p8uUG97FJIMBqjUUcLWxEnDFUqJEM6briS8aW_Cp4_A,3777
@@ -277,7 +277,7 @@ watcher/decision_engine/strategy/strategies/uniform_airflow.py,sha256=MzCCmXirah
277
277
  watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py,sha256=-usWMjqFcbTSWbGKNmsSlZ-0wnMNwLv2w22Fu6nIqyw,27190
278
278
  watcher/decision_engine/strategy/strategies/workload_balance.py,sha256=QFOr-UwJkv0XrYPpm-oZmbEoznDyk2yTDnGHJvkzCe4,15893
279
279
  watcher/decision_engine/strategy/strategies/workload_stabilization.py,sha256=Lfn51YMJTxS6n3AwGe-Dc0oVsMCMLXypoTwfevlOl88,25445
280
- watcher/decision_engine/strategy/strategies/zone_migration.py,sha256=C4DYdSErOHuihuE1KARE4KyyID4HF1evFlxCX4xrL80,34934
280
+ watcher/decision_engine/strategy/strategies/zone_migration.py,sha256=rWrrOauKtXuH6YK-xg9BIISZN2u5b6dcKgzd75suZ98,35867
281
281
  watcher/hacking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
282
282
  watcher/hacking/checks.py,sha256=D-PG1fgVyU5TIKVmZcJ0CXbmLx-8_ivU5vCKQvtZkjk,10383
283
283
  watcher/locale/de/LC_MESSAGES/watcher.po,sha256=-EqexOllNxp5DIvacuQAKPVDB8NASduyQXOpoXRMbB4,26317
@@ -325,7 +325,7 @@ watcher/tests/api/v1/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
325
325
  watcher/tests/api/v1/test_actions.py,sha256=l27mglSLBQE_A-eQFv5rUzu4nwS3CaIQtLo37JMxgqI,33989
326
326
  watcher/tests/api/v1/test_actions_plans.py,sha256=L7xC9ZANo41qfnmBL3RNQf-jAb6e8NaIQekTPpp0sSk,33795
327
327
  watcher/tests/api/v1/test_audit_templates.py,sha256=Snj3vFLthLDciTlLUFu80Rj2u91mnVvHa5afY6LRRIo,38366
328
- watcher/tests/api/v1/test_audits.py,sha256=NpVDYiVTg4t75A06AsZztrdxPIFx8QAQb3f5eRcveKc,64432
328
+ watcher/tests/api/v1/test_audits.py,sha256=T4_8hu4-L-bQIXCseVF0bEJUHtTxoV06IN7gB69PZcA,65828
329
329
  watcher/tests/api/v1/test_data_model.py,sha256=jR5yf5jSxbwb89N5XxlZcqDdxrLCuKuCxudZ6xCZHdM,6635
330
330
  watcher/tests/api/v1/test_goals.py,sha256=vveUL0rQHv0LV9xa1T4EQCNpyAbdQMFF14_HHKsyHUs,7335
331
331
  watcher/tests/api/v1/test_microversions.py,sha256=0Oo_AZqg-Mvmc5GGVvIPkWvr2Czl1ku4tUPlEh8WclE,4998
@@ -366,7 +366,7 @@ watcher/tests/cmd/test_db_manage.py,sha256=itbu-PM6I5OAP_nvWUxGYDfl5tE2e58eVafwx
366
366
  watcher/tests/cmd/test_decision_engine.py,sha256=7hSHAPLQo2WZxQoY-pEfuVMjpnAJytyK7SUizin54Ig,2267
367
367
  watcher/tests/cmd/test_status.py,sha256=EBwifVusDZuTOpiVGKNqrmJv6hK8ZlZcngm15Jntyc8,1586
368
368
  watcher/tests/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
369
- watcher/tests/common/test_cinder_helper.py,sha256=nRTvqOURsME31QBHZxeG-75YdriW2HCr60mCR1cyl20,24754
369
+ watcher/tests/common/test_cinder_helper.py,sha256=iFf6qgvyF-VfOg3jnm2Xj3MoQVIBP6h3-vJZiYWIYXg,32071
370
370
  watcher/tests/common/test_clients.py,sha256=SD6qQsND91faF9Q1eTR8TIEFiAoiqKZef6FgBKIFqRw,17984
371
371
  watcher/tests/common/test_executor.py,sha256=W1F7ANlyTEX-v8wycQXE5SQ0yIkagvh3w2u7xk7x4p0,1404
372
372
  watcher/tests/common/test_ironic_helper.py,sha256=Ut-bpvPBQJWdu9xWPvzQ0sFEimuHU0_i3bC3T9aHvE0,2162
@@ -375,7 +375,7 @@ watcher/tests/common/test_nova_helper.py,sha256=Fjtp-RFr-M46U_6hnkS5Z2GhDNEBvdmH
375
375
  watcher/tests/common/test_oslo_service_helper.py,sha256=aG28fDYYtCDGrSPrRbcPBKeFtq4v8KBYMslXzLYLXlQ,1677
376
376
  watcher/tests/common/test_placement_helper.py,sha256=Y4P2YbWhC0itWGJbaDkoua9ot0XknXuM8disvceIXQQ,11761
377
377
  watcher/tests/common/test_scheduling.py,sha256=pOokZK33L9eaXUX42OZmBukkIwu91wAthSG4XwezXU4,2235
378
- watcher/tests/common/test_service.py,sha256=C7i4Pdawc_XKpqZuklJqQd3unztgiVpvkwwjUujGRK0,3535
378
+ watcher/tests/common/test_service.py,sha256=zQ4kKVr6BsP-4cvlGS3GL2GyWzt2Zu0UhLmz2W97R_g,4401
379
379
  watcher/tests/common/test_utils.py,sha256=Mq-51gFQv2EPdGaO44_py8ewdbObvnhwE4fpnzPTQPM,1697
380
380
  watcher/tests/common/loader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
381
381
  watcher/tests/common/loader/test_loader.py,sha256=mBnhe2apSmqpEL-8QZUlRDhDuWEEGo7iZQxpZIuGCWM,3612
@@ -443,7 +443,7 @@ watcher/tests/decision_engine/model/faker_cluster_state.py,sha256=p3SW99eEzDQWId
443
443
  watcher/tests/decision_engine/model/gnocchi_metrics.py,sha256=Fs5gGrAgxHA2eENzJbWCMDEUNMZPxy2qUwhLFNWTS-M,9357
444
444
  watcher/tests/decision_engine/model/monasca_metrics.py,sha256=s-voe8Cu7Su2_Tn_sp3dSxWTPI3zgFd9Wtrf9CZ7Wu4,4121
445
445
  watcher/tests/decision_engine/model/test_element.py,sha256=9PggbUFDshEEcbVYhU-49TKQgtufHIlTfqoJNI8Czb4,6000
446
- watcher/tests/decision_engine/model/test_model.py,sha256=UwqHkRyo9puoWjZaRto90HE3k_ycwveUYvg_a3jGNlY,20671
446
+ watcher/tests/decision_engine/model/test_model.py,sha256=BLWwwRXXrc4TcoUPRwpHe4YXDGgyW1rt8XR7oJEWLeo,21982
447
447
  watcher/tests/decision_engine/model/data/ironic_scenario_1.xml,sha256=BxJ0R1FlqJa4ryyMPY81rswjW9WOSQxpsbX5lhL_Ono,451
448
448
  watcher/tests/decision_engine/model/data/scenario_1.xml,sha256=7vAaXW--mVZPuhXMh5bIhj072IY1SQVnphnh6StZHJs,10426
449
449
  watcher/tests/decision_engine/model/data/scenario_10.xml,sha256=XkdELZDxrUIrAnEqCxHbav0g_8FKzP_zbGuHrWv_4r8,4984
@@ -550,7 +550,7 @@ watcher/tests/decision_engine/strategy/strategies/test_uniform_airflow.py,sha256
550
550
  watcher/tests/decision_engine/strategy/strategies/test_vm_workload_consolidation.py,sha256=AXY55dBg_8w37qpy0BT89ScUo81BUCZXwZO957jTgEE,19203
551
551
  watcher/tests/decision_engine/strategy/strategies/test_workload_balance.py,sha256=m2VljGe-9IJBBRyJwkseDiRxLB1z510tQL3orGyG470,8380
552
552
  watcher/tests/decision_engine/strategy/strategies/test_workload_stabilization.py,sha256=eGr_PX8dpM4oTlC-GFrimDN1kQbKpNFzA_C5c4mo250,14313
553
- watcher/tests/decision_engine/strategy/strategies/test_zone_migration.py,sha256=syUFRGHNi6kPfzgTgJ0AtAliajR16bCb4jGpa27bKS0,68197
553
+ watcher/tests/decision_engine/strategy/strategies/test_zone_migration.py,sha256=kH0fqMw0URbBJG9BSnZg2qvTMxXZEuJhQWyTNFOc70Y,72059
554
554
  watcher/tests/fixtures/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
555
555
  watcher/tests/fixtures/watcher.py,sha256=9BqKxgaBLWG0LEss-rnRQu3CuB4Z6hzry6hjI0UNxhk,4923
556
556
  watcher/tests/notifications/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -574,9 +574,9 @@ watcher/tests/objects/test_strategy.py,sha256=WoJYFQD0LU7vkUT2MMy1bx3S6eY458aTIC
574
574
  watcher/tests/objects/utils.py,sha256=WtBFlXWIBRvl1keAjz1GtrkUWppil2pFIA-RngcqA5c,8058
575
575
  watcher/wsgi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
576
576
  watcher/wsgi/api.py,sha256=F1s1Q_CMuRdgS_f7Gl3XzavjZqdxqO9IHLxSWl6NOO4,789
577
- python_watcher-15.1.0.dist-info/METADATA,sha256=4m3xIlcgr8hXxtg5K6sgpHe44fk93wlMpz4a7bnm7Vw,4110
578
- python_watcher-15.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
579
- python_watcher-15.1.0.dist-info/entry_points.txt,sha256=neFmDxaEe7U6uSPJEFcai3Aw4cyI_aFnHXDoWBK1fqI,4464
580
- python_watcher-15.1.0.dist-info/pbr.json,sha256=tq55Q-B7howl7MbzEzrOBt5yHnr2WaArntB3YJ6VSjI,47
581
- python_watcher-15.1.0.dist-info/top_level.txt,sha256=O2B7Sbzshq5s-tZxb2a7VETvMDiJItSYieAiJ6Jm5yY,8
582
- python_watcher-15.1.0.dist-info/RECORD,,
577
+ python_watcher-15.1.1.dist-info/METADATA,sha256=6uqqmtya0CNSmsp7JIq1pubEj-Hp97Yqy3Aluj_JOHA,4110
578
+ python_watcher-15.1.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
579
+ python_watcher-15.1.1.dist-info/entry_points.txt,sha256=neFmDxaEe7U6uSPJEFcai3Aw4cyI_aFnHXDoWBK1fqI,4464
580
+ python_watcher-15.1.1.dist-info/pbr.json,sha256=Cbz81KbjaXA-Il88VMNiuOYe2l9fdNhmiCGFdY4tpZU,47
581
+ python_watcher-15.1.1.dist-info/top_level.txt,sha256=O2B7Sbzshq5s-tZxb2a7VETvMDiJItSYieAiJ6Jm5yY,8
582
+ python_watcher-15.1.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.1)
2
+ Generator: setuptools (83.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1 @@
1
+ {"git_version": "8e27e90a", "is_release": true}
@@ -94,18 +94,6 @@ class CinderHelper(object):
94
94
  except cinder_exception.NotFound:
95
95
  return self.cinder.volumes.find(name=volume)
96
96
 
97
- def backendname_from_poolname(self, poolname):
98
- """Get backendname from poolname"""
99
- # pooolname formatted as host@backend#pool since ocata
100
- # as of ocata, may as only host
101
- backend = poolname.split('#')[0]
102
- backendname = ""
103
- try:
104
- backendname = backend.split('@')[1]
105
- except IndexError:
106
- pass
107
- return backendname
108
-
109
97
  def _has_snapshot(self, volume):
110
98
  """Judge volume has a snapshot"""
111
99
  volume = self.get_volume(volume)
@@ -132,6 +120,51 @@ class CinderHelper(object):
132
120
  else:
133
121
  return True
134
122
 
123
+ def _check_backend_matches_type(self, pool, volume_type):
124
+ """Check if a storage pool matches volume type requirements.
125
+
126
+ Verifies that all extra_specs properties defined in the volume
127
+ type are present in the pool's capabilities with matching values.
128
+
129
+ :param pool: Storage pool dictionary with capabilities
130
+ :param volume_type: Volume type object with extra_specs
131
+ :returns: True if pool matches all volume type requirements,
132
+ False otherwise
133
+ """
134
+ for field_name, field_value in volume_type.extra_specs.items():
135
+ pool_value = pool.get("capabilities", {}).get(field_name)
136
+ if pool_value is not None and field_value != pool_value:
137
+ # the property associated with the volume type is
138
+ # not defined in the pool, so the type can't be used in the
139
+ # pool
140
+ LOG.debug(
141
+ f"property {field_name} with value {field_value} "
142
+ f"does not match value {pool_value} from pool "
143
+ f"{pool['name']}"
144
+ )
145
+ return False
146
+ return True
147
+
148
+ def get_volume_types_for_pool(self, pool):
149
+ """Return a list of volume types that can be associated with a pool.
150
+
151
+ :param pool: Storage pool dictionary with capabilities
152
+ :returns: List of volume types than can be scheduled in the input pool
153
+ """
154
+ volume_type_list = self.get_volume_type_list()
155
+
156
+ pool_volume_types = []
157
+ for volume_type in volume_type_list:
158
+ if not volume_type.extra_specs:
159
+ # if there are no properties associated with the volume type
160
+ # it can be used in any pool
161
+ pool_volume_types.append(volume_type.name)
162
+ continue
163
+ if self._check_backend_matches_type(pool, volume_type):
164
+ pool_volume_types.append(volume_type.name)
165
+
166
+ return pool_volume_types
167
+
135
168
  def check_volume_deleted(self, volume, retry=120, retry_interval=10):
136
169
  """Check volume has been deleted"""
137
170
  volume = self.get_volume(volume)
@@ -224,11 +257,20 @@ class CinderHelper(object):
224
257
  def migrate(self, volume, dest_node):
225
258
  """Migrate volume to dest_node"""
226
259
  volume = self.get_volume(volume)
227
- dest_backend = self.backendname_from_poolname(dest_node)
228
- dest_type = self.get_volume_type_by_backendname(dest_backend)
229
- if volume.volume_type not in dest_type:
260
+ pool_dict = self.get_storage_pool_by_name(dest_node).to_dict()
261
+ dest_types = self.get_volume_types_for_pool(pool_dict)
262
+ if volume.volume_type not in dest_types:
230
263
  raise exception.Invalid(
231
- message=(_("Volume type must be same for migrating")))
264
+ message=(
265
+ _(
266
+ "Volume type '%(volume_type)s' is not compatible with "
267
+ "destination pool '%(pool_name)s'"
268
+ ) % {
269
+ 'volume_type': volume.volume_type,
270
+ 'pool_name': dest_node
271
+ }
272
+ )
273
+ )
232
274
 
233
275
  source_node = getattr(volume, 'os-vol-host-attr:host')
234
276
  LOG.debug("Volume %(volume)s found on host '%(host)s'.",
watcher/common/service.py CHANGED
@@ -241,7 +241,7 @@ class Service(service.ServiceBase):
241
241
  )
242
242
 
243
243
  def start(self):
244
- LOG.debug("Connecting to '%s'", CONF.transport_url)
244
+ LOG.debug("Connecting to messaging transport")
245
245
  if self.conductor_topic_handler:
246
246
  self.conductor_topic_handler.start()
247
247
  if self.notification_handler:
@@ -250,7 +250,7 @@ class Service(service.ServiceBase):
250
250
  self.heartbeat.start()
251
251
 
252
252
  def stop(self):
253
- LOG.debug("Disconnecting from '%s'", CONF.transport_url)
253
+ LOG.debug("Disconnecting from messaging transport")
254
254
  if self.conductor_topic_handler:
255
255
  self.conductor_topic_handler.stop()
256
256
  if self.notification_handler:
@@ -29,6 +29,7 @@ GRAFANA_CLIENT_OPTS = [
29
29
  # A similar structure to the database_map would solve this.
30
30
  cfg.StrOpt('token',
31
31
  default=None,
32
+ secret=True,
32
33
  help="Authentication token to gain access"),
33
34
  # TODO(Dantali0n) each individual metric could have its own base url.
34
35
  # A similar structure to the database_map would solve this.
@@ -23,6 +23,7 @@ MAAS_CLIENT_OPTS = [
23
23
  cfg.StrOpt('url',
24
24
  help='MaaS URL, example: http://1.2.3.4:5240/MAAS'),
25
25
  cfg.StrOpt('api_key',
26
+ secret=True,
26
27
  help='MaaS API authentication key.'),
27
28
  cfg.IntOpt('timeout',
28
29
  default=60,
@@ -51,16 +51,19 @@ class Element(base.WatcherObject, base.WatcherObjectDictCompat,
51
51
 
52
52
  def as_xml_element(self):
53
53
  sorted_fieldmap = []
54
+ element_name = self.__class__.__name__
54
55
  for field in self.fields:
55
56
  try:
56
57
  value = str(self[field])
57
58
  sorted_fieldmap.append((field, value))
59
+ except NotImplementedError:
60
+ LOG.debug("Attribute %s for object %s: %s is not provided",
61
+ field, element_name, self)
58
62
  except Exception as exc:
59
63
  LOG.exception(exc)
60
64
 
61
65
  attrib = collections.OrderedDict(sorted_fieldmap)
62
66
 
63
- element_name = self.__class__.__name__
64
67
  instance_el = etree.Element(element_name, attrib=attrib)
65
68
 
66
69
  return instance_el
@@ -269,7 +269,14 @@ class ModelRoot(nx.DiGraph, base.Model):
269
269
  in_dict = {}
270
270
  for field in cn.fields:
271
271
  new_name = "node_"+str(field)
272
- in_dict[new_name] = cn[field]
272
+ try:
273
+ in_dict[new_name] = cn[field]
274
+ # NotImplementedError means field not assigned
275
+ # which is really not an issue as it can be an
276
+ # optional field
277
+ except NotImplementedError:
278
+ LOG.debug("Attribute %s for Compute: %s is not provided",
279
+ field, cn.hostname)
273
280
  node_instances = self.get_node_instances(cn)
274
281
  if not node_instances:
275
282
  deep_in_dict = in_dict.copy()
@@ -278,7 +285,11 @@ class ModelRoot(nx.DiGraph, base.Model):
278
285
  for instance in sorted(node_instances, key=lambda x: x.uuid):
279
286
  for field in instance.fields:
280
287
  new_name = "server_"+str(field)
281
- in_dict[new_name] = instance[field]
288
+ try:
289
+ in_dict[new_name] = instance[field]
290
+ except NotImplementedError:
291
+ LOG.debug("Attribute %s for Instance: %s is not "
292
+ "provided", field, instance.uuid)
282
293
  if in_dict != {}:
283
294
  deep_in_dict = in_dict.copy()
284
295
  ret_list.append(deep_in_dict)
@@ -381,16 +381,36 @@ class ZoneMigration(base.ZoneMigrationBaseStrategy):
381
381
  if node.get("src_node") == src_node:
382
382
  return node.get("dst_node")
383
383
 
384
- def get_dst_pool_and_type(self, src_pool):
384
+ def get_dst_pool_and_type(self, src_pool, src_type):
385
385
  """Get destination pool and type from self.migration_storage_pools
386
386
 
387
387
  :param src_pool: storage pool name
388
- :returns: set of storage pool name and volume type name
388
+ :param src_type: storage volume type
389
+ :returns: set of storage dst pool name and dst volume type name, any
390
+ of those values can be None if no match for it is found in the
391
+ input_parameters
389
392
  """
393
+ src_pool_type_inputs = []
394
+ src_pool_inputs = []
390
395
  for pool in self.migrate_storage_pools:
396
+ if pool.get("src_type") is None:
397
+ src_pool_inputs.append(pool)
398
+ else:
399
+ src_pool_type_inputs.append(pool)
400
+
401
+ pool_to_check = (src_pool, src_type)
402
+ # First pass: prefer exact match (both src_pool and src_type)
403
+ for pool in src_pool_type_inputs:
404
+ user_pool = (pool.get("src_pool"), pool.get("src_type"))
405
+ if pool_to_check == user_pool:
406
+ return (pool.get("dst_pool"), pool.get("dst_type"))
407
+
408
+ # Second pass: fall back to matching just src_pool
409
+ for pool in src_pool_inputs:
391
410
  if pool.get("src_pool") == src_pool:
392
- return (pool.get("dst_pool", None),
393
- pool.get("dst_type"))
411
+ return (pool.get("dst_pool"), pool.get("dst_type"))
412
+
413
+ return (None, None)
394
414
 
395
415
  def volumes_migration(self, volumes, action_counter, instance_targets=[]):
396
416
  instance_target_ids = {instance.id for instance in instance_targets}
@@ -406,17 +426,20 @@ class ZoneMigration(base.ZoneMigrationBaseStrategy):
406
426
  continue
407
427
 
408
428
  src_type = volume.volume_type
409
- dst_pool, dst_type = self.get_dst_pool_and_type(pool)
429
+ dst_pool, dst_type = self.get_dst_pool_and_type(pool, src_type)
410
430
  LOG.debug(src_type)
411
431
  LOG.debug("%s %s", dst_pool, dst_type)
412
432
 
413
- if src_type == dst_type:
433
+ if dst_type is None or src_type == dst_type:
414
434
  if dst_pool is None:
415
- LOG.warning("volume %s will not be migrated because "
416
- "it already has type %s and 'dst_pool'"
417
- " was not specified for src_pool %s",
418
- volume.name, dst_type,
419
- pool)
435
+ type_msg = (
436
+ "" if dst_type is None else
437
+ f"it already has type {dst_type} and ")
438
+ LOG.info(
439
+ "volume %s will not be migrated because %s'dst_pool' "
440
+ "was not specified for src_pool %s",
441
+ volume.name, type_msg, pool
442
+ )
420
443
  continue
421
444
  self._volume_migrate(volume, dst_pool)
422
445
  else:
@@ -1394,6 +1394,39 @@ class TestAuditZoneMigration(TestPostBase):
1394
1394
  self.assertIn(expected_error_msg, response.json['error_message'])
1395
1395
  mock_trigger_audit.assert_not_called()
1396
1396
 
1397
+ @mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit')
1398
+ def test_create_audit_zone_migration_no_dst_pool_dst_type(
1399
+ self, mock_trigger_audit
1400
+ ):
1401
+ """Verify zone migration rejects no destination parameters.
1402
+
1403
+ Tests that an audit creation fails with BAD_REQUEST when neither
1404
+ dst_type nor dst_pool are passed in the input parameters. This
1405
+ validates the schema correctly enforces that the at least one
1406
+ destination parameters is present.
1407
+ """
1408
+ mock_trigger_audit.return_value = mock.ANY
1409
+ zm_params = {
1410
+ 'storage_pools': [
1411
+ {"src_type": "src_type_name",
1412
+ "src_pool": "src_pool_name"}
1413
+ ]
1414
+ }
1415
+
1416
+ audit_input_dict = self._prepare_audit_params(zm_params)
1417
+
1418
+ response = self.post_json('/audits', audit_input_dict,
1419
+ expect_errors=True)
1420
+ self.assertEqual("application/json", response.content_type)
1421
+ self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_int)
1422
+ expected_error_msgs = (
1423
+ "is not valid under any of the given schemas",
1424
+ "Failed validating 'oneOf' in schema"
1425
+ )
1426
+ for expected_error_msg in expected_error_msgs:
1427
+ self.assertIn(expected_error_msg, response.json['error_message'])
1428
+ mock_trigger_audit.assert_not_called()
1429
+
1397
1430
  @mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit')
1398
1431
  def test_create_audit_zone_migration_with_multiple_valid_inputs(
1399
1432
  self, mock_trigger_audit
@@ -21,6 +21,9 @@ from http import HTTPStatus
21
21
  import time
22
22
 
23
23
  from cinderclient import exceptions as cinder_exception
24
+ from cinderclient.v3.pools import Pool as CinderPool
25
+ from cinderclient.v3.volume_types import VolumeType as CinderVolType
26
+ from cinderclient.v3.volumes import Volume as CinderVolume
24
27
 
25
28
  from watcher.common import cinder_helper
26
29
  from watcher.common import clients
@@ -67,8 +70,20 @@ class TestCinderHelper(base.TestCase):
67
70
 
68
71
  @staticmethod
69
72
  def fake_pool(**kwargs):
70
- pool = mock.MagicMock()
73
+ pool = mock.MagicMock(spec=CinderPool)
71
74
  pool.name = kwargs.get('name', 'host@backend#pool')
75
+ host_backend = pool.name.split('#')[0]
76
+ default_capabilities = {
77
+ 'volume_backend_name': host_backend.split('@')[1]
78
+ }
79
+ pool.capabilities = kwargs.get('capabilities', default_capabilities)
80
+
81
+ def _to_dict():
82
+ return {
83
+ 'name': pool.name,
84
+ 'capabilities': pool.capabilities
85
+ }
86
+ pool.to_dict = _to_dict
72
87
 
73
88
  return pool
74
89
 
@@ -98,7 +113,7 @@ class TestCinderHelper(base.TestCase):
98
113
 
99
114
  @staticmethod
100
115
  def fake_volume_type(**kwargs):
101
- volume_type = mock.MagicMock()
116
+ volume_type = mock.MagicMock(spec=CinderVolType)
102
117
  volume_type.name = kwargs.get('name', 'fake_type')
103
118
  extra_specs = {'volume_backend_name': 'backend'}
104
119
  volume_type.extra_specs = kwargs.get('extra_specs', extra_specs)
@@ -133,7 +148,7 @@ class TestCinderHelper(base.TestCase):
133
148
 
134
149
  @staticmethod
135
150
  def fake_volume(**kwargs):
136
- volume = mock.MagicMock()
151
+ volume = mock.MagicMock(spec=CinderVolume)
137
152
  volume.id = kwargs.get('id', '45a37aeb-95ab-4ddb-a305-7d9f62c2f5ba')
138
153
  volume.name = kwargs.get('name', 'fakename')
139
154
  volume.size = kwargs.get('size', '1')
@@ -142,51 +157,72 @@ class TestCinderHelper(base.TestCase):
142
157
  volume.availability_zone = kwargs.get('availability_zone', 'nova')
143
158
  volume.volume_type = kwargs.get('volume_type', 'fake_type')
144
159
  volume.migration_status = kwargs.get('migration_status')
145
- volume.os_vol_host_attr_host = kwargs.get('os_vol_host_attr_host')
160
+ setattr(
161
+ volume, 'os-vol-host-attr:host',
162
+ kwargs.get('os_vol_host_attr_host')
163
+ )
146
164
  return volume
147
165
 
148
166
  @mock.patch.object(time, 'sleep', mock.Mock())
149
- def test_migrate_success(self, mock_cinder):
167
+ @mock.patch.object(cinder_helper.CinderHelper, 'get_storage_pool_by_name')
168
+ def test_migrate_success(self, mock_get_pool, mock_cinder):
150
169
 
151
170
  cinder_util = cinder_helper.CinderHelper()
152
171
 
153
- volume = self.fake_volume()
154
- setattr(volume, 'os-vol-host-attr:host', 'source_node')
155
- setattr(volume, 'migration_status', 'success')
172
+ volume = self.fake_volume(
173
+ os_vol_host_attr_host='source_node',
174
+ migration_status='success'
175
+ )
156
176
  cinder_util.cinder.volumes.get.return_value = volume
157
177
 
158
178
  volume_type = self.fake_volume_type()
159
179
  cinder_util.cinder.volume_types.list.return_value = [volume_type]
180
+ mock_pool = self.fake_pool()
181
+ mock_get_pool.return_value = mock_pool
160
182
 
161
183
  result = cinder_util.migrate(volume, 'host@backend#pool')
184
+ mock_get_pool.assert_called_once_with('host@backend#pool')
162
185
  self.assertTrue(result)
163
186
 
164
187
  @mock.patch.object(time, 'sleep', mock.Mock())
165
- def test_migrate_fail(self, mock_cinder):
188
+ @mock.patch.object(cinder_helper.CinderHelper, 'get_storage_pool_by_name')
189
+ def test_migrate_fail(self, mock_get_pool, mock_cinder):
166
190
 
167
191
  cinder_util = cinder_helper.CinderHelper()
168
192
 
169
193
  volume = self.fake_volume()
170
194
  cinder_util.cinder.volumes.get.return_value = volume
195
+ mock_pool = self.fake_pool()
196
+ mock_get_pool.return_value = mock_pool
171
197
 
172
- volume_type = self.fake_volume_type()
173
- volume_type.name = 'notbackend'
198
+ volume_type = self.fake_volume_type(
199
+ name='notbackend',
200
+ extra_specs={'volume_backend_name': 'diff_backend'}
201
+ )
174
202
  cinder_util.cinder.volume_types.list.return_value = [volume_type]
175
203
 
176
204
  self.assertRaisesRegex(
177
205
  exception.Invalid,
178
- "Volume type must be same for migrating",
206
+ "Volume type 'fake_type' is not compatible with destination "
207
+ "pool 'host@backend#pool'",
179
208
  cinder_util.migrate, volume, 'host@backend#pool')
180
209
 
181
- volume = self.fake_volume()
182
- setattr(volume, 'os-vol-host-attr:host', 'source_node')
183
- setattr(volume, 'migration_status', 'error')
210
+ volume = self.fake_volume(
211
+ migration_status='error',
212
+ os_vol_host_attr_host='source_node'
213
+ )
184
214
  cinder_util.cinder.volumes.get.return_value = volume
185
215
 
186
- volume_type = self.fake_volume_type()
216
+ # check that a volume type without any volume_backend_name passes the
217
+ # volume type check and proceeds to the migration
218
+ volume_type = self.fake_volume_type(extra_specs={})
187
219
  cinder_util.cinder.volume_types.list.return_value = [volume_type]
188
220
 
189
221
  result = cinder_util.migrate(volume, 'host@backend#pool')
222
+ mock_get_pool.assert_called_with('host@backend#pool')
223
+ cinder_util.cinder.volumes.migrate_volume.assert_called_with(
224
+ volume, 'host@backend#pool', False, True
225
+ )
190
226
  self.assertFalse(result)
191
227
 
192
228
  @mock.patch.object(time, 'sleep', mock.Mock())
@@ -607,3 +643,157 @@ class TestCinderHelper(base.TestCase):
607
643
  "Volume migration error on volume %(volume)s.",
608
644
  {'volume': volume.id})
609
645
  self.assertFalse(result)
646
+
647
+ def test_get_volume_types_for_pool_matching_backend(
648
+ self, mock_cinder):
649
+ """Test matching backend and no extra_specs are selected."""
650
+ cinder_util = cinder_helper.CinderHelper()
651
+ pool = self.fake_pool(
652
+ name='host@backend#pool',
653
+ capabilities={'volume_backend_name': 'backend'}
654
+ )
655
+ volume_type1 = self.fake_volume_type(
656
+ name='type_matching',
657
+ extra_specs={'volume_backend_name': 'backend'}
658
+ )
659
+ volume_type2 = self.fake_volume_type(
660
+ name='type_no_specs',
661
+ extra_specs={}
662
+ )
663
+ cinder_util.cinder.volume_types.list.return_value = [
664
+ volume_type1, volume_type2
665
+ ]
666
+ result = cinder_util.get_volume_types_for_pool(
667
+ pool.to_dict())
668
+ self.assertEqual(
669
+ sorted(result),
670
+ ['type_matching', 'type_no_specs'])
671
+
672
+ def test_get_volume_types_for_pool_different_backend(
673
+ self, mock_cinder):
674
+ """Test different backend is excluded, no specs is included."""
675
+ cinder_util = cinder_helper.CinderHelper()
676
+ pool = self.fake_pool(
677
+ name='host@backend#pool',
678
+ capabilities={'volume_backend_name': 'backend'}
679
+ )
680
+ volume_type1 = self.fake_volume_type(
681
+ name='type_no_specs',
682
+ extra_specs={}
683
+ )
684
+ volume_type2 = self.fake_volume_type(
685
+ name='type_different_backend',
686
+ extra_specs={'volume_backend_name': 'different_backend'}
687
+ )
688
+ cinder_util.cinder.volume_types.list.return_value = [
689
+ volume_type1, volume_type2
690
+ ]
691
+ result = cinder_util.get_volume_types_for_pool(
692
+ pool.to_dict())
693
+ self.assertEqual(result, ['type_no_specs'])
694
+
695
+ def test_get_volume_types_for_pool_matching_capability(
696
+ self, mock_cinder):
697
+ """Test volume type with matching capability is selected."""
698
+ cinder_util = cinder_helper.CinderHelper()
699
+ pool = self.fake_pool(
700
+ name='host@backend#pool',
701
+ capabilities={'disk_speed': 'fast'}
702
+ )
703
+ volume_type = self.fake_volume_type(
704
+ name='type_fast_disk',
705
+ extra_specs={'disk_speed': 'fast'}
706
+ )
707
+ cinder_util.cinder.volume_types.list.return_value = [
708
+ volume_type]
709
+ result = cinder_util.get_volume_types_for_pool(
710
+ pool.to_dict())
711
+ self.assertEqual(result, ['type_fast_disk'])
712
+
713
+ def test_get_volume_types_for_pool_mismatched_capability(
714
+ self, mock_cinder):
715
+ """Test volume type with different capability value excluded."""
716
+ cinder_util = cinder_helper.CinderHelper()
717
+ pool = self.fake_pool(
718
+ name='host@backend#pool',
719
+ capabilities={'disk_speed': 'fast'}
720
+ )
721
+ volume_type = self.fake_volume_type(
722
+ name='type_slow_disk',
723
+ extra_specs={'disk_speed': 'slow'}
724
+ )
725
+ cinder_util.cinder.volume_types.list.return_value = [
726
+ volume_type]
727
+ result = cinder_util.get_volume_types_for_pool(
728
+ pool.to_dict())
729
+ self.assertEqual([], result)
730
+
731
+ def test_get_volume_types_for_pool_no_capabilities(
732
+ self, mock_cinder):
733
+ """Test pool with no capabilities selects no-specs types."""
734
+ cinder_util = cinder_helper.CinderHelper()
735
+ pool = self.fake_pool(
736
+ name='host@backend#pool',
737
+ capabilities={}
738
+ )
739
+ volume_type1 = self.fake_volume_type(
740
+ name='type_no_specs',
741
+ extra_specs={}
742
+ )
743
+ volume_type2 = self.fake_volume_type(
744
+ name='type_with_specs',
745
+ extra_specs={'volume_backend_name': 'backend'}
746
+ )
747
+ cinder_util.cinder.volume_types.list.return_value = [
748
+ volume_type1, volume_type2
749
+ ]
750
+ result = cinder_util.get_volume_types_for_pool(
751
+ pool.to_dict())
752
+ self.assertEqual(result, ['type_no_specs', 'type_with_specs'])
753
+
754
+ def test_get_volume_types_for_pool_requirements_not_in_caps(
755
+ self, mock_cinder):
756
+ """Test volume type with requirements not in pool capabilities."""
757
+ cinder_util = cinder_helper.CinderHelper()
758
+ pool = self.fake_pool(
759
+ name='host@backend#pool',
760
+ capabilities={'volume_backend_name': 'backend'}
761
+ )
762
+ volume_type = self.fake_volume_type(
763
+ name='type_extra_reqs',
764
+ extra_specs={
765
+ 'volume_backend_name': 'backend',
766
+ 'disk_speed': 'fast',
767
+ }
768
+ )
769
+ cinder_util.cinder.volume_types.list.return_value = [
770
+ volume_type]
771
+ result = cinder_util.get_volume_types_for_pool(
772
+ pool.to_dict())
773
+ self.assertEqual(result, ['type_extra_reqs'])
774
+
775
+ def test_get_volume_types_for_pool_multiple_specs_partial_match(
776
+ self, mock_cinder):
777
+ """Test volume type with multiple extra_specs partial match."""
778
+ cinder_util = cinder_helper.CinderHelper()
779
+ pool = self.fake_pool(
780
+ name='host@backend#pool',
781
+ capabilities={
782
+ 'volume_backend_name': 'backend',
783
+ 'disk_speed': 'fast',
784
+ 'compression': 'enabled',
785
+ }
786
+ )
787
+ volume_type = self.fake_volume_type(
788
+ name='type_partial',
789
+ extra_specs={
790
+ 'volume_backend_name': 'backend',
791
+ 'disk_speed': 'slow',
792
+ 'compression': 'enabled',
793
+ }
794
+ )
795
+ cinder_util.cinder.volume_types.list.return_value = [
796
+ volume_type]
797
+ result = cinder_util.get_volume_types_for_pool(
798
+ pool.to_dict())
799
+ self.assertEqual([], result)
@@ -104,3 +104,25 @@ class TestService(base.TestCase):
104
104
  self.assertIsInstance(
105
105
  dummy_service.conductor_topic_handler,
106
106
  om.rpc.server.RPCServer)
107
+
108
+ @mock.patch.object(service.LOG, 'debug', autospec=True)
109
+ def test_transport_url_not_logged(self, mock_debug):
110
+ transport_url = 'rabbit://api:secret-pass@example.com:5672/'
111
+ CONF.set_override('transport_url', transport_url)
112
+
113
+ dummy_service = service.Service(DummyManager)
114
+ dummy_service.conductor_topic_handler = None
115
+ dummy_service.notification_handler = None
116
+ dummy_service.heartbeat = None
117
+
118
+ dummy_service.start()
119
+ dummy_service.stop()
120
+
121
+ logged = ' '.join(
122
+ str(arg) for call in mock_debug.call_args_list for arg in call.args
123
+ )
124
+
125
+ self.assertNotIn(transport_url, logged)
126
+ self.assertNotIn('secret-pass', logged)
127
+ mock_debug.assert_any_call("Connecting to messaging transport")
128
+ mock_debug.assert_any_call("Disconnecting from messaging transport")
@@ -66,18 +66,21 @@ class TestModel(base.TestCase):
66
66
 
67
67
  @mock.patch.object(model_root.ModelRoot, 'get_all_compute_nodes')
68
68
  @mock.patch.object(model_root.ModelRoot, 'get_node_instances')
69
- def test_get_model_to_list(self, mock_instances, mock_nodes):
70
- fake_compute_node = mock.MagicMock(
71
- uuid='fake_node_uuid',
72
- fields=['uuid'])
73
- fake_instance = mock.MagicMock(
74
- uuid='fake_instance_uuid',
75
- fields=['uuid'])
69
+ @mock.patch('watcher.decision_engine.model.model_root.LOG')
70
+ def test_get_model_to_list(self, mock_log, mock_instances, mock_nodes):
71
+ fake_compute_node = element.ComputeNode(
72
+ uuid='fake_node_uuid', hostname="hostname0")
73
+ fake_instance = element.Instance(
74
+ uuid='fake_instance_uuid')
76
75
 
77
76
  mock_nodes.return_value = {'fake_node_uuid': fake_compute_node}
78
77
  mock_instances.return_value = [fake_instance]
79
78
 
80
- expected_keys = ['server_uuid', 'node_uuid']
79
+ expected_keys = [
80
+ 'server_uuid', 'node_uuid', 'node_state', 'node_hostname',
81
+ 'node_status', 'server_flavor_extra_specs', 'server_locked',
82
+ 'server_pinned_az', 'server_state', 'server_watcher_exclude'
83
+ ]
81
84
 
82
85
  result = model_root.ModelRoot().to_list()
83
86
  self.assertEqual(1, len(result))
@@ -85,10 +88,20 @@ class TestModel(base.TestCase):
85
88
  result_keys = result[0].keys()
86
89
  self.assertEqual(sorted(expected_keys), sorted(result_keys))
87
90
 
91
+ mock_log.debug.assert_any_call(
92
+ 'Attribute %s for Instance: %s is not provided', 'name',
93
+ 'fake_instance_uuid'
94
+ )
95
+ mock_log.debug.assert_any_call(
96
+ 'Attribute %s for Compute: %s is not provided', 'memory',
97
+ 'hostname0'
98
+ )
99
+
88
100
  # test compute node has no instance
89
101
  mock_instances.return_value = []
90
102
 
91
- expected_keys = ['node_uuid']
103
+ expected_keys = ['node_hostname', 'node_status',
104
+ 'node_state', 'node_uuid']
92
105
 
93
106
  result = model_root.ModelRoot().to_list()
94
107
  self.assertEqual(1, len(result))
@@ -96,6 +109,23 @@ class TestModel(base.TestCase):
96
109
  result_keys = result[0].keys()
97
110
  self.assertEqual(expected_keys, list(result_keys))
98
111
 
112
+ def test_model_to_list_all_fields(self):
113
+ struct_str = self.load_data('scenario_1.xml')
114
+ model = model_root.ModelRoot.from_xml(struct_str)
115
+ model_list = model.to_list()
116
+ self.assertIsInstance(model_list, list)
117
+ self.assertEqual(len(model_list), 8)
118
+ self.assertEqual(model_list[0]['server_uuid'], 'INSTANCE_0')
119
+
120
+ def test_model_to_xml_all_fields(self):
121
+ struct_str = self.load_data('scenario_2_with_metrics.xml')
122
+
123
+ model = model_root.ModelRoot.from_xml(struct_str)
124
+ model_xml = model.to_xml()
125
+ self.assertIsInstance(model_xml, str)
126
+ self.assertIn('Instance', model_xml)
127
+ self.assertIn('Compute', model_xml)
128
+
99
129
  def test_get_node_by_instance_uuid(self):
100
130
  model = model_root.ModelRoot()
101
131
  uuid_ = "{0}".format(uuidutils.generate_uuid())
@@ -549,6 +549,30 @@ class TestZoneMigration(TestBaseStrategy):
549
549
  global_efficacy_value = solution.global_efficacy[2].get('value', 0)
550
550
  self.assertEqual(100, global_efficacy_value)
551
551
 
552
+ def test_execute_migrate_volume_no_dst_type(self):
553
+ volume_on_src1 = self.fake_volume(host="src1@back1#pool1",
554
+ id=volume_uuid_mapping["volume_1"],
555
+ name="volume_1")
556
+ self.m_c_helper.get_volume_list.return_value = [
557
+ volume_on_src1,
558
+ ]
559
+
560
+ self.m_n_helper.get_instance_list.return_value = []
561
+
562
+ self.input_parameters["storage_pools"] = [
563
+ {"src_pool": "src1@back1#pool1",
564
+ "src_type": "type1", "dst_pool": "dst1@back1#pool1"},
565
+ ]
566
+
567
+ solution = self.strategy.execute()
568
+
569
+ migration_types = collections.Counter(
570
+ [action.get('input_parameters')['migration_type']
571
+ for action in solution.actions])
572
+ self.assertEqual(1, migration_types.get("migrate", 0))
573
+ global_efficacy_value = solution.global_efficacy[2].get('value', 0)
574
+ self.assertEqual(100, global_efficacy_value)
575
+
552
576
  def test_execute_migrate_volume_no_dst_pool(self):
553
577
  volume_on_src1 = self.fake_volume(host="src1@back1#pool1",
554
578
  id=volume_uuid_mapping["volume_1"],
@@ -1549,3 +1573,88 @@ class TestZoneMigration(TestBaseStrategy):
1549
1573
  targets = self.strategy.filtered_targets()
1550
1574
  self.assertEqual(targets.get("volume"),
1551
1575
  [volume_on_src2, volume_on_src1])
1576
+
1577
+ def test_get_dst_pool_and_type_matching_src_pool_type(self):
1578
+ self.input_parameters["storage_pools"] = [
1579
+ {
1580
+ "src_pool": "src1@back1#pool1",
1581
+ "src_type": "type1",
1582
+ "dst_pool": "dst1@back1#pool1",
1583
+ },
1584
+ {
1585
+ "src_pool": "src1@back1#pool1",
1586
+ "src_type": "type2", "dst_type": "type3"
1587
+ }
1588
+ ]
1589
+ pool = "src1@back1#pool1"
1590
+ src_type = "type1"
1591
+ dst_pool, dst_type = self.strategy.get_dst_pool_and_type(
1592
+ pool, src_type
1593
+ )
1594
+ self.assertEqual(dst_pool, "dst1@back1#pool1")
1595
+ self.assertIsNone(dst_type)
1596
+
1597
+ def test_get_dst_pool_and_type_matching_src_pool(self):
1598
+ self.input_parameters["storage_pools"] = [
1599
+ {
1600
+ "src_pool": "src1@back1#pool1",
1601
+ "src_type": "type1",
1602
+ "dst_pool": "dst1@back1#pool1",
1603
+ },
1604
+ {
1605
+ "src_pool": "src1@back1#pool1",
1606
+ "dst_pool": "dst2@back2#pool2"
1607
+ }
1608
+ ]
1609
+ pool = "src1@back1#pool1"
1610
+ src_type = "type1"
1611
+ dst_pool, dst_type = self.strategy.get_dst_pool_and_type(
1612
+ pool, src_type
1613
+ )
1614
+ self.assertEqual(dst_pool, "dst1@back1#pool1")
1615
+ self.assertIsNone(dst_type)
1616
+
1617
+ # check that after not matching type it falls backs to matching
1618
+ # src_pool
1619
+ pool = "src1@back1#pool1"
1620
+ src_type = "typet"
1621
+ dst_pool, dst_type = self.strategy.get_dst_pool_and_type(
1622
+ pool, src_type
1623
+ )
1624
+ self.assertEqual(dst_pool, "dst2@back2#pool2")
1625
+ self.assertIsNone(dst_type)
1626
+
1627
+ def test_get_dst_pool_and_type_not_matching_pool(self):
1628
+ pool = "src3@back1#pool1"
1629
+ src_type = "type5"
1630
+ dst_pool, dst_type = self.strategy.get_dst_pool_and_type(
1631
+ pool, src_type
1632
+ )
1633
+ self.assertIsNone(dst_pool)
1634
+ self.assertIsNone(dst_type)
1635
+
1636
+ def test_get_dst_pool_and_type_same_src_pool_different_src_type(self):
1637
+ self.input_parameters["storage_pools"] = [
1638
+ {
1639
+ "src_pool": "src1@back1#pool1",
1640
+ "src_type": "type1",
1641
+ "dst_pool": "dst1@back1#pool1",
1642
+ },
1643
+ {
1644
+ "src_pool": "src1@back1#pool1",
1645
+ "src_type": "type2", "dst_type": "type3"
1646
+ }
1647
+ ]
1648
+ pool = "src1@back1#pool1"
1649
+ src_type = "type2"
1650
+ dst_pool, dst_type = self.strategy.get_dst_pool_and_type(
1651
+ pool, src_type
1652
+ )
1653
+ self.assertIsNone(dst_pool)
1654
+ self.assertEqual(dst_type, "type3")
1655
+ src_type = "type1"
1656
+ dst_pool, dst_type = self.strategy.get_dst_pool_and_type(
1657
+ pool, src_type
1658
+ )
1659
+ self.assertEqual(dst_pool, "dst1@back1#pool1")
1660
+ self.assertIsNone(dst_type)
@@ -1 +0,0 @@
1
- {"git_version": "398f603b", "is_release": true}