python-watcher 16.0.0__py3-none-any.whl → 16.0.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 (27) hide show
  1. {python_watcher-16.0.0.dist-info → python_watcher-16.0.1.dist-info}/METADATA +1 -1
  2. {python_watcher-16.0.0.dist-info → python_watcher-16.0.1.dist-info}/RECORD +26 -26
  3. {python_watcher-16.0.0.dist-info → python_watcher-16.0.1.dist-info}/WHEEL +1 -1
  4. python_watcher-16.0.1.dist-info/pbr.json +1 -0
  5. watcher/common/service.py +2 -2
  6. watcher/conf/grafana_client.py +1 -0
  7. watcher/conf/maas_client.py +1 -0
  8. watcher/decision_engine/model/collector/base.py +10 -6
  9. watcher/decision_engine/model/notification/base.py +22 -0
  10. watcher/decision_engine/model/notification/cinder.py +4 -4
  11. watcher/decision_engine/model/notification/nova.py +1 -1
  12. watcher/decision_engine/strategy/strategies/workload_balance.py +81 -30
  13. watcher/tests/unit/common/test_service.py +22 -0
  14. watcher/tests/unit/decision_engine/cluster/test_cluster_data_model_collector.py +74 -0
  15. watcher/tests/unit/decision_engine/model/notification/test_notifications.py +1 -1
  16. watcher/tests/unit/decision_engine/model/notification/test_nova_notifications.py +17 -2
  17. watcher/tests/unit/decision_engine/strategy/strategies/test_workload_balance.py +22 -8
  18. python_watcher-16.0.0.dist-info/pbr.json +0 -1
  19. {python_watcher-16.0.0.data → python_watcher-16.0.1.data}/data/etc/apache2/watcher +0 -0
  20. {python_watcher-16.0.0.data → python_watcher-16.0.1.data}/data/etc/watcher/README-watcher.conf.txt +0 -0
  21. {python_watcher-16.0.0.data → python_watcher-16.0.1.data}/data/etc/watcher/oslo-config-generator/watcher.conf +0 -0
  22. {python_watcher-16.0.0.data → python_watcher-16.0.1.data}/data/etc/watcher/oslo-policy-generator/watcher-policy-generator.conf +0 -0
  23. {python_watcher-16.0.0.data → python_watcher-16.0.1.data}/scripts/watcher-api-wsgi +0 -0
  24. {python_watcher-16.0.0.dist-info → python_watcher-16.0.1.dist-info}/entry_points.txt +0 -0
  25. {python_watcher-16.0.0.dist-info → python_watcher-16.0.1.dist-info}/licenses/AUTHORS +0 -0
  26. {python_watcher-16.0.0.dist-info → python_watcher-16.0.1.dist-info}/licenses/LICENSE +0 -0
  27. {python_watcher-16.0.0.dist-info → python_watcher-16.0.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: 16.0.0
3
+ Version: 16.0.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-16.0.0.data/data/etc/apache2/watcher,sha256=urybYUTb0kAVIvbm4WK5oDfXR82QSK2lyMl4Z9dCz4Q,1190
2
- python_watcher-16.0.0.data/data/etc/watcher/README-watcher.conf.txt,sha256=EIa_sjXDyl1XSg0ovsFwUjqw0euS0N2PYn4HtyBGJME,131
3
- python_watcher-16.0.0.data/data/etc/watcher/oslo-config-generator/watcher.conf,sha256=ZN93-gX9IPi8a2_vO_Wl_GRjafnO2Xm7oEk2ou9Fv04,486
4
- python_watcher-16.0.0.data/data/etc/watcher/oslo-policy-generator/watcher-policy-generator.conf,sha256=ZfIuVdtrj7IZm2BrRQvAW3JOL_kqCiILf-vBmwX3ufw,76
5
- python_watcher-16.0.0.data/scripts/watcher-api-wsgi,sha256=YcIpUO3iRVW6Fu3Md_ooAeHkmJJasG031naMdf8cv2A,1829
6
- python_watcher-16.0.0.dist-info/licenses/AUTHORS,sha256=A38NQqsvLAj9OTI09_UiZ7Wv0Bt-XmN2Ie-oVWyLMSo,8276
7
- python_watcher-16.0.0.dist-info/licenses/LICENSE,sha256=XfKg2H1sVi8OoRxoisUlMqoo10TKvHmU_wU39ks7MyA,10143
1
+ python_watcher-16.0.1.data/data/etc/apache2/watcher,sha256=urybYUTb0kAVIvbm4WK5oDfXR82QSK2lyMl4Z9dCz4Q,1190
2
+ python_watcher-16.0.1.data/data/etc/watcher/README-watcher.conf.txt,sha256=EIa_sjXDyl1XSg0ovsFwUjqw0euS0N2PYn4HtyBGJME,131
3
+ python_watcher-16.0.1.data/data/etc/watcher/oslo-config-generator/watcher.conf,sha256=ZN93-gX9IPi8a2_vO_Wl_GRjafnO2Xm7oEk2ou9Fv04,486
4
+ python_watcher-16.0.1.data/data/etc/watcher/oslo-policy-generator/watcher-policy-generator.conf,sha256=ZfIuVdtrj7IZm2BrRQvAW3JOL_kqCiILf-vBmwX3ufw,76
5
+ python_watcher-16.0.1.data/scripts/watcher-api-wsgi,sha256=YcIpUO3iRVW6Fu3Md_ooAeHkmJJasG031naMdf8cv2A,1829
6
+ python_watcher-16.0.1.dist-info/licenses/AUTHORS,sha256=A38NQqsvLAj9OTI09_UiZ7Wv0Bt-XmN2Ie-oVWyLMSo,8276
7
+ python_watcher-16.0.1.dist-info/licenses/LICENSE,sha256=XfKg2H1sVi8OoRxoisUlMqoo10TKvHmU_wU39ks7MyA,10143
8
8
  watcher/__init__.py,sha256=Vs2olXPZ_IAV1rukHYRl3ITzYFBNXX9mIUtFXkMWYwg,640
9
9
  watcher/_i18n.py,sha256=fmiL4Nl4XcrhO07jHIk8fkzPaJBuK6pvYlNlSty2p_c,1269
10
10
  watcher/eventlet.py,sha256=z6WuDS9HJAq6vRZCg5ozYpWCs3lwhDuv9R70bj8tpQ0,1476
@@ -92,7 +92,7 @@ watcher/common/placement_helper.py,sha256=WtXNYCTRbs1JmrnHUXzjO3ebEyusnxLXHyeUSw
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=tHf3qI1nZ_bJnC3QXkD7zM_ld7zkeGTNbuXrAP9PWTY,2186
95
- watcher/common/service.py,sha256=E1GqlgtA4djzo6um3O6QEf5lrNpW2UH6CT-NJ1AObmM,13698
95
+ watcher/common/service.py,sha256=kdCLbEleIK1QCemedEYfb1djMwmmqK0J00AP3HHMj4k,13688
96
96
  watcher/common/service_manager.py,sha256=KT-sKrQqhoTX1QSlmPcPZo0OJYnoykEp2yxOJd1XTVk,1387
97
97
  watcher/common/utils.py,sha256=GC6rkrQlt5xmMhkdKogksXNzrQgvCbY6QNp-roFwnZ0,5948
98
98
  watcher/common/loader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -128,11 +128,11 @@ watcher/conf/db.py,sha256=TkdSUfqmq3s7M02lHhg2F3WNw_KVHXQtgYo-Quyz1J8,1317
128
128
  watcher/conf/decision_engine.py,sha256=A1ZfeKdKCIAJ1B8122FHA3sCBiafVBy7fVezIniZ-KQ,4030
129
129
  watcher/conf/exception.py,sha256=k6BRqb7z27gndxuZffATm0b8qACzh1co2luJy3AJx2w,963
130
130
  watcher/conf/gnocchi_client.py,sha256=NXD_ZSTobVOawUtXzd5uo4yn7cbSJbzFe9wNt9LcW-4,1563
131
- watcher/conf/grafana_client.py,sha256=7F3F0zDdnOdi4Ju_jSLVToB5j26gjxTDAewWjJUanyw,6882
131
+ watcher/conf/grafana_client.py,sha256=7JaaCyImzgwG5m5zZoKqyFUE0_txdx5J6QcyYWSjpv4,6910
132
132
  watcher/conf/grafana_translators.py,sha256=-7I2lYtm6MGmDGteFR9TDWWyTby29vd8pLnWROAvYi8,1625
133
133
  watcher/conf/ironic_client.py,sha256=6dItPh2QBVvbWmejnC_oZr4W_vEEi1wDhp74Cu_tCfo,1561
134
134
  watcher/conf/keystone_client.py,sha256=mbMBo4nhZoKngAQNbqXvrzh1W3opULQ-4ETgFMk_ZGo,1324
135
- watcher/conf/maas_client.py,sha256=7KGb3yIKWDvCdYA5Kc22uszkFEOp9rcJQHpQFES2dfA,1277
135
+ watcher/conf/maas_client.py,sha256=99UaXMNg_1GPyfU2q4TjjHpRmI6zAStcrYzvo6lDyyg,1305
136
136
  watcher/conf/models.py,sha256=Fo1zQMcUjtEIroMZ4mx3kRLr2A_-3JldMZJQZ7SKIko,1390
137
137
  watcher/conf/nova.py,sha256=4fjc72SVNVlWoVZfXvX6HBaZu4SkuBgLbi3Mb9jc2nU,4143
138
138
  watcher/conf/nova_client.py,sha256=IWvE0jWfqD7ttpr00hVJhHBXyP4fVo6a7rI53k6ySBo,2764
@@ -208,7 +208,7 @@ watcher/decision_engine/model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
208
208
  watcher/decision_engine/model/base.py,sha256=fKQtaGLAaT0OaLxObKMa_MFVNPO_os9E3cYFyWyORSU,1097
209
209
  watcher/decision_engine/model/model_root.py,sha256=iDMGHBmJuoB5R04jRVIFGJPFSDRum2oCphcJE2e6SKA,24893
210
210
  watcher/decision_engine/model/collector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
211
- watcher/decision_engine/model/collector/base.py,sha256=ORrRnmejMbpBhMprjrDIqxxZgq6mRfeWEugDkNqiMXk,9652
211
+ watcher/decision_engine/model/collector/base.py,sha256=A4TyXdlwvKIABvR5sDO_GkCPWoDqYS9nlHvCSFnoCWY,9891
212
212
  watcher/decision_engine/model/collector/cinder.py,sha256=nlkdeBdnsV4OcDzYx18IQ_G9mNfh-bSfB7yKkPjeJX4,11797
213
213
  watcher/decision_engine/model/collector/ironic.py,sha256=BPMLbsilwwxl2JihK7Kbxx82FAAvrF5tS7DSKrNRAT4,4229
214
214
  watcher/decision_engine/model/collector/manager.py,sha256=kaX4iX9JbpXdp_aQbMTXL-B_D0ohm0OAl8cwjOpOSa4,3229
@@ -222,10 +222,10 @@ watcher/decision_engine/model/element/node.py,sha256=vuyb6BsF61a2Zx-wskFoK2T5A3E
222
222
  watcher/decision_engine/model/element/storage_resource.py,sha256=SxEANRSpzOoN08hzVip-Tw-zB6CGea2iIZRRFHHsAQI,902
223
223
  watcher/decision_engine/model/element/volume.py,sha256=Jsscli7Y4hYzNgBB1e1xR7_UNcSiEVjiVuarkT61NGk,1830
224
224
  watcher/decision_engine/model/notification/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
225
- watcher/decision_engine/model/notification/base.py,sha256=CpjE3ECVJFn7bhLZ-I8dx3XwLXrYvqOsQftt678cah4,1060
226
- watcher/decision_engine/model/notification/cinder.py,sha256=gD3z5l2paEAHtDpKT3Y-3shcRR09zfkNKiWjEQc1DiU,15013
225
+ watcher/decision_engine/model/notification/base.py,sha256=I5ot0ycC3MKtAJ4WAxTGRpW5UrEWzLiT3bq0WHHXkgw,1796
226
+ watcher/decision_engine/model/notification/cinder.py,sha256=DsD7M0lZ4luJtzx4j7OMRzpxg4KHb9Zu1s8fIF7WHpk,15045
227
227
  watcher/decision_engine/model/notification/filtering.py,sha256=XS7l9bSnM-UEYWxqeIxAhQHHlDdsVtRH1ZyqDdy3n3Q,2861
228
- watcher/decision_engine/model/notification/nova.py,sha256=hUPJQWvgVxahdrWni8aiZPZsBQ9P8JBFjS6jZ4WacPs,15223
228
+ watcher/decision_engine/model/notification/nova.py,sha256=x7kGz1QZy7FiUfXbZnlgdk1nznk-jgboaYSODlD-qgE,15231
229
229
  watcher/decision_engine/planner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
230
230
  watcher/decision_engine/planner/base.py,sha256=BqyOPXKt5LVyiyAqEtOqvxojSi_NWtMTaMYPsfEPA9g,3043
231
231
  watcher/decision_engine/planner/manager.py,sha256=vznpcCBGnAChEevX_X32v0wuBuNlsYigV4Qc7GF3PYY,992
@@ -272,7 +272,7 @@ watcher/decision_engine/strategy/strategies/saving_energy.py,sha256=dpl5vjXlFCd1
272
272
  watcher/decision_engine/strategy/strategies/storage_capacity_balance.py,sha256=7wOCAlSlBccRew6uzlZSU4NNsyN1aW44163DMLuLgpI,15588
273
273
  watcher/decision_engine/strategy/strategies/uniform_airflow.py,sha256=f1TERvtj7ZcL9NOUcQX2FYd77o4aFWRsjOj3YxdcxS0,13345
274
274
  watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py,sha256=cQLAppqzAGskGEE8VkJFGHzEA6XAPqyoXtg9OmzghXw,27135
275
- watcher/decision_engine/strategy/strategies/workload_balance.py,sha256=GQkai1p7UlkwOucFQcVAUPTqpA6IHl5ArhDBNPfNV2E,15846
275
+ watcher/decision_engine/strategy/strategies/workload_balance.py,sha256=taEvebcCZL7s-9z2goGHHOpP5PUE8JktfDRkrFdv04I,17596
276
276
  watcher/decision_engine/strategy/strategies/workload_stabilization.py,sha256=0Sg_WSEHbypRuclkDRC9rLAzH6v3T-wr_lMy4jtEU1U,25392
277
277
  watcher/decision_engine/strategy/strategies/zone_migration.py,sha256=Uj1hLXQO2o67OkVF-6BZDx46wJUIpBk4ShIxcqBQWKo,36168
278
278
  watcher/hacking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -377,7 +377,7 @@ watcher/tests/unit/common/test_nova_helper.py,sha256=3Hqu1IZ20RGg3foFgV3YpyNLbSX
377
377
  watcher/tests/unit/common/test_oslo_service_helper.py,sha256=X3M23ld-l4dB3HeGf-QGYUGvGWq0pvrbMjtu5OWYO94,1682
378
378
  watcher/tests/unit/common/test_placement_helper.py,sha256=wXxvNjwCVerTC8uyjbHXgSHJ4Ajr5pCYP2FexlyXSFc,11740
379
379
  watcher/tests/unit/common/test_scheduling.py,sha256=qKGYuwBmTit0tmAPNAfxSfoWh67cnlfOKdde9shq9SI,2240
380
- watcher/tests/unit/common/test_service.py,sha256=0siJCx2sY3pVej2C42YjK14pHzpGaFhGJkxN2TPfWjw,15251
380
+ watcher/tests/unit/common/test_service.py,sha256=4BQ--qhdBoDqnVxcHltJP3YLya1JQb6KbHjU-d726SQ,16117
381
381
  watcher/tests/unit/common/test_utils.py,sha256=P718Ob2VG9ps3rl2go_SKTeL3lYI-AKL1YfOxuLLUno,1702
382
382
  watcher/tests/unit/common/utils.py,sha256=zkkQeeR9vmE7drsvGMN8q4sXbluCi0KSL23A2puOtx8,6169
383
383
  watcher/tests/unit/common/loader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -419,7 +419,7 @@ watcher/tests/unit/decision_engine/audit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5
419
419
  watcher/tests/unit/decision_engine/audit/test_audit_handlers.py,sha256=wVTW722VhoWNx17jEBYyf6ZHu5bF7LBG7uouC_8NwZY,25408
420
420
  watcher/tests/unit/decision_engine/cluster/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
421
421
  watcher/tests/unit/decision_engine/cluster/test_cinder_cdmc.py,sha256=uPPBs5mpSM0U3csQ14eMw5cMXsALMREHbRyOdtzN9F8,5259
422
- watcher/tests/unit/decision_engine/cluster/test_cluster_data_model_collector.py,sha256=c0bGfkCGPPrISk5X0dNOratz9DyGjs2vCiTjGBQl9ao,3013
422
+ watcher/tests/unit/decision_engine/cluster/test_cluster_data_model_collector.py,sha256=VQ-5NfmpqtzwUCv8wEJwRJZL3E-afBFj_L5pTbxJQfo,5770
423
423
  watcher/tests/unit/decision_engine/cluster/test_nova_cdmc.py,sha256=QeMXtRitVMpK7wZGWkPrURVIGQRdnwEK9AKyusRdmpI,32796
424
424
  watcher/tests/unit/decision_engine/datasources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
425
425
  watcher/tests/unit/decision_engine/datasources/test_aetos_helper.py,sha256=xMYx5V-3j-DHodQXTHmQnFWxBCP0hlRknXlQy97X_tU,2138
@@ -469,8 +469,8 @@ watcher/tests/unit/decision_engine/model/data/storage_scenario_1.xml,sha256=bB_G
469
469
  watcher/tests/unit/decision_engine/model/notification/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
470
470
  watcher/tests/unit/decision_engine/model/notification/fake_managers.py,sha256=CTVikb7tmRPcYNPnbRkMZYPljrwloq9szEadb1ueBaE,2287
471
471
  watcher/tests/unit/decision_engine/model/notification/test_cinder_notifications.py,sha256=gOCS1hwnv4lNJMxXLa-EMwOIGIuvgLHJxOLxN9hw4kE,30558
472
- watcher/tests/unit/decision_engine/model/notification/test_notifications.py,sha256=-RaWNiD2E0vRl5ICIoHO_Gy8C_19il9hJRDk9thu5kk,3669
473
- watcher/tests/unit/decision_engine/model/notification/test_nova_notifications.py,sha256=PtU0AracAVgGj1bIgwzbtytMfzuAK0LOvlnQJfCFa4c,38650
472
+ watcher/tests/unit/decision_engine/model/notification/test_notifications.py,sha256=icZH8XjoS8nWFsn6ZjKBx0bsB7_ESKrg_g0kyeDn4Vg,3677
473
+ watcher/tests/unit/decision_engine/model/notification/test_nova_notifications.py,sha256=zq5bnJj0Yj21qI50DmYTY_dYo1hSUKCOQ5mU-LSNkVE,39050
474
474
  watcher/tests/unit/decision_engine/model/notification/data/capacity.json,sha256=CA-SuGK3Pu0guK9YKg-bR1a_Ek0QOPCG-EeyqTXYadw,318
475
475
  watcher/tests/unit/decision_engine/model/notification/data/instance-create-end.json,sha256=aWw_hqFNqosDisDNqPkwlEwxS3fGWtnu6mnYnuMxy0U,4171
476
476
  watcher/tests/unit/decision_engine/model/notification/data/instance-delete-end.json,sha256=sgRs0KW-DGncU59d2e5PvG7CsSPYAwKxnOWeV7bx0BI,1726
@@ -551,7 +551,7 @@ watcher/tests/unit/decision_engine/strategy/strategies/test_storage_capacity_bal
551
551
  watcher/tests/unit/decision_engine/strategy/strategies/test_strategy_endpoint.py,sha256=kbScw8qxt_Rw1eb5we5b6PgZ7VeqK007l2ogLddg4lA,2865
552
552
  watcher/tests/unit/decision_engine/strategy/strategies/test_uniform_airflow.py,sha256=GLrs8Le9xY7CUaZWbJEYl7vvTOi0X_K4stqLoNRLYWc,7225
553
553
  watcher/tests/unit/decision_engine/strategy/strategies/test_vm_workload_consolidation.py,sha256=KU7IDQeF3vspveqFlYqZ-tmtn6cvgqhv2BSJFjnZ5Ec,19154
554
- watcher/tests/unit/decision_engine/strategy/strategies/test_workload_balance.py,sha256=tqIEBlGqqpA9RF6TPFrILSGfz2Wvx7ELnnvffRfiYjE,8339
554
+ watcher/tests/unit/decision_engine/strategy/strategies/test_workload_balance.py,sha256=FG-FzlaIOPIzUUibGSlG7r_nCcm0oYEnbdTysgsRBYQ,8555
555
555
  watcher/tests/unit/decision_engine/strategy/strategies/test_workload_stabilization.py,sha256=U7dMJuSlcXhTB7wsoGl0LHQhpW6pbmUrGHf4J0GrObw,14556
556
556
  watcher/tests/unit/decision_engine/strategy/strategies/test_zone_migration.py,sha256=aBsI2pSQJWlC3-qReJvmc7yFtdF-6TLw8Bn_EaHv6iA,85348
557
557
  watcher/tests/unit/notifications/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -575,9 +575,9 @@ watcher/tests/unit/objects/test_strategy.py,sha256=xPMHVzmcKaMaCluF7IG891266Ylzz
575
575
  watcher/tests/unit/objects/utils.py,sha256=K2L0PRggr5_iwPyxLvqonL2y10EMAp2FYnhpJCu78aU,8063
576
576
  watcher/wsgi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
577
577
  watcher/wsgi/api.py,sha256=F1s1Q_CMuRdgS_f7Gl3XzavjZqdxqO9IHLxSWl6NOO4,789
578
- python_watcher-16.0.0.dist-info/METADATA,sha256=G4-VxxpVhSaVkvSIF0a84rS7k1O-QMTXkkTbOTH-gNA,3895
579
- python_watcher-16.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
580
- python_watcher-16.0.0.dist-info/entry_points.txt,sha256=TAIOgRZMToICTZ6FEGRIRfitQGXlta1r_TUvRc8iv0Y,4542
581
- python_watcher-16.0.0.dist-info/pbr.json,sha256=H1or3vocXQIvOqKI-UHnLBwCzS6ZXUgPWhsPGukInh4,47
582
- python_watcher-16.0.0.dist-info/top_level.txt,sha256=O2B7Sbzshq5s-tZxb2a7VETvMDiJItSYieAiJ6Jm5yY,8
583
- python_watcher-16.0.0.dist-info/RECORD,,
578
+ python_watcher-16.0.1.dist-info/METADATA,sha256=FKxXjHp0iGB7G12hz6QoJ6Dz9IvEy1vdPLgHGHjRw0Q,3895
579
+ python_watcher-16.0.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
580
+ python_watcher-16.0.1.dist-info/entry_points.txt,sha256=TAIOgRZMToICTZ6FEGRIRfitQGXlta1r_TUvRc8iv0Y,4542
581
+ python_watcher-16.0.1.dist-info/pbr.json,sha256=8CUskEFHoMYPz_b6C9AEAB_Ppo8tlDtUeeepmzPtMBQ,47
582
+ python_watcher-16.0.1.dist-info/top_level.txt,sha256=O2B7Sbzshq5s-tZxb2a7VETvMDiJItSYieAiJ6Jm5yY,8
583
+ python_watcher-16.0.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": "05aa7074", "is_release": true}
watcher/common/service.py CHANGED
@@ -240,7 +240,7 @@ class Service(service.ServiceBase):
240
240
  )
241
241
 
242
242
  def start(self):
243
- LOG.debug("Connecting to '%s'", CONF.transport_url)
243
+ LOG.debug("Connecting to messaging transport")
244
244
  if self.conductor_topic_handler:
245
245
  self.conductor_topic_handler.start()
246
246
  if self.notification_handler:
@@ -249,7 +249,7 @@ class Service(service.ServiceBase):
249
249
  self.heartbeat.start()
250
250
 
251
251
  def stop(self):
252
- LOG.debug("Disconnecting from '%s'", CONF.transport_url)
252
+ LOG.debug("Disconnecting from messaging transport")
253
253
  if self.conductor_topic_handler:
254
254
  self.conductor_topic_handler.stop()
255
255
  if self.notification_handler:
@@ -28,6 +28,7 @@ GRAFANA_CLIENT_OPTS = [
28
28
  # A similar structure to the database_map would solve this.
29
29
  cfg.StrOpt('token',
30
30
  default=None,
31
+ secret=True,
31
32
  help="Authentication token to gain access"),
32
33
  # TODO(Dantali0n) each individual metric could have its own base url.
33
34
  # 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,
@@ -127,6 +127,7 @@ class BaseClusterDataModelCollector(loadable.LoadableSingleton,
127
127
  super().__init__(config)
128
128
  self.osc = osc if osc else clients.OpenStackClients()
129
129
  self.lock = threading.RLock()
130
+ self.sync_lock = threading.RLock()
130
131
  self._audit_scope_handler = None
131
132
  self._cluster_data_model = None
132
133
  self._data_model_scope = None
@@ -188,13 +189,16 @@ class BaseClusterDataModelCollector(loadable.LoadableSingleton,
188
189
  """Synchronize the cluster data model
189
190
 
190
191
  Whenever called this synchronization will perform a drop-in replacement
191
- with the existing cluster data model
192
+ with the existing cluster data model. The sync lock is held for the
193
+ full duration so notification handlers wait until the new model is
194
+ current before applying updates.
192
195
  """
193
- try:
194
- self.cluster_data_model = self.execute()
195
- except Exception as e:
196
- LOG.exception(e)
197
- self.set_cluster_data_model_as_stale()
196
+ with self.sync_lock:
197
+ try:
198
+ self.cluster_data_model = self.execute()
199
+ except Exception as e:
200
+ LOG.exception(e)
201
+ self.set_cluster_data_model_as_stale()
198
202
 
199
203
 
200
204
  class BaseModelBuilder:
@@ -17,6 +17,11 @@
17
17
 
18
18
  import abc
19
19
 
20
+ from oslo_log import log
21
+
22
+
23
+ LOG = log.getLogger(__name__)
24
+
20
25
 
21
26
  class NotificationEndpoint(metaclass=abc.ABCMeta):
22
27
 
@@ -34,3 +39,20 @@ class NotificationEndpoint(metaclass=abc.ABCMeta):
34
39
  @property
35
40
  def cluster_data_model(self):
36
41
  return self.collector.cluster_data_model
42
+
43
+ def info(self, ctxt, publisher_id, event_type, payload, metadata):
44
+ """oslo.messaging entry point.
45
+
46
+ Acquires the collector sync lock before processing to ensure
47
+ notifications are never applied to a model that is about to be
48
+ replaced by an in-progress synchronization.
49
+ """
50
+ with self.collector.sync_lock:
51
+ self.process_info(
52
+ ctxt, publisher_id, event_type, payload, metadata
53
+ )
54
+
55
+ @abc.abstractmethod
56
+ def process_info(self, ctxt, publisher_id, event_type, payload, metadata):
57
+ """Process the notification. Subclasses must implement this."""
58
+ raise NotImplementedError()
@@ -250,7 +250,7 @@ class CapacityNotificationEndpoint(CinderNotification):
250
250
  event_type='capacity.pool',
251
251
  )
252
252
 
253
- def info(self, ctxt, publisher_id, event_type, payload, metadata):
253
+ def process_info(self, ctxt, publisher_id, event_type, payload, metadata):
254
254
  ctxt.request_id = metadata['message_id']
255
255
  ctxt.project_domain = event_type
256
256
  LOG.info("Event '%(event)s' received from %(publisher)s "
@@ -285,7 +285,7 @@ class VolumeCreateEnd(VolumeNotificationEndpoint):
285
285
  event_type='volume.create.end',
286
286
  )
287
287
 
288
- def info(self, ctxt, publisher_id, event_type, payload, metadata):
288
+ def process_info(self, ctxt, publisher_id, event_type, payload, metadata):
289
289
  ctxt.request_id = metadata['message_id']
290
290
  ctxt.project_domain = event_type
291
291
  LOG.info("Event '%(event)s' received from %(publisher)s "
@@ -314,7 +314,7 @@ class VolumeUpdateEnd(VolumeNotificationEndpoint):
314
314
  event_type='volume.update.end',
315
315
  )
316
316
 
317
- def info(self, ctxt, publisher_id, event_type, payload, metadata):
317
+ def process_info(self, ctxt, publisher_id, event_type, payload, metadata):
318
318
  ctxt.request_id = metadata['message_id']
319
319
  ctxt.project_domain = event_type
320
320
  LOG.info("Event '%(event)s' received from %(publisher)s "
@@ -376,7 +376,7 @@ class VolumeDeleteEnd(VolumeNotificationEndpoint):
376
376
  event_type='volume.delete.end',
377
377
  )
378
378
 
379
- def info(self, ctxt, publisher_id, event_type, payload, metadata):
379
+ def process_info(self, ctxt, publisher_id, event_type, payload, metadata):
380
380
  ctxt.request_id = metadata['message_id']
381
381
  ctxt.project_domain = event_type
382
382
  LOG.info("Event '%(event)s' received from %(publisher)s "
@@ -357,7 +357,7 @@ class VersionedNotification(NovaNotification):
357
357
  publisher_id=self.publisher_id_regex,
358
358
  )
359
359
 
360
- def info(self, ctxt, publisher_id, event_type, payload, metadata):
360
+ def process_info(self, ctxt, publisher_id, event_type, payload, metadata):
361
361
  LOG.info("Event '%(event)s' received from %(publisher)s "
362
362
  "with metadata %(metadata)s",
363
363
  dict(event=event_type,
@@ -192,33 +192,77 @@ class WorkloadBalance(base.WorkloadStabilizationBaseStrategy):
192
192
  workload = instance_data['workload']
193
193
  # calculate the available resources
194
194
  free_res = self.compute_model.get_node_free_resources(host)
195
- if (free_res['vcpu'] >= required_cores and
196
- free_res['memory'] >= required_mem and
197
- free_res['disk'] >= required_disk):
198
- if self._meter == 'instance_cpu_usage':
199
- usage = src_instance_workload + workload
200
- usage_percent = usage / host.vcpus * 100
201
- limit = self.threshold / 100 * host.vcpus
202
- if usage < limit:
203
- destination_hosts.append(instance_data)
204
- LOG.debug(f"Host {host.hostname} evaluated as destination "
205
- f"for {instance_to_migrate.uuid}. Host usage "
206
- f"for cpu would be {usage_percent}."
207
- f"The threshold is: {self.threshold}. "
208
- f"selected: {usage < limit}"
209
- )
210
- if self._meter == 'instance_ram_usage':
211
- usage = src_instance_workload + workload
212
- usage_percent = usage / host.memory * 100
213
- limit = self.threshold / 100 * host.memory
214
- if usage < limit:
215
- destination_hosts.append(instance_data)
216
- LOG.debug(f"Host {host.hostname} evaluated as destination "
217
- f"for {instance_to_migrate.uuid}. Host usage "
218
- f"for ram would be {usage_percent}."
219
- f"The threshold is: {self.threshold}. "
220
- f"selected: {usage < limit}"
221
- )
195
+ if free_res['vcpu'] < required_cores:
196
+ LOG.debug(
197
+ "Host %(host)s rejected for instance %(instance)s: "
198
+ "insufficient vCPUs (available: %(available)s, "
199
+ "required: %(required)s)",
200
+ dict(
201
+ host=host.hostname,
202
+ instance=instance_to_migrate.uuid,
203
+ available=free_res['vcpu'],
204
+ required=required_cores,
205
+ ),
206
+ )
207
+ continue
208
+ if free_res['memory'] < required_mem:
209
+ LOG.debug(
210
+ "Host %(host)s rejected for instance %(instance)s: "
211
+ "insufficient memory (available: %(available)s MB, "
212
+ "required: %(required)s MB)",
213
+ dict(
214
+ host=host.hostname,
215
+ instance=instance_to_migrate.uuid,
216
+ available=free_res['memory'],
217
+ required=required_mem,
218
+ ),
219
+ )
220
+ continue
221
+ if free_res['disk'] < required_disk:
222
+ LOG.debug(
223
+ "Host %(host)s rejected for instance %(instance)s: "
224
+ "insufficient disk (available: %(available)s GB, "
225
+ "required: %(required)s GB)",
226
+ dict(
227
+ host=host.hostname,
228
+ instance=instance_to_migrate.uuid,
229
+ available=free_res['disk'],
230
+ required=required_disk,
231
+ ),
232
+ )
233
+ continue
234
+ if self._meter == 'instance_cpu_usage':
235
+ usage = src_instance_workload + workload
236
+ usage_percent = usage / host.vcpus * 100
237
+ limit = self.threshold / 100 * host.vcpus
238
+ if usage < limit:
239
+ destination_hosts.append(instance_data)
240
+ LOG.debug(
241
+ "Host %s evaluated as destination for %s. "
242
+ "Host usage for cpu would be %s."
243
+ "The threshold is: %s. selected: %s",
244
+ host.hostname,
245
+ instance_to_migrate.uuid,
246
+ usage_percent,
247
+ self.threshold,
248
+ usage < limit,
249
+ )
250
+ if self._meter == 'instance_ram_usage':
251
+ usage = src_instance_workload + workload
252
+ usage_percent = usage / host.memory * 100
253
+ limit = self.threshold / 100 * host.memory
254
+ if usage < limit:
255
+ destination_hosts.append(instance_data)
256
+ LOG.debug(
257
+ "Host %s evaluated as destination for %s. "
258
+ "Host usage for ram would be %s."
259
+ "The threshold is: %s. selected: %s",
260
+ host.hostname,
261
+ instance_to_migrate.uuid,
262
+ usage_percent,
263
+ self.threshold,
264
+ usage < limit,
265
+ )
222
266
  return destination_hosts
223
267
 
224
268
  def group_hosts_by_cpu_or_ram_util(self):
@@ -326,6 +370,10 @@ class WorkloadBalance(base.WorkloadStabilizationBaseStrategy):
326
370
  instance_to_migrate = self.choose_instance_to_migrate(
327
371
  source_nodes, avg_workload, workload_cache)
328
372
  if not instance_to_migrate:
373
+ LOG.debug(
374
+ "No suitable instance found to migrate from "
375
+ "any overloaded source node"
376
+ )
329
377
  return self.solution
330
378
  source_node, instance_src = instance_to_migrate
331
379
  # find the hosts that have enough resource for the VM to be migrated
@@ -334,9 +382,12 @@ class WorkloadBalance(base.WorkloadStabilizationBaseStrategy):
334
382
  # sort the filtered result by workload
335
383
  # pick up the lowest one as dest server
336
384
  if not destination_hosts:
337
- # for instance.
338
- LOG.warning("No proper target host could be found, it might "
339
- "be because of there's no enough CPU/Memory/DISK")
385
+ LOG.warning(
386
+ "No proper target host could be found for instance "
387
+ "%(instance)s. Check debug logs for per-host rejection "
388
+ "details.",
389
+ dict(instance=instance_src.uuid),
390
+ )
340
391
  return self.solution
341
392
  destination_hosts = sorted(destination_hosts,
342
393
  key=lambda x: (x[self._meter]))
@@ -108,6 +108,28 @@ class TestService(base.TestCase):
108
108
  dummy_service.conductor_topic_handler,
109
109
  om.rpc.server.RPCServer)
110
110
 
111
+ @mock.patch.object(service.LOG, 'debug', autospec=True)
112
+ def test_transport_url_not_logged(self, mock_debug):
113
+ transport_url = 'rabbit://api:secret-pass@example.com:5672/'
114
+ CONF.set_override('transport_url', transport_url)
115
+
116
+ dummy_service = service.Service(DummyManager)
117
+ dummy_service.conductor_topic_handler = None
118
+ dummy_service.notification_handler = None
119
+ dummy_service.heartbeat = None
120
+
121
+ dummy_service.start()
122
+ dummy_service.stop()
123
+
124
+ logged = ' '.join(
125
+ str(arg) for call in mock_debug.call_args_list for arg in call.args
126
+ )
127
+
128
+ self.assertNotIn(transport_url, logged)
129
+ self.assertNotIn('secret-pass', logged)
130
+ mock_debug.assert_any_call("Connecting to messaging transport")
131
+ mock_debug.assert_any_call("Disconnecting from messaging transport")
132
+
111
133
 
112
134
  class TestServiceMonitoringBase(db_base.DbTestCase):
113
135
 
@@ -13,6 +13,8 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
 
16
+ import threading
17
+
16
18
  from unittest import mock
17
19
 
18
20
  from watcher.decision_engine.model.collector import base
@@ -20,6 +22,7 @@ from watcher.decision_engine.model.collector import cinder
20
22
  from watcher.decision_engine.model.collector import ironic
21
23
  from watcher.decision_engine.model.collector import nova
22
24
  from watcher.decision_engine.model import model_root
25
+ from watcher.decision_engine.model.notification import base as notif_base
23
26
  from watcher.tests.unit import base as test_base
24
27
 
25
28
 
@@ -59,6 +62,77 @@ class TestClusterDataModelCollector(test_base.TestCase):
59
62
  collector.get_latest_cluster_data_model())
60
63
 
61
64
 
65
+ class TestSyncLockNotificationRace(test_base.TestCase):
66
+ """Regression test for notification updates lost during synchronization.
67
+
68
+ When synchronize() is rebuilding the model via execute(), a concurrent
69
+ notification must not apply its update to the old model that will be
70
+ discarded. Without sync_lock in synchronize() and in
71
+ NotificationEndpoint.info(), the notification races execute(), calls
72
+ add_node() on old_model, and the update is silently lost when
73
+ synchronize() replaces _cluster_data_model with new_model.
74
+
75
+ This test fails without the fix and passes once sync_lock is added to
76
+ both synchronize() and NotificationEndpoint.info().
77
+ """
78
+
79
+ def test_notification_node_reaches_new_model_not_old(self):
80
+ collector = DummyClusterDataModelCollector(config=mock.Mock())
81
+
82
+ old_model = mock.Mock(spec=model_root.ModelRoot)
83
+ new_model = mock.Mock(spec=model_root.ModelRoot)
84
+ collector._cluster_data_model = old_model
85
+
86
+ execute_started = threading.Event()
87
+ execute_may_finish = threading.Event()
88
+
89
+ def slow_execute():
90
+ # execute start to build a new model
91
+ execute_started.set()
92
+ # Event to trigger the end of execute()
93
+ execute_may_finish.wait(timeout=5)
94
+ return new_model
95
+
96
+ dummy_node = mock.Mock()
97
+
98
+ # Notification endpoint that adds dummy_node to whatever model
99
+ # is current at info() time.
100
+ class AddNodeEndpoint(notif_base.NotificationEndpoint):
101
+ @property
102
+ def filter_rule(self):
103
+ return None
104
+
105
+ def process_info(
106
+ self, ctxt, publisher_id, event_type, payload, metadata
107
+ ):
108
+ self.cluster_data_model.add_node(dummy_node)
109
+
110
+ endpoint = AddNodeEndpoint(collector)
111
+
112
+ with mock.patch.object(collector, 'execute', side_effect=slow_execute):
113
+ sync_thread = threading.Thread(
114
+ target=collector.synchronize, daemon=True
115
+ )
116
+ sync_thread.start()
117
+ execute_started.wait(timeout=3)
118
+
119
+ # Notification fires while execute() is still running.
120
+ notif_thread = threading.Thread(
121
+ target=endpoint.info,
122
+ args=(mock.Mock(), 'pub', 'event', {}, {}),
123
+ daemon=True,
124
+ )
125
+ notif_thread.start()
126
+
127
+ # finish execute() processing
128
+ execute_may_finish.set()
129
+ sync_thread.join(timeout=3)
130
+ notif_thread.join(timeout=3)
131
+
132
+ new_model.add_node.assert_called_once_with(dummy_node)
133
+ old_model.add_node.assert_not_called()
134
+
135
+
62
136
  class TestComputeDataModelCollector(test_base.TestCase):
63
137
 
64
138
  def test_model_scope_is_none(self):
@@ -45,7 +45,7 @@ class DummyNotification(base.NotificationEndpoint):
45
45
  payload={'data': {'nested': r'^T.*'}},
46
46
  )
47
47
 
48
- def info(self, ctxt, publisher_id, event_type, payload, metadata):
48
+ def process_info(self, ctxt, publisher_id, event_type, payload, metadata):
49
49
  pass
50
50
 
51
51
 
@@ -27,6 +27,7 @@ from watcher.common import exception
27
27
  from watcher.common import nova_helper
28
28
  from watcher.common import placement_helper
29
29
  from watcher.common import service as watcher_service
30
+ from watcher.decision_engine.model.collector import base
30
31
  from watcher.decision_engine.model import element
31
32
  from watcher.decision_engine.model.notification import nova as novanotification
32
33
  from watcher.tests.unit import base as base_test
@@ -111,6 +112,19 @@ class TestReceiveNovaNotifications(NotificationTestCase):
111
112
  expected_message, self.FAKE_METADATA)
112
113
 
113
114
 
115
+ class DummyClusterDataModelCollector(base.BaseClusterDataModelCollector):
116
+ @property
117
+ def notification_endpoints(self):
118
+ return []
119
+
120
+ def get_audit_scope_handler(self, audit_scope):
121
+ return None
122
+
123
+ def execute(self):
124
+ # Return none since there are not audits
125
+ return
126
+
127
+
114
128
  @ddt.ddt
115
129
  class TestNovaNotifications(NotificationTestCase):
116
130
 
@@ -906,8 +920,9 @@ class TestNovaNotifications(NotificationTestCase):
906
920
  def test_info_no_cdm(self):
907
921
  # Tests that a notification is received before an audit has been
908
922
  # performed which would create the nova CDM.
909
- mock_collector = mock.Mock(cluster_data_model=None)
910
- handler = novanotification.VersionedNotification(mock_collector)
923
+ m_config = mock.Mock()
924
+ _collector = DummyClusterDataModelCollector(config=m_config)
925
+ handler = novanotification.VersionedNotification(_collector)
911
926
  payload = {
912
927
  'nova_object.data': {
913
928
  'uuid': '9966d6bd-a45c-4e1c-9d57-3054899a3ec7',
@@ -117,10 +117,17 @@ class TestWorkloadBalance(TestBaseStrategy):
117
117
  'Higher than threshold 25.0: True'),
118
118
  mock.call('Host usage for Node_1: host_cpu_usage_percent is 7.5. '
119
119
  'Higher than threshold 25.0: False'),
120
- mock.call('Host hostname_1 evaluated as destination for '
121
- '73b09e16-35b7-4922-804e-e8f5d9b740fc. Host usage for '
122
- 'cpu would be 20.0.The threshold is: 25.0. selected: '
123
- 'True')]
120
+ mock.call(
121
+ 'Host %s evaluated as destination for %s. '
122
+ 'Host usage for cpu would be %s.'
123
+ 'The threshold is: %s. selected: %s',
124
+ 'hostname_1',
125
+ '73b09e16-35b7-4922-804e-e8f5d9b740fc',
126
+ 20.0,
127
+ 25.0,
128
+ True,
129
+ ),
130
+ ]
124
131
  mock_debug.assert_has_calls(expected_calls, any_order=True)
125
132
 
126
133
  @mock.patch.object(workload_balance.LOG, 'debug', autospec=True)
@@ -143,10 +150,17 @@ class TestWorkloadBalance(TestBaseStrategy):
143
150
  '37.121212121212125. Higher than threshold 30.0: True'),
144
151
  mock.call('Host usage for Node_1: host_ram_usage_percent is '
145
152
  '18.181818181818183. Higher than threshold 30.0: False'),
146
- mock.call('Host hostname_1 evaluated as destination for '
147
- '73b09e16-35b7-4922-804e-e8f5d9b740fc. Host usage for '
148
- 'ram would be 25.0.The threshold is: 30.0. selected: '
149
- 'True')]
153
+ mock.call(
154
+ 'Host %s evaluated as destination for %s. '
155
+ 'Host usage for ram would be %s.'
156
+ 'The threshold is: %s. selected: %s',
157
+ 'hostname_1',
158
+ '73b09e16-35b7-4922-804e-e8f5d9b740fc',
159
+ 25.0,
160
+ 30.0,
161
+ True,
162
+ ),
163
+ ]
150
164
  mock_debug.assert_has_calls(expected_calls, any_order=True)
151
165
 
152
166
  def test_execute_no_workload(self):
@@ -1 +0,0 @@
1
- {"git_version": "336ef360", "is_release": true}